constraint colors
This commit is contained in:
		| @ -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,28 @@ 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 = | ||||
|       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 +326,41 @@ 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 = 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 +496,8 @@ class TangentialArcToSegment implements SegmentUtils { | ||||
|     theme, | ||||
|     isSelected, | ||||
|     sceneInfra, | ||||
|     ast, | ||||
|     code, | ||||
|   }) => { | ||||
|     if (input.type !== 'straight-segment') | ||||
|       return new Error('Invalid segment type') | ||||
| @ -409,8 +516,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 +571,8 @@ class TangentialArcToSegment implements SegmentUtils { | ||||
|     group, | ||||
|     scale = 1, | ||||
|     sceneInfra, | ||||
|     ast, | ||||
|     code, | ||||
|   }) => { | ||||
|     if (input.type !== 'straight-segment') | ||||
|       return new Error('Invalid segment type') | ||||
| @ -460,6 +580,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 +734,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 +837,8 @@ class CircleSegment implements SegmentUtils { | ||||
|     group, | ||||
|     scale = 1, | ||||
|     sceneInfra, | ||||
|     ast, | ||||
|     code, | ||||
|   }) => { | ||||
|     if (input.type !== 'arc-segment') { | ||||
|       return new Error('Invalid segment type') | ||||
| @ -687,6 +848,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 +1018,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 +1034,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 +1119,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 +1129,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 +1276,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 +1436,8 @@ class ArcSegment implements SegmentUtils { | ||||
|     group, | ||||
|     scale = 1, | ||||
|     sceneInfra, | ||||
|     ast, | ||||
|     code, | ||||
|   }) => { | ||||
|     if (input.type !== 'arc-segment') { | ||||
|       return new Error('Invalid segment type') | ||||
| @ -1207,6 +1450,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 +1672,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 +1688,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 +1782,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 +1807,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 +1929,8 @@ export function createProfileStartHandle({ | ||||
|   theme, | ||||
|   isSelected, | ||||
|   size = 12, | ||||
|   ast, | ||||
|   code, | ||||
|   ...rest | ||||
| }: { | ||||
|   from: Coords2d | ||||
| @ -1626,15 +1938,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 +1972,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 { convert, OKLCH, sRGB } from '@texel/color' | ||||
|  | ||||
| /** A media query matcher for dark mode */ | ||||
| export const darkModeMatcher = | ||||
| @ -58,6 +59,76 @@ export function getOppositeTheme(theme: Themes) { | ||||
|   return resolvedTheme === Themes.Dark ? Themes.Light : Themes.Dark | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Converts OKLCH values to RGB using @texel/color 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] { | ||||
|   // Convert OKLCH to sRGB using @texel/color | ||||
|   const [r, g, b] = convert([l, c, h], OKLCH, sRGB) | ||||
|  | ||||
|   // 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, r)) | ||||
|   const clampedG = Math.max(0, Math.min(1, g)) | ||||
|   const clampedB = Math.max(0, Math.min(1, 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 | ||||
|  | ||||
		Reference in New Issue
	
	Block a user