Compare commits
8 Commits
nadro/adho
...
kurt-contr
Author | SHA1 | Date | |
---|---|---|---|
5474b3409e | |||
29ae16fbf0 | |||
38ee257996 | |||
47c29b2681 | |||
23f51d73ee | |||
f752a496de | |||
6545fb6db0 | |||
e63eb18d65 |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
18
package-lock.json
generated
@ -43,6 +43,7 @@
|
||||
"bson": "^6.10.3",
|
||||
"chokidar": "^4.0.3",
|
||||
"codemirror": "^6.0.1",
|
||||
"culori": "^4.0.2",
|
||||
"decamelize": "^6.0.0",
|
||||
"diff": "^7.0.0",
|
||||
"electron-updater": "^6.6.2",
|
||||
@ -92,6 +93,7 @@
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^15.0.7",
|
||||
"@types/culori": "^4.0.0",
|
||||
"@types/diff": "^7.0.2",
|
||||
"@types/electron": "^1.6.10",
|
||||
"@types/hammerjs": "^2.0.46",
|
||||
@ -7392,6 +7394,13 @@
|
||||
"integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/culori": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/culori/-/culori-4.0.0.tgz",
|
||||
"integrity": "sha512-aFljQwjb++sl6TAyEXeHTiK/fk9epZOQ+nMmadjnAvzZFIvNoQ0x8XQYfcOaRTBwmDUPUlghhZCJ66MTcqQAsg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/debug": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
||||
@ -11823,6 +11832,15 @@
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/culori": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/culori/-/culori-4.0.2.tgz",
|
||||
"integrity": "sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||
|
@ -45,6 +45,7 @@
|
||||
"bson": "^6.10.3",
|
||||
"chokidar": "^4.0.3",
|
||||
"codemirror": "^6.0.1",
|
||||
"culori": "^4.0.2",
|
||||
"decamelize": "^6.0.0",
|
||||
"diff": "^7.0.0",
|
||||
"electron-updater": "^6.6.2",
|
||||
@ -170,6 +171,7 @@
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^15.0.7",
|
||||
"@types/culori": "^4.0.0",
|
||||
"@types/diff": "^7.0.2",
|
||||
"@types/electron": "^1.6.10",
|
||||
"@types/hammerjs": "^2.0.46",
|
||||
|
@ -324,6 +324,8 @@ export class SceneEntities {
|
||||
group: segment,
|
||||
scale: factor,
|
||||
sceneInfra: this.sceneInfra,
|
||||
// Note: AST and code not available in onCamChange, so constraints won't be checked here
|
||||
// This is primarily for scaling changes
|
||||
})
|
||||
callBack && !err(callBack) && callbacks.push(callBack)
|
||||
if (segment.name === PROFILE_START) {
|
||||
@ -729,6 +731,8 @@ export class SceneEntities {
|
||||
scale,
|
||||
theme: this.sceneInfra._theme,
|
||||
isDraft: false,
|
||||
ast: maybeModdedAst,
|
||||
code: this.codeManager.code,
|
||||
})
|
||||
_profileStart.layers.set(SKETCH_LAYER)
|
||||
_profileStart.traverse((child) => {
|
||||
@ -866,6 +870,8 @@ export class SceneEntities {
|
||||
isSelected,
|
||||
sceneInfra: this.sceneInfra,
|
||||
selection,
|
||||
ast: maybeModdedAst,
|
||||
code: this.codeManager.code,
|
||||
})
|
||||
if (err(result)) return
|
||||
const { group: _group, updateOverlaysCallback } = result
|
||||
@ -3252,6 +3258,8 @@ export class SceneEntities {
|
||||
scale: factor,
|
||||
prevSegment: sgPaths[index - 1],
|
||||
sceneInfra: this.sceneInfra,
|
||||
ast: modifiedAst,
|
||||
code: this.codeManager.code,
|
||||
})
|
||||
if (callBack && !err(callBack)) return callBack
|
||||
|
||||
|
@ -76,12 +76,17 @@ import {
|
||||
} from '@src/clientSideScene/sceneUtils'
|
||||
import { angleLengthInfo } from '@src/components/Toolbar/angleLengthInfo'
|
||||
import type { Coords2d } from '@src/lang/std/sketch'
|
||||
import { getConstraintInfoKw } from '@src/lang/std/sketch'
|
||||
import type { SegmentInputs } from '@src/lang/std/stdTypes'
|
||||
import type { PathToNode } from '@src/lang/wasm'
|
||||
import type { PathToNode, Program } from '@src/lang/wasm'
|
||||
import { getTangentialArcToInfo } from '@src/lang/wasm'
|
||||
import { getNodeFromPath } from '@src/lang/queryAst'
|
||||
import type { Selections } from '@src/lib/selections'
|
||||
import type { Themes } from '@src/lib/theme'
|
||||
import { getThemeColorForThreeJs } from '@src/lib/theme'
|
||||
import {
|
||||
getThemeColorForThreeJs,
|
||||
getPrimaryColorForThreeJs,
|
||||
} from '@src/lib/theme'
|
||||
import { err } from '@src/lib/trap'
|
||||
import { isClockwise, normaliseAngle, roundOff } from '@src/lib/utils'
|
||||
import { getTangentPointFromPreviousArc } from '@src/lib/utils2d'
|
||||
@ -95,6 +100,7 @@ import toast from 'react-hot-toast'
|
||||
import { ARG_INTERIOR_ABSOLUTE } from '@src/lang/constants'
|
||||
|
||||
const ANGLE_INDICATOR_RADIUS = 30 // in px
|
||||
|
||||
interface CreateSegmentArgs {
|
||||
input: SegmentInputs
|
||||
prevSegment: Sketch['paths'][number]
|
||||
@ -108,6 +114,9 @@ interface CreateSegmentArgs {
|
||||
isSelected?: boolean
|
||||
sceneInfra: SceneInfra
|
||||
selection?: Selections
|
||||
// Add optional AST and code for constraint checking
|
||||
ast?: Program
|
||||
code?: string
|
||||
}
|
||||
|
||||
interface UpdateSegmentArgs {
|
||||
@ -116,6 +125,9 @@ interface UpdateSegmentArgs {
|
||||
group: Group
|
||||
sceneInfra: SceneInfra
|
||||
scale?: number
|
||||
// Add optional AST and code for constraint checking
|
||||
ast?: Program
|
||||
code?: string
|
||||
}
|
||||
|
||||
interface CreateSegmentResult {
|
||||
@ -144,6 +156,55 @@ export interface SegmentUtils {
|
||||
) => CreateSegmentResult['updateOverlaysCallback'] | Error
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a segment is fully constrained by examining all its constraint info
|
||||
*/
|
||||
function isSegmentFullyConstrained(
|
||||
pathToNode: PathToNode,
|
||||
ast: Program,
|
||||
code: string
|
||||
): boolean {
|
||||
try {
|
||||
const nodeMeta = getNodeFromPath<any>(ast, pathToNode)
|
||||
if (err(nodeMeta) || nodeMeta.node.type !== 'CallExpressionKw') {
|
||||
return false
|
||||
}
|
||||
|
||||
const constraintInfos = getConstraintInfoKw(nodeMeta.node, code, pathToNode)
|
||||
|
||||
// If there are no constraints, consider it not fully constrained
|
||||
if (constraintInfos.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if all constraints are constrained
|
||||
return constraintInfos.every((info) => info.isConstrained)
|
||||
} catch (error) {
|
||||
console.warn('Error checking segment constraints:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the appropriate color for a segment based on selection, constraints, and theme
|
||||
*/
|
||||
function getSegmentColor({
|
||||
theme,
|
||||
isSelected,
|
||||
callExpName = '',
|
||||
isFullyConstrained = false,
|
||||
}: {
|
||||
theme: Themes
|
||||
isSelected: boolean
|
||||
callExpName?: string
|
||||
isFullyConstrained: boolean
|
||||
}): number {
|
||||
if (isSelected) return 0x0000ff // Blue for selected
|
||||
if (callExpName === 'close') return 0x444444 // Gray for close segments
|
||||
if (!isFullyConstrained) return getPrimaryColorForThreeJs() // Primary color for unconstrained segments
|
||||
return getThemeColorForThreeJs(theme) // Default theme color for constrained segments
|
||||
}
|
||||
|
||||
class StraightSegment implements SegmentUtils {
|
||||
init: SegmentUtils['init'] = ({
|
||||
input,
|
||||
@ -158,13 +219,32 @@ class StraightSegment implements SegmentUtils {
|
||||
sceneInfra,
|
||||
prevSegment,
|
||||
selection,
|
||||
ast,
|
||||
code,
|
||||
}) => {
|
||||
if (input.type !== 'straight-segment')
|
||||
return new Error('Invalid segment type')
|
||||
const { from, to } = input
|
||||
const baseColor =
|
||||
callExpName === 'close' ? 0x444444 : getThemeColorForThreeJs(theme)
|
||||
const color = isSelected ? 0x0000ff : baseColor
|
||||
|
||||
// Check if segment is fully constrained (only if we have AST and code)
|
||||
const isFullyConstrained =
|
||||
callExpName === 'close'
|
||||
? true
|
||||
: ast && code
|
||||
? isSegmentFullyConstrained(pathToNode, ast, code)
|
||||
: false
|
||||
|
||||
const color = getSegmentColor({
|
||||
theme,
|
||||
isSelected: !!isSelected,
|
||||
callExpName,
|
||||
isFullyConstrained,
|
||||
})
|
||||
const baseColor = !isFullyConstrained
|
||||
? getPrimaryColorForThreeJs()
|
||||
: callExpName === 'close'
|
||||
? 0x444444
|
||||
: getThemeColorForThreeJs(theme)
|
||||
const meshType = isDraftSegment
|
||||
? STRAIGHT_SEGMENT_DASH
|
||||
: STRAIGHT_SEGMENT_BODY
|
||||
@ -250,12 +330,40 @@ class StraightSegment implements SegmentUtils {
|
||||
group,
|
||||
scale = 1,
|
||||
sceneInfra,
|
||||
ast,
|
||||
code,
|
||||
}) => {
|
||||
if (input.type !== 'straight-segment')
|
||||
return new Error('Invalid segment type')
|
||||
const { from, to } = input
|
||||
group.userData.from = from
|
||||
group.userData.to = to
|
||||
|
||||
// Check if segment is fully constrained and update color if needed
|
||||
if (ast && code) {
|
||||
const pathToNode = group.userData.pathToNode
|
||||
const isFullyConstrained =
|
||||
group.userData.callExpName === 'close'
|
||||
? true
|
||||
: isSegmentFullyConstrained(pathToNode, ast, code)
|
||||
const color = getSegmentColor({
|
||||
theme: sceneInfra._theme,
|
||||
isSelected: group.userData.isSelected,
|
||||
callExpName: group.userData.callExpName,
|
||||
isFullyConstrained,
|
||||
})
|
||||
|
||||
// Update the material color
|
||||
const straightSegmentBody = group.children.find(
|
||||
(child) => child.userData.type === STRAIGHT_SEGMENT_BODY
|
||||
) as Mesh
|
||||
if (
|
||||
straightSegmentBody &&
|
||||
straightSegmentBody.material instanceof MeshBasicMaterial
|
||||
) {
|
||||
straightSegmentBody.material.color.set(color)
|
||||
}
|
||||
}
|
||||
const shape = createLineShape(scale)
|
||||
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
||||
const labelGroup = group.getObjectByName(SEGMENT_LENGTH_LABEL) as Group
|
||||
@ -391,6 +499,8 @@ class TangentialArcToSegment implements SegmentUtils {
|
||||
theme,
|
||||
isSelected,
|
||||
sceneInfra,
|
||||
ast,
|
||||
code,
|
||||
}) => {
|
||||
if (input.type !== 'straight-segment')
|
||||
return new Error('Invalid segment type')
|
||||
@ -409,8 +519,19 @@ class TangentialArcToSegment implements SegmentUtils {
|
||||
isDashed: isDraftSegment,
|
||||
scale,
|
||||
})
|
||||
const baseColor = getThemeColorForThreeJs(theme)
|
||||
const color = isSelected ? 0x0000ff : baseColor
|
||||
|
||||
// Check if segment is fully constrained (only if we have AST and code)
|
||||
const isFullyConstrained =
|
||||
ast && code ? isSegmentFullyConstrained(pathToNode, ast, code) : false
|
||||
|
||||
const color = getSegmentColor({
|
||||
theme,
|
||||
isSelected: !!isSelected,
|
||||
isFullyConstrained,
|
||||
})
|
||||
const baseColor = !isFullyConstrained
|
||||
? getPrimaryColorForThreeJs()
|
||||
: getThemeColorForThreeJs(theme)
|
||||
const body = new MeshBasicMaterial({ color })
|
||||
const mesh = new Mesh(geometry, body)
|
||||
const arrowGroup = createArrowhead(scale, theme, color)
|
||||
@ -453,6 +574,8 @@ class TangentialArcToSegment implements SegmentUtils {
|
||||
group,
|
||||
scale = 1,
|
||||
sceneInfra,
|
||||
ast,
|
||||
code,
|
||||
}) => {
|
||||
if (input.type !== 'straight-segment')
|
||||
return new Error('Invalid segment type')
|
||||
@ -460,6 +583,32 @@ class TangentialArcToSegment implements SegmentUtils {
|
||||
group.userData.from = from
|
||||
group.userData.to = to
|
||||
group.userData.prevSegment = prevSegment
|
||||
|
||||
// Check if segment is fully constrained and update color if needed
|
||||
if (ast && code) {
|
||||
const pathToNode = group.userData.pathToNode
|
||||
const isFullyConstrained = isSegmentFullyConstrained(
|
||||
pathToNode,
|
||||
ast,
|
||||
code
|
||||
)
|
||||
const color = getSegmentColor({
|
||||
theme: sceneInfra._theme,
|
||||
isSelected: group.userData.isSelected,
|
||||
isFullyConstrained,
|
||||
})
|
||||
|
||||
// Update the material color
|
||||
const tangentialArcSegmentBody = group.children.find(
|
||||
(child) => child.userData.type === TANGENTIAL_ARC_TO_SEGMENT_BODY
|
||||
) as Mesh
|
||||
if (
|
||||
tangentialArcSegmentBody &&
|
||||
tangentialArcSegmentBody.material instanceof MeshBasicMaterial
|
||||
) {
|
||||
tangentialArcSegmentBody.material.color.set(color)
|
||||
}
|
||||
}
|
||||
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
||||
const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
||||
|
||||
@ -588,13 +737,26 @@ class CircleSegment implements SegmentUtils {
|
||||
theme,
|
||||
isSelected,
|
||||
sceneInfra,
|
||||
ast,
|
||||
code,
|
||||
}) => {
|
||||
if (input.type !== 'arc-segment') {
|
||||
return new Error('Invalid segment type')
|
||||
}
|
||||
const { from, center, radius } = input
|
||||
const baseColor = getThemeColorForThreeJs(theme)
|
||||
const color = isSelected ? 0x0000ff : baseColor
|
||||
|
||||
// Check if segment is fully constrained
|
||||
const isFullyConstrained =
|
||||
ast && code ? isSegmentFullyConstrained(pathToNode, ast, code) : false
|
||||
|
||||
const color = getSegmentColor({
|
||||
theme,
|
||||
isSelected: !!isSelected,
|
||||
isFullyConstrained,
|
||||
})
|
||||
const baseColor = !isFullyConstrained
|
||||
? getPrimaryColorForThreeJs()
|
||||
: getThemeColorForThreeJs(theme)
|
||||
|
||||
const group = new Group()
|
||||
const geometry = createArcGeometry({
|
||||
@ -678,6 +840,8 @@ class CircleSegment implements SegmentUtils {
|
||||
group,
|
||||
scale = 1,
|
||||
sceneInfra,
|
||||
ast,
|
||||
code,
|
||||
}) => {
|
||||
if (input.type !== 'arc-segment') {
|
||||
return new Error('Invalid segment type')
|
||||
@ -687,6 +851,32 @@ class CircleSegment implements SegmentUtils {
|
||||
group.userData.center = center
|
||||
group.userData.radius = radius
|
||||
group.userData.prevSegment = prevSegment
|
||||
|
||||
// Check if segment is fully constrained and update color if needed
|
||||
if (ast && code) {
|
||||
const pathToNode = group.userData.pathToNode
|
||||
const isFullyConstrained = isSegmentFullyConstrained(
|
||||
pathToNode,
|
||||
ast,
|
||||
code
|
||||
)
|
||||
const color = getSegmentColor({
|
||||
theme: sceneInfra._theme,
|
||||
isSelected: group.userData.isSelected,
|
||||
isFullyConstrained,
|
||||
})
|
||||
|
||||
// Update the material color
|
||||
const circleSegmentBody = group.children.find(
|
||||
(child) => child.userData.type === CIRCLE_SEGMENT_BODY
|
||||
) as Mesh
|
||||
if (
|
||||
circleSegmentBody &&
|
||||
circleSegmentBody.material instanceof MeshBasicMaterial
|
||||
) {
|
||||
circleSegmentBody.material.color.set(color)
|
||||
}
|
||||
}
|
||||
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
||||
const radiusLengthIndicator = group.getObjectByName(
|
||||
SEGMENT_LENGTH_LABEL
|
||||
@ -831,6 +1021,8 @@ class CircleThreePointSegment implements SegmentUtils {
|
||||
isSelected = false,
|
||||
sceneInfra,
|
||||
prevSegment,
|
||||
ast,
|
||||
code,
|
||||
}) => {
|
||||
if (input.type !== 'circle-three-point-segment') {
|
||||
return new Error('Invalid segment type')
|
||||
@ -845,8 +1037,19 @@ class CircleThreePointSegment implements SegmentUtils {
|
||||
p3[1]
|
||||
)
|
||||
const center: [number, number] = [center_x, center_y]
|
||||
const baseColor = getThemeColorForThreeJs(theme)
|
||||
const color = isSelected ? 0x0000ff : baseColor
|
||||
|
||||
// Check if segment is fully constrained
|
||||
const isFullyConstrained =
|
||||
ast && code ? isSegmentFullyConstrained(pathToNode, ast, code) : false
|
||||
|
||||
const color = getSegmentColor({
|
||||
theme,
|
||||
isSelected: !!isSelected,
|
||||
isFullyConstrained,
|
||||
})
|
||||
const baseColor = !isFullyConstrained
|
||||
? getPrimaryColorForThreeJs()
|
||||
: getThemeColorForThreeJs(theme)
|
||||
|
||||
const group = new Group()
|
||||
const geometry = createArcGeometry({
|
||||
@ -919,6 +1122,8 @@ class CircleThreePointSegment implements SegmentUtils {
|
||||
group,
|
||||
scale = 1,
|
||||
sceneInfra,
|
||||
ast,
|
||||
code,
|
||||
}) => {
|
||||
if (input.type !== 'circle-three-point-segment') {
|
||||
return new Error('Invalid segment type')
|
||||
@ -927,6 +1132,32 @@ class CircleThreePointSegment implements SegmentUtils {
|
||||
group.userData.p1 = p1
|
||||
group.userData.p2 = p2
|
||||
group.userData.p3 = p3
|
||||
|
||||
// Check if segment is fully constrained and update color if needed
|
||||
if (ast && code) {
|
||||
const pathToNode = group.userData.pathToNode
|
||||
const isFullyConstrained = isSegmentFullyConstrained(
|
||||
pathToNode,
|
||||
ast,
|
||||
code
|
||||
)
|
||||
const color = getSegmentColor({
|
||||
theme: sceneInfra._theme,
|
||||
isSelected: group.userData.isSelected,
|
||||
isFullyConstrained,
|
||||
})
|
||||
|
||||
// Update the material color
|
||||
const circleSegmentBody = group.children.find(
|
||||
(child) => child.userData.type === CIRCLE_THREE_POINT_SEGMENT_BODY
|
||||
) as Mesh
|
||||
if (
|
||||
circleSegmentBody &&
|
||||
circleSegmentBody.material instanceof MeshBasicMaterial
|
||||
) {
|
||||
circleSegmentBody.material.color.set(color)
|
||||
}
|
||||
}
|
||||
const { center_x, center_y, radius } = calculate_circle_from_3_points(
|
||||
p1[0],
|
||||
p1[1],
|
||||
@ -1048,13 +1279,26 @@ class ArcSegment implements SegmentUtils {
|
||||
theme,
|
||||
isSelected,
|
||||
sceneInfra,
|
||||
ast,
|
||||
code,
|
||||
}) => {
|
||||
if (input.type !== 'arc-segment') {
|
||||
return new Error('Invalid segment type')
|
||||
}
|
||||
const { from, to, center, radius, ccw } = input
|
||||
const baseColor = getThemeColorForThreeJs(theme)
|
||||
const color = isSelected ? 0x0000ff : baseColor
|
||||
|
||||
// Check if segment is fully constrained
|
||||
const isFullyConstrained =
|
||||
ast && code ? isSegmentFullyConstrained(pathToNode, ast, code) : false
|
||||
|
||||
const color = getSegmentColor({
|
||||
theme,
|
||||
isSelected: !!isSelected,
|
||||
isFullyConstrained,
|
||||
})
|
||||
const baseColor = !isFullyConstrained
|
||||
? getPrimaryColorForThreeJs()
|
||||
: getThemeColorForThreeJs(theme)
|
||||
|
||||
// Calculate start and end angles
|
||||
const startAngle = Math.atan2(from[1] - center[1], from[0] - center[0])
|
||||
@ -1195,6 +1439,8 @@ class ArcSegment implements SegmentUtils {
|
||||
group,
|
||||
scale = 1,
|
||||
sceneInfra,
|
||||
ast,
|
||||
code,
|
||||
}) => {
|
||||
if (input.type !== 'arc-segment') {
|
||||
return new Error('Invalid segment type')
|
||||
@ -1207,6 +1453,32 @@ class ArcSegment implements SegmentUtils {
|
||||
group.userData.ccw = ccw
|
||||
group.userData.prevSegment = prevSegment
|
||||
|
||||
// Check if segment is fully constrained and update color if needed
|
||||
if (ast && code) {
|
||||
const pathToNode = group.userData.pathToNode
|
||||
const isFullyConstrained = isSegmentFullyConstrained(
|
||||
pathToNode,
|
||||
ast,
|
||||
code
|
||||
)
|
||||
const color = getSegmentColor({
|
||||
theme: sceneInfra._theme,
|
||||
isSelected: group.userData.isSelected,
|
||||
isFullyConstrained,
|
||||
})
|
||||
|
||||
// Update the material color
|
||||
const arcSegmentBody = group.children.find(
|
||||
(child) => child.userData.type === ARC_SEGMENT_BODY
|
||||
) as Mesh
|
||||
if (
|
||||
arcSegmentBody &&
|
||||
arcSegmentBody.material instanceof MeshBasicMaterial
|
||||
) {
|
||||
arcSegmentBody.material.color.set(color)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate start and end angles
|
||||
const startAngle = Math.atan2(from[1] - center[1], from[0] - center[0])
|
||||
const endAngle = Math.atan2(to[1] - center[1], to[0] - center[0])
|
||||
@ -1403,6 +1675,8 @@ class ThreePointArcSegment implements SegmentUtils {
|
||||
isSelected = false,
|
||||
sceneInfra,
|
||||
prevSegment,
|
||||
ast,
|
||||
code,
|
||||
}) => {
|
||||
if (input.type !== 'circle-three-point-segment') {
|
||||
return new Error('Invalid segment type')
|
||||
@ -1417,8 +1691,19 @@ class ThreePointArcSegment implements SegmentUtils {
|
||||
p3[1]
|
||||
)
|
||||
const center: [number, number] = [center_x, center_y]
|
||||
const baseColor = getThemeColorForThreeJs(theme)
|
||||
const color = isSelected ? 0x0000ff : baseColor
|
||||
|
||||
// Check if segment is fully constrained
|
||||
const isFullyConstrained =
|
||||
ast && code ? isSegmentFullyConstrained(pathToNode, ast, code) : false
|
||||
|
||||
const color = getSegmentColor({
|
||||
theme,
|
||||
isSelected: !!isSelected,
|
||||
isFullyConstrained,
|
||||
})
|
||||
const baseColor = !isFullyConstrained
|
||||
? getPrimaryColorForThreeJs()
|
||||
: getThemeColorForThreeJs(theme)
|
||||
|
||||
// Calculate start and end angles
|
||||
const startAngle = Math.atan2(p1[1] - center[1], p1[0] - center[0])
|
||||
@ -1500,6 +1785,8 @@ class ThreePointArcSegment implements SegmentUtils {
|
||||
group,
|
||||
scale = 1,
|
||||
sceneInfra,
|
||||
ast,
|
||||
code,
|
||||
}) => {
|
||||
if (input.type !== 'circle-three-point-segment') {
|
||||
return new Error('Invalid segment type')
|
||||
@ -1523,6 +1810,32 @@ class ThreePointArcSegment implements SegmentUtils {
|
||||
group.userData.radius = radius
|
||||
group.userData.prevSegment = prevSegment
|
||||
|
||||
// Check if segment is fully constrained and update color if needed
|
||||
if (ast && code) {
|
||||
const pathToNode = group.userData.pathToNode
|
||||
const isFullyConstrained = isSegmentFullyConstrained(
|
||||
pathToNode,
|
||||
ast,
|
||||
code
|
||||
)
|
||||
const color = getSegmentColor({
|
||||
theme: sceneInfra._theme,
|
||||
isSelected: group.userData.isSelected,
|
||||
isFullyConstrained,
|
||||
})
|
||||
|
||||
// Update the material color
|
||||
const arcSegmentBody = group.children.find(
|
||||
(child) => child.userData.type === THREE_POINT_ARC_SEGMENT_BODY
|
||||
) as Mesh
|
||||
if (
|
||||
arcSegmentBody &&
|
||||
arcSegmentBody.material instanceof MeshBasicMaterial
|
||||
) {
|
||||
arcSegmentBody.material.color.set(color)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate start and end angles
|
||||
const startAngle = Math.atan2(p1[1] - center[1], p1[0] - center[0])
|
||||
const endAngle = Math.atan2(p3[1] - center[1], p3[0] - center[0])
|
||||
@ -1619,6 +1932,8 @@ export function createProfileStartHandle({
|
||||
theme,
|
||||
isSelected,
|
||||
size = 12,
|
||||
ast,
|
||||
code,
|
||||
...rest
|
||||
}: {
|
||||
from: Coords2d
|
||||
@ -1626,15 +1941,30 @@ export function createProfileStartHandle({
|
||||
theme: Themes
|
||||
isSelected?: boolean
|
||||
size?: number
|
||||
ast?: Program
|
||||
code?: string
|
||||
} & (
|
||||
| { isDraft: true }
|
||||
| { isDraft: false; id: string; pathToNode: PathToNode }
|
||||
)) {
|
||||
const group = new Group()
|
||||
|
||||
// Check if profile start is fully constrained (only if we have AST, code, and it's not a draft)
|
||||
const isFullyConstrained =
|
||||
!isDraft && ast && code && 'pathToNode' in rest
|
||||
? isSegmentFullyConstrained(rest.pathToNode, ast, code)
|
||||
: false
|
||||
|
||||
const geometry = new BoxGeometry(size, size, size) // in pixels scaled later
|
||||
const baseColor = getThemeColorForThreeJs(theme)
|
||||
const color = isSelected ? 0x0000ff : baseColor
|
||||
const color = getSegmentColor({
|
||||
theme,
|
||||
isSelected: !!isSelected,
|
||||
callExpName: 'profileStart',
|
||||
isFullyConstrained,
|
||||
})
|
||||
const baseColor = !isFullyConstrained
|
||||
? getPrimaryColorForThreeJs()
|
||||
: getThemeColorForThreeJs(theme)
|
||||
const body = new MeshBasicMaterial({ color })
|
||||
const mesh = new Mesh(geometry, body)
|
||||
|
||||
@ -1645,6 +1975,7 @@ export function createProfileStartHandle({
|
||||
from,
|
||||
isSelected,
|
||||
baseColor,
|
||||
isFullyConstrained,
|
||||
...rest,
|
||||
}
|
||||
group.name = isDraft ? DRAFT_POINT : PROFILE_START
|
||||
|
@ -1,4 +1,5 @@
|
||||
import type { AppTheme } from '@rust/kcl-lib/bindings/AppTheme'
|
||||
import { converter } from 'culori'
|
||||
|
||||
/** A media query matcher for dark mode */
|
||||
export const darkModeMatcher =
|
||||
@ -58,6 +59,84 @@ export function getOppositeTheme(theme: Themes) {
|
||||
return resolvedTheme === Themes.Dark ? Themes.Light : Themes.Dark
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts OKLCH values to RGB using Culori library
|
||||
* @param l - Lightness (0-1)
|
||||
* @param c - Chroma (0-1)
|
||||
* @param h - Hue (0-360 degrees)
|
||||
* @returns RGB values as [r, g, b] where each component is 0-255
|
||||
*/
|
||||
function oklchToRgb(l: number, c: number, h: number): [number, number, number] {
|
||||
// Create a converter from OKLCH to RGB using Culori
|
||||
const toRgb = converter('rgb')
|
||||
|
||||
// Convert OKLCH to RGB using Culori
|
||||
const rgb = toRgb({ mode: 'oklch', l, c, h })
|
||||
|
||||
if (!rgb) {
|
||||
// Fallback if conversion fails
|
||||
return [255, 255, 255]
|
||||
}
|
||||
|
||||
// Clamp values. When OKLCH values represent colors outside the sRGB gamut, the RGB values can be negative or greater than 1.
|
||||
const clampedR = Math.max(0, Math.min(1, rgb.r))
|
||||
const clampedG = Math.max(0, Math.min(1, rgb.g))
|
||||
const clampedB = Math.max(0, Math.min(1, rgb.b))
|
||||
|
||||
// Convert from 0-1 range to 0-255 range
|
||||
return [
|
||||
Math.round(clampedR * 255),
|
||||
Math.round(clampedG * 255),
|
||||
Math.round(clampedB * 255),
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the primary color from CSS custom properties and converts it to Three.js hex format
|
||||
* @returns Primary color as a hex number for Three.js, or fallback purple if unable to get CSS value
|
||||
*/
|
||||
export function getPrimaryColorForThreeJs(): number {
|
||||
if (typeof globalThis.window === 'undefined' || !globalThis.document) {
|
||||
// Fallback for SSR or when DOM is not available
|
||||
return 0x7c3aed // Default purple
|
||||
}
|
||||
|
||||
try {
|
||||
const computedStyle = getComputedStyle(document.documentElement)
|
||||
|
||||
// Get the individual primary color components
|
||||
const hue = parseFloat(
|
||||
computedStyle.getPropertyValue('--primary-hue').trim()
|
||||
)
|
||||
const chroma = parseFloat(
|
||||
computedStyle.getPropertyValue('--primary-chroma').trim()
|
||||
)
|
||||
const lightness =
|
||||
parseFloat(
|
||||
computedStyle
|
||||
.getPropertyValue('--primary-lightness')
|
||||
.replace('%', '')
|
||||
.trim()
|
||||
) / 100
|
||||
|
||||
if (Number.isNaN(hue) || Number.isNaN(chroma) || Number.isNaN(lightness)) {
|
||||
console.warn(
|
||||
'Unable to parse primary color components from CSS, using fallback'
|
||||
)
|
||||
return 0x7c3aed // Default purple
|
||||
}
|
||||
|
||||
// Convert OKLCH to RGB
|
||||
const [r, g, b] = oklchToRgb(lightness, chroma, hue)
|
||||
|
||||
// Convert RGB to hex
|
||||
return (r << 16) | (g << 8) | b
|
||||
} catch (error) {
|
||||
console.warn('Error getting primary color from CSS:', error)
|
||||
return 0x7c3aed // Default purple fallback
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The engine takes RGBA values from 0-1
|
||||
* So we convert from the conventional 0-255 found in Figma
|
||||
|