Compare commits
8 Commits
dev
...
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",
|
"bson": "^6.10.3",
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
|
"culori": "^4.0.2",
|
||||||
"decamelize": "^6.0.0",
|
"decamelize": "^6.0.0",
|
||||||
"diff": "^7.0.0",
|
"diff": "^7.0.0",
|
||||||
"electron-updater": "^6.6.2",
|
"electron-updater": "^6.6.2",
|
||||||
@ -92,6 +93,7 @@
|
|||||||
"@playwright/test": "^1.52.0",
|
"@playwright/test": "^1.52.0",
|
||||||
"@testing-library/jest-dom": "^5.17.0",
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
"@testing-library/react": "^15.0.7",
|
"@testing-library/react": "^15.0.7",
|
||||||
|
"@types/culori": "^4.0.0",
|
||||||
"@types/diff": "^7.0.2",
|
"@types/diff": "^7.0.2",
|
||||||
"@types/electron": "^1.6.10",
|
"@types/electron": "^1.6.10",
|
||||||
"@types/hammerjs": "^2.0.46",
|
"@types/hammerjs": "^2.0.46",
|
||||||
@ -7392,6 +7394,13 @@
|
|||||||
"integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==",
|
"integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/debug": {
|
||||||
"version": "4.1.12",
|
"version": "4.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
||||||
@ -11823,6 +11832,15 @@
|
|||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/damerau-levenshtein": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||||
|
@ -45,6 +45,7 @@
|
|||||||
"bson": "^6.10.3",
|
"bson": "^6.10.3",
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
|
"culori": "^4.0.2",
|
||||||
"decamelize": "^6.0.0",
|
"decamelize": "^6.0.0",
|
||||||
"diff": "^7.0.0",
|
"diff": "^7.0.0",
|
||||||
"electron-updater": "^6.6.2",
|
"electron-updater": "^6.6.2",
|
||||||
@ -170,6 +171,7 @@
|
|||||||
"@playwright/test": "^1.52.0",
|
"@playwright/test": "^1.52.0",
|
||||||
"@testing-library/jest-dom": "^5.17.0",
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
"@testing-library/react": "^15.0.7",
|
"@testing-library/react": "^15.0.7",
|
||||||
|
"@types/culori": "^4.0.0",
|
||||||
"@types/diff": "^7.0.2",
|
"@types/diff": "^7.0.2",
|
||||||
"@types/electron": "^1.6.10",
|
"@types/electron": "^1.6.10",
|
||||||
"@types/hammerjs": "^2.0.46",
|
"@types/hammerjs": "^2.0.46",
|
||||||
|
@ -324,6 +324,8 @@ export class SceneEntities {
|
|||||||
group: segment,
|
group: segment,
|
||||||
scale: factor,
|
scale: factor,
|
||||||
sceneInfra: this.sceneInfra,
|
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)
|
callBack && !err(callBack) && callbacks.push(callBack)
|
||||||
if (segment.name === PROFILE_START) {
|
if (segment.name === PROFILE_START) {
|
||||||
@ -729,6 +731,8 @@ export class SceneEntities {
|
|||||||
scale,
|
scale,
|
||||||
theme: this.sceneInfra._theme,
|
theme: this.sceneInfra._theme,
|
||||||
isDraft: false,
|
isDraft: false,
|
||||||
|
ast: maybeModdedAst,
|
||||||
|
code: this.codeManager.code,
|
||||||
})
|
})
|
||||||
_profileStart.layers.set(SKETCH_LAYER)
|
_profileStart.layers.set(SKETCH_LAYER)
|
||||||
_profileStart.traverse((child) => {
|
_profileStart.traverse((child) => {
|
||||||
@ -866,6 +870,8 @@ export class SceneEntities {
|
|||||||
isSelected,
|
isSelected,
|
||||||
sceneInfra: this.sceneInfra,
|
sceneInfra: this.sceneInfra,
|
||||||
selection,
|
selection,
|
||||||
|
ast: maybeModdedAst,
|
||||||
|
code: this.codeManager.code,
|
||||||
})
|
})
|
||||||
if (err(result)) return
|
if (err(result)) return
|
||||||
const { group: _group, updateOverlaysCallback } = result
|
const { group: _group, updateOverlaysCallback } = result
|
||||||
@ -3252,6 +3258,8 @@ export class SceneEntities {
|
|||||||
scale: factor,
|
scale: factor,
|
||||||
prevSegment: sgPaths[index - 1],
|
prevSegment: sgPaths[index - 1],
|
||||||
sceneInfra: this.sceneInfra,
|
sceneInfra: this.sceneInfra,
|
||||||
|
ast: modifiedAst,
|
||||||
|
code: this.codeManager.code,
|
||||||
})
|
})
|
||||||
if (callBack && !err(callBack)) return callBack
|
if (callBack && !err(callBack)) return callBack
|
||||||
|
|
||||||
|
@ -76,12 +76,17 @@ import {
|
|||||||
} from '@src/clientSideScene/sceneUtils'
|
} from '@src/clientSideScene/sceneUtils'
|
||||||
import { angleLengthInfo } from '@src/components/Toolbar/angleLengthInfo'
|
import { angleLengthInfo } from '@src/components/Toolbar/angleLengthInfo'
|
||||||
import type { Coords2d } from '@src/lang/std/sketch'
|
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 { 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 { getTangentialArcToInfo } from '@src/lang/wasm'
|
||||||
|
import { getNodeFromPath } from '@src/lang/queryAst'
|
||||||
import type { Selections } from '@src/lib/selections'
|
import type { Selections } from '@src/lib/selections'
|
||||||
import type { Themes } from '@src/lib/theme'
|
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 { err } from '@src/lib/trap'
|
||||||
import { isClockwise, normaliseAngle, roundOff } from '@src/lib/utils'
|
import { isClockwise, normaliseAngle, roundOff } from '@src/lib/utils'
|
||||||
import { getTangentPointFromPreviousArc } from '@src/lib/utils2d'
|
import { getTangentPointFromPreviousArc } from '@src/lib/utils2d'
|
||||||
@ -95,6 +100,7 @@ import toast from 'react-hot-toast'
|
|||||||
import { ARG_INTERIOR_ABSOLUTE } from '@src/lang/constants'
|
import { ARG_INTERIOR_ABSOLUTE } from '@src/lang/constants'
|
||||||
|
|
||||||
const ANGLE_INDICATOR_RADIUS = 30 // in px
|
const ANGLE_INDICATOR_RADIUS = 30 // in px
|
||||||
|
|
||||||
interface CreateSegmentArgs {
|
interface CreateSegmentArgs {
|
||||||
input: SegmentInputs
|
input: SegmentInputs
|
||||||
prevSegment: Sketch['paths'][number]
|
prevSegment: Sketch['paths'][number]
|
||||||
@ -108,6 +114,9 @@ interface CreateSegmentArgs {
|
|||||||
isSelected?: boolean
|
isSelected?: boolean
|
||||||
sceneInfra: SceneInfra
|
sceneInfra: SceneInfra
|
||||||
selection?: Selections
|
selection?: Selections
|
||||||
|
// Add optional AST and code for constraint checking
|
||||||
|
ast?: Program
|
||||||
|
code?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateSegmentArgs {
|
interface UpdateSegmentArgs {
|
||||||
@ -116,6 +125,9 @@ interface UpdateSegmentArgs {
|
|||||||
group: Group
|
group: Group
|
||||||
sceneInfra: SceneInfra
|
sceneInfra: SceneInfra
|
||||||
scale?: number
|
scale?: number
|
||||||
|
// Add optional AST and code for constraint checking
|
||||||
|
ast?: Program
|
||||||
|
code?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CreateSegmentResult {
|
interface CreateSegmentResult {
|
||||||
@ -144,6 +156,55 @@ export interface SegmentUtils {
|
|||||||
) => CreateSegmentResult['updateOverlaysCallback'] | Error
|
) => 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 {
|
class StraightSegment implements SegmentUtils {
|
||||||
init: SegmentUtils['init'] = ({
|
init: SegmentUtils['init'] = ({
|
||||||
input,
|
input,
|
||||||
@ -158,13 +219,32 @@ class StraightSegment implements SegmentUtils {
|
|||||||
sceneInfra,
|
sceneInfra,
|
||||||
prevSegment,
|
prevSegment,
|
||||||
selection,
|
selection,
|
||||||
|
ast,
|
||||||
|
code,
|
||||||
}) => {
|
}) => {
|
||||||
if (input.type !== 'straight-segment')
|
if (input.type !== 'straight-segment')
|
||||||
return new Error('Invalid segment type')
|
return new Error('Invalid segment type')
|
||||||
const { from, to } = input
|
const { from, to } = input
|
||||||
const baseColor =
|
|
||||||
callExpName === 'close' ? 0x444444 : getThemeColorForThreeJs(theme)
|
// Check if segment is fully constrained (only if we have AST and code)
|
||||||
const color = isSelected ? 0x0000ff : baseColor
|
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
|
const meshType = isDraftSegment
|
||||||
? STRAIGHT_SEGMENT_DASH
|
? STRAIGHT_SEGMENT_DASH
|
||||||
: STRAIGHT_SEGMENT_BODY
|
: STRAIGHT_SEGMENT_BODY
|
||||||
@ -250,12 +330,40 @@ class StraightSegment implements SegmentUtils {
|
|||||||
group,
|
group,
|
||||||
scale = 1,
|
scale = 1,
|
||||||
sceneInfra,
|
sceneInfra,
|
||||||
|
ast,
|
||||||
|
code,
|
||||||
}) => {
|
}) => {
|
||||||
if (input.type !== 'straight-segment')
|
if (input.type !== 'straight-segment')
|
||||||
return new Error('Invalid segment type')
|
return new Error('Invalid segment type')
|
||||||
const { from, to } = input
|
const { from, to } = input
|
||||||
group.userData.from = from
|
group.userData.from = from
|
||||||
group.userData.to = to
|
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 shape = createLineShape(scale)
|
||||||
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
||||||
const labelGroup = group.getObjectByName(SEGMENT_LENGTH_LABEL) as Group
|
const labelGroup = group.getObjectByName(SEGMENT_LENGTH_LABEL) as Group
|
||||||
@ -391,6 +499,8 @@ class TangentialArcToSegment implements SegmentUtils {
|
|||||||
theme,
|
theme,
|
||||||
isSelected,
|
isSelected,
|
||||||
sceneInfra,
|
sceneInfra,
|
||||||
|
ast,
|
||||||
|
code,
|
||||||
}) => {
|
}) => {
|
||||||
if (input.type !== 'straight-segment')
|
if (input.type !== 'straight-segment')
|
||||||
return new Error('Invalid segment type')
|
return new Error('Invalid segment type')
|
||||||
@ -409,8 +519,19 @@ class TangentialArcToSegment implements SegmentUtils {
|
|||||||
isDashed: isDraftSegment,
|
isDashed: isDraftSegment,
|
||||||
scale,
|
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 body = new MeshBasicMaterial({ color })
|
||||||
const mesh = new Mesh(geometry, body)
|
const mesh = new Mesh(geometry, body)
|
||||||
const arrowGroup = createArrowhead(scale, theme, color)
|
const arrowGroup = createArrowhead(scale, theme, color)
|
||||||
@ -453,6 +574,8 @@ class TangentialArcToSegment implements SegmentUtils {
|
|||||||
group,
|
group,
|
||||||
scale = 1,
|
scale = 1,
|
||||||
sceneInfra,
|
sceneInfra,
|
||||||
|
ast,
|
||||||
|
code,
|
||||||
}) => {
|
}) => {
|
||||||
if (input.type !== 'straight-segment')
|
if (input.type !== 'straight-segment')
|
||||||
return new Error('Invalid segment type')
|
return new Error('Invalid segment type')
|
||||||
@ -460,6 +583,32 @@ class TangentialArcToSegment implements SegmentUtils {
|
|||||||
group.userData.from = from
|
group.userData.from = from
|
||||||
group.userData.to = to
|
group.userData.to = to
|
||||||
group.userData.prevSegment = prevSegment
|
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 arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
||||||
const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
||||||
|
|
||||||
@ -588,13 +737,26 @@ class CircleSegment implements SegmentUtils {
|
|||||||
theme,
|
theme,
|
||||||
isSelected,
|
isSelected,
|
||||||
sceneInfra,
|
sceneInfra,
|
||||||
|
ast,
|
||||||
|
code,
|
||||||
}) => {
|
}) => {
|
||||||
if (input.type !== 'arc-segment') {
|
if (input.type !== 'arc-segment') {
|
||||||
return new Error('Invalid segment type')
|
return new Error('Invalid segment type')
|
||||||
}
|
}
|
||||||
const { from, center, radius } = input
|
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 group = new Group()
|
||||||
const geometry = createArcGeometry({
|
const geometry = createArcGeometry({
|
||||||
@ -678,6 +840,8 @@ class CircleSegment implements SegmentUtils {
|
|||||||
group,
|
group,
|
||||||
scale = 1,
|
scale = 1,
|
||||||
sceneInfra,
|
sceneInfra,
|
||||||
|
ast,
|
||||||
|
code,
|
||||||
}) => {
|
}) => {
|
||||||
if (input.type !== 'arc-segment') {
|
if (input.type !== 'arc-segment') {
|
||||||
return new Error('Invalid segment type')
|
return new Error('Invalid segment type')
|
||||||
@ -687,6 +851,32 @@ class CircleSegment implements SegmentUtils {
|
|||||||
group.userData.center = center
|
group.userData.center = center
|
||||||
group.userData.radius = radius
|
group.userData.radius = radius
|
||||||
group.userData.prevSegment = prevSegment
|
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 arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
||||||
const radiusLengthIndicator = group.getObjectByName(
|
const radiusLengthIndicator = group.getObjectByName(
|
||||||
SEGMENT_LENGTH_LABEL
|
SEGMENT_LENGTH_LABEL
|
||||||
@ -831,6 +1021,8 @@ class CircleThreePointSegment implements SegmentUtils {
|
|||||||
isSelected = false,
|
isSelected = false,
|
||||||
sceneInfra,
|
sceneInfra,
|
||||||
prevSegment,
|
prevSegment,
|
||||||
|
ast,
|
||||||
|
code,
|
||||||
}) => {
|
}) => {
|
||||||
if (input.type !== 'circle-three-point-segment') {
|
if (input.type !== 'circle-three-point-segment') {
|
||||||
return new Error('Invalid segment type')
|
return new Error('Invalid segment type')
|
||||||
@ -845,8 +1037,19 @@ class CircleThreePointSegment implements SegmentUtils {
|
|||||||
p3[1]
|
p3[1]
|
||||||
)
|
)
|
||||||
const center: [number, number] = [center_x, center_y]
|
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 group = new Group()
|
||||||
const geometry = createArcGeometry({
|
const geometry = createArcGeometry({
|
||||||
@ -919,6 +1122,8 @@ class CircleThreePointSegment implements SegmentUtils {
|
|||||||
group,
|
group,
|
||||||
scale = 1,
|
scale = 1,
|
||||||
sceneInfra,
|
sceneInfra,
|
||||||
|
ast,
|
||||||
|
code,
|
||||||
}) => {
|
}) => {
|
||||||
if (input.type !== 'circle-three-point-segment') {
|
if (input.type !== 'circle-three-point-segment') {
|
||||||
return new Error('Invalid segment type')
|
return new Error('Invalid segment type')
|
||||||
@ -927,6 +1132,32 @@ class CircleThreePointSegment implements SegmentUtils {
|
|||||||
group.userData.p1 = p1
|
group.userData.p1 = p1
|
||||||
group.userData.p2 = p2
|
group.userData.p2 = p2
|
||||||
group.userData.p3 = p3
|
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(
|
const { center_x, center_y, radius } = calculate_circle_from_3_points(
|
||||||
p1[0],
|
p1[0],
|
||||||
p1[1],
|
p1[1],
|
||||||
@ -1048,13 +1279,26 @@ class ArcSegment implements SegmentUtils {
|
|||||||
theme,
|
theme,
|
||||||
isSelected,
|
isSelected,
|
||||||
sceneInfra,
|
sceneInfra,
|
||||||
|
ast,
|
||||||
|
code,
|
||||||
}) => {
|
}) => {
|
||||||
if (input.type !== 'arc-segment') {
|
if (input.type !== 'arc-segment') {
|
||||||
return new Error('Invalid segment type')
|
return new Error('Invalid segment type')
|
||||||
}
|
}
|
||||||
const { from, to, center, radius, ccw } = input
|
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
|
// Calculate start and end angles
|
||||||
const startAngle = Math.atan2(from[1] - center[1], from[0] - center[0])
|
const startAngle = Math.atan2(from[1] - center[1], from[0] - center[0])
|
||||||
@ -1195,6 +1439,8 @@ class ArcSegment implements SegmentUtils {
|
|||||||
group,
|
group,
|
||||||
scale = 1,
|
scale = 1,
|
||||||
sceneInfra,
|
sceneInfra,
|
||||||
|
ast,
|
||||||
|
code,
|
||||||
}) => {
|
}) => {
|
||||||
if (input.type !== 'arc-segment') {
|
if (input.type !== 'arc-segment') {
|
||||||
return new Error('Invalid segment type')
|
return new Error('Invalid segment type')
|
||||||
@ -1207,6 +1453,32 @@ class ArcSegment implements SegmentUtils {
|
|||||||
group.userData.ccw = ccw
|
group.userData.ccw = ccw
|
||||||
group.userData.prevSegment = prevSegment
|
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
|
// Calculate start and end angles
|
||||||
const startAngle = Math.atan2(from[1] - center[1], from[0] - center[0])
|
const startAngle = Math.atan2(from[1] - center[1], from[0] - center[0])
|
||||||
const endAngle = Math.atan2(to[1] - center[1], to[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,
|
isSelected = false,
|
||||||
sceneInfra,
|
sceneInfra,
|
||||||
prevSegment,
|
prevSegment,
|
||||||
|
ast,
|
||||||
|
code,
|
||||||
}) => {
|
}) => {
|
||||||
if (input.type !== 'circle-three-point-segment') {
|
if (input.type !== 'circle-three-point-segment') {
|
||||||
return new Error('Invalid segment type')
|
return new Error('Invalid segment type')
|
||||||
@ -1417,8 +1691,19 @@ class ThreePointArcSegment implements SegmentUtils {
|
|||||||
p3[1]
|
p3[1]
|
||||||
)
|
)
|
||||||
const center: [number, number] = [center_x, center_y]
|
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
|
// Calculate start and end angles
|
||||||
const startAngle = Math.atan2(p1[1] - center[1], p1[0] - center[0])
|
const startAngle = Math.atan2(p1[1] - center[1], p1[0] - center[0])
|
||||||
@ -1500,6 +1785,8 @@ class ThreePointArcSegment implements SegmentUtils {
|
|||||||
group,
|
group,
|
||||||
scale = 1,
|
scale = 1,
|
||||||
sceneInfra,
|
sceneInfra,
|
||||||
|
ast,
|
||||||
|
code,
|
||||||
}) => {
|
}) => {
|
||||||
if (input.type !== 'circle-three-point-segment') {
|
if (input.type !== 'circle-three-point-segment') {
|
||||||
return new Error('Invalid segment type')
|
return new Error('Invalid segment type')
|
||||||
@ -1523,6 +1810,32 @@ class ThreePointArcSegment implements SegmentUtils {
|
|||||||
group.userData.radius = radius
|
group.userData.radius = radius
|
||||||
group.userData.prevSegment = prevSegment
|
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
|
// Calculate start and end angles
|
||||||
const startAngle = Math.atan2(p1[1] - center[1], p1[0] - center[0])
|
const startAngle = Math.atan2(p1[1] - center[1], p1[0] - center[0])
|
||||||
const endAngle = Math.atan2(p3[1] - center[1], p3[0] - center[0])
|
const endAngle = Math.atan2(p3[1] - center[1], p3[0] - center[0])
|
||||||
@ -1619,6 +1932,8 @@ export function createProfileStartHandle({
|
|||||||
theme,
|
theme,
|
||||||
isSelected,
|
isSelected,
|
||||||
size = 12,
|
size = 12,
|
||||||
|
ast,
|
||||||
|
code,
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
from: Coords2d
|
from: Coords2d
|
||||||
@ -1626,15 +1941,30 @@ export function createProfileStartHandle({
|
|||||||
theme: Themes
|
theme: Themes
|
||||||
isSelected?: boolean
|
isSelected?: boolean
|
||||||
size?: number
|
size?: number
|
||||||
|
ast?: Program
|
||||||
|
code?: string
|
||||||
} & (
|
} & (
|
||||||
| { isDraft: true }
|
| { isDraft: true }
|
||||||
| { isDraft: false; id: string; pathToNode: PathToNode }
|
| { isDraft: false; id: string; pathToNode: PathToNode }
|
||||||
)) {
|
)) {
|
||||||
const group = new Group()
|
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 geometry = new BoxGeometry(size, size, size) // in pixels scaled later
|
||||||
const baseColor = getThemeColorForThreeJs(theme)
|
const color = getSegmentColor({
|
||||||
const color = isSelected ? 0x0000ff : baseColor
|
theme,
|
||||||
|
isSelected: !!isSelected,
|
||||||
|
callExpName: 'profileStart',
|
||||||
|
isFullyConstrained,
|
||||||
|
})
|
||||||
|
const baseColor = !isFullyConstrained
|
||||||
|
? getPrimaryColorForThreeJs()
|
||||||
|
: getThemeColorForThreeJs(theme)
|
||||||
const body = new MeshBasicMaterial({ color })
|
const body = new MeshBasicMaterial({ color })
|
||||||
const mesh = new Mesh(geometry, body)
|
const mesh = new Mesh(geometry, body)
|
||||||
|
|
||||||
@ -1645,6 +1975,7 @@ export function createProfileStartHandle({
|
|||||||
from,
|
from,
|
||||||
isSelected,
|
isSelected,
|
||||||
baseColor,
|
baseColor,
|
||||||
|
isFullyConstrained,
|
||||||
...rest,
|
...rest,
|
||||||
}
|
}
|
||||||
group.name = isDraft ? DRAFT_POINT : PROFILE_START
|
group.name = isDraft ? DRAFT_POINT : PROFILE_START
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import type { AppTheme } from '@rust/kcl-lib/bindings/AppTheme'
|
import type { AppTheme } from '@rust/kcl-lib/bindings/AppTheme'
|
||||||
|
import { converter } from 'culori'
|
||||||
|
|
||||||
/** A media query matcher for dark mode */
|
/** A media query matcher for dark mode */
|
||||||
export const darkModeMatcher =
|
export const darkModeMatcher =
|
||||||
@ -58,6 +59,84 @@ export function getOppositeTheme(theme: Themes) {
|
|||||||
return resolvedTheme === Themes.Dark ? Themes.Light : Themes.Dark
|
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
|
* The engine takes RGBA values from 0-1
|
||||||
* So we convert from the conventional 0-255 found in Figma
|
* So we convert from the conventional 0-255 found in Figma
|
||||||
|