* use engine animations for sketch on face, but not default planes * massage things * fix type issue * small problem in playwright test< * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * some tests fixes * more test tweaks * more test tweaks * clean up * more tidy * tests are a pain * more test stuff * test stuff again * fix micro think axes in sketch mode * more test shit * more test shit more * more test tweaks * more test tweaks * more test stuff * trigger ci * clean up --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1801 lines
56 KiB
TypeScript
1801 lines
56 KiB
TypeScript
import {
|
|
BoxGeometry,
|
|
DoubleSide,
|
|
ExtrudeGeometry,
|
|
Group,
|
|
Intersection,
|
|
LineCurve3,
|
|
Mesh,
|
|
MeshBasicMaterial,
|
|
Object3D,
|
|
Object3DEventMap,
|
|
OrthographicCamera,
|
|
PerspectiveCamera,
|
|
PlaneGeometry,
|
|
Points,
|
|
Quaternion,
|
|
Scene,
|
|
Shape,
|
|
Vector2,
|
|
Vector3,
|
|
} from 'three'
|
|
import {
|
|
ARROWHEAD,
|
|
AXIS_GROUP,
|
|
DEFAULT_PLANES,
|
|
DefaultPlane,
|
|
defaultPlaneColor,
|
|
getSceneScale,
|
|
INTERSECTION_PLANE_LAYER,
|
|
OnMouseEnterLeaveArgs,
|
|
RAYCASTABLE_PLANE,
|
|
SKETCH_GROUP_SEGMENTS,
|
|
SKETCH_LAYER,
|
|
X_AXIS,
|
|
XZ_PLANE,
|
|
Y_AXIS,
|
|
YZ_PLANE,
|
|
} from './sceneInfra'
|
|
import { isQuaternionVertical, quaternionFromUpNForward } from './helpers'
|
|
import {
|
|
CallExpression,
|
|
getTangentialArcToInfo,
|
|
parse,
|
|
Path,
|
|
PathToNode,
|
|
PipeExpression,
|
|
Program,
|
|
ProgramMemory,
|
|
programMemoryInit,
|
|
recast,
|
|
SketchGroup,
|
|
VariableDeclaration,
|
|
VariableDeclarator,
|
|
} from 'lang/wasm'
|
|
import {
|
|
engineCommandManager,
|
|
kclManager,
|
|
sceneInfra,
|
|
codeManager,
|
|
editorManager,
|
|
} from 'lib/singletons'
|
|
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
|
import { executeAst, useStore } from 'useStore'
|
|
import {
|
|
createArcGeometry,
|
|
dashedStraight,
|
|
profileStart,
|
|
straightSegment,
|
|
tangentialArcToSegment,
|
|
} from './segments'
|
|
import {
|
|
addCloseToPipe,
|
|
addNewSketchLn,
|
|
changeSketchArguments,
|
|
updateStartProfileAtArgs,
|
|
} from 'lang/std/sketch'
|
|
import { roundOff, throttle } from 'lib/utils'
|
|
import {
|
|
createArrayExpression,
|
|
createCallExpressionStdLib,
|
|
createLiteral,
|
|
createPipeExpression,
|
|
createPipeSubstitution,
|
|
findUniqueName,
|
|
} from 'lang/modifyAst'
|
|
import {
|
|
getEventForSegmentSelection,
|
|
sendSelectEventToEngine,
|
|
} from 'lib/selections'
|
|
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
|
|
import { createGridHelper, orthoScale, perspScale } from './helpers'
|
|
import { Models } from '@kittycad/lib'
|
|
import { uuidv4 } from 'lib/utils'
|
|
import { SketchDetails } from 'machines/modelingMachine'
|
|
import { EngineCommandManager } from 'lang/std/engineConnection'
|
|
import {
|
|
getRectangleCallExpressions,
|
|
updateRectangleSketch,
|
|
} from 'lib/rectangleTool'
|
|
|
|
type DraftSegment = 'line' | 'tangentialArcTo'
|
|
|
|
export const EXTRA_SEGMENT_HANDLE = 'extraSegmentHandle'
|
|
export const EXTRA_SEGMENT_OFFSET_PX = 8
|
|
export const PROFILE_START = 'profile-start'
|
|
export const STRAIGHT_SEGMENT = 'straight-segment'
|
|
export const STRAIGHT_SEGMENT_BODY = 'straight-segment-body'
|
|
export const STRAIGHT_SEGMENT_DASH = 'straight-segment-body-dashed'
|
|
export const TANGENTIAL_ARC_TO__SEGMENT_DASH =
|
|
'tangential-arc-to-segment-body-dashed'
|
|
export const TANGENTIAL_ARC_TO_SEGMENT = 'tangential-arc-to-segment'
|
|
export const TANGENTIAL_ARC_TO_SEGMENT_BODY = 'tangential-arc-to-segment-body'
|
|
export const SEGMENT_WIDTH_PX = 1.6
|
|
export const HIDE_SEGMENT_LENGTH = 75 // in pixels
|
|
export const HIDE_HOVER_SEGMENT_LENGTH = 60 // in pixels
|
|
|
|
// This singleton Class is responsible for all of the things the user sees and interacts with.
|
|
// That mostly mean sketch elements.
|
|
// Cameras, controls, raycasters, etc are handled by sceneInfra
|
|
export class SceneEntities {
|
|
engineCommandManager: EngineCommandManager
|
|
scene: Scene
|
|
sceneProgramMemory: ProgramMemory = { root: {}, return: null }
|
|
activeSegments: { [key: string]: Group } = {}
|
|
intersectionPlane: Mesh | null = null
|
|
axisGroup: Group | null = null
|
|
currentSketchQuaternion: Quaternion | null = null
|
|
constructor(engineCommandManager: EngineCommandManager) {
|
|
this.engineCommandManager = engineCommandManager
|
|
this.scene = sceneInfra?.scene
|
|
sceneInfra?.camControls.subscribeToCamChange(this.onCamChange)
|
|
window.addEventListener('resize', this.onWindowResize)
|
|
}
|
|
|
|
onWindowResize = () => {
|
|
this.onCamChange()
|
|
}
|
|
onCamChange = () => {
|
|
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
|
|
|
Object.values(this.activeSegments).forEach((segment) => {
|
|
const factor =
|
|
(sceneInfra.camControls.camera instanceof OrthographicCamera
|
|
? orthoFactor
|
|
: perspScale(sceneInfra.camControls.camera, segment)) /
|
|
sceneInfra._baseUnitMultiplier
|
|
if (
|
|
segment.userData.from &&
|
|
segment.userData.to &&
|
|
segment.userData.type === STRAIGHT_SEGMENT
|
|
) {
|
|
this.updateStraightSegment({
|
|
from: segment.userData.from,
|
|
to: segment.userData.to,
|
|
group: segment,
|
|
scale: factor,
|
|
})
|
|
}
|
|
|
|
if (
|
|
segment.userData.from &&
|
|
segment.userData.to &&
|
|
segment.userData.prevSegment &&
|
|
segment.userData.type === TANGENTIAL_ARC_TO_SEGMENT
|
|
) {
|
|
this.updateTangentialArcToSegment({
|
|
prevSegment: segment.userData.prevSegment,
|
|
from: segment.userData.from,
|
|
to: segment.userData.to,
|
|
group: segment,
|
|
scale: factor,
|
|
})
|
|
}
|
|
if (segment.name === PROFILE_START) {
|
|
segment.scale.set(factor, factor, factor)
|
|
}
|
|
})
|
|
if (this.axisGroup) {
|
|
const factor =
|
|
sceneInfra.camControls.camera instanceof OrthographicCamera
|
|
? orthoFactor
|
|
: perspScale(sceneInfra.camControls.camera, this.axisGroup)
|
|
const x = this.axisGroup.getObjectByName(X_AXIS)
|
|
x?.scale.set(1, factor / sceneInfra._baseUnitMultiplier, 1)
|
|
const y = this.axisGroup.getObjectByName(Y_AXIS)
|
|
y?.scale.set(factor / sceneInfra._baseUnitMultiplier, 1, 1)
|
|
}
|
|
}
|
|
|
|
createIntersectionPlane() {
|
|
if (sceneInfra.scene.getObjectByName(RAYCASTABLE_PLANE)) {
|
|
console.warn('createIntersectionPlane called when it already exists')
|
|
return
|
|
}
|
|
const hundredM = 100_0000
|
|
const planeGeometry = new PlaneGeometry(hundredM, hundredM)
|
|
const planeMaterial = new MeshBasicMaterial({
|
|
color: 0xff0000,
|
|
side: DoubleSide,
|
|
transparent: true,
|
|
opacity: 0.5,
|
|
})
|
|
this.intersectionPlane = new Mesh(planeGeometry, planeMaterial)
|
|
this.intersectionPlane.userData = { type: RAYCASTABLE_PLANE }
|
|
this.intersectionPlane.name = RAYCASTABLE_PLANE
|
|
this.intersectionPlane.layers.set(INTERSECTION_PLANE_LAYER)
|
|
this.scene.add(this.intersectionPlane)
|
|
}
|
|
createSketchAxis(
|
|
sketchPathToNode: PathToNode,
|
|
forward: [number, number, number],
|
|
up: [number, number, number],
|
|
sketchPosition?: [number, number, number]
|
|
) {
|
|
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
|
const baseXColor = 0x000055
|
|
const baseYColor = 0x550000
|
|
const axisPixelWidth = 1.6
|
|
const xAxisGeometry = new BoxGeometry(100000, axisPixelWidth, 0.01)
|
|
const yAxisGeometry = new BoxGeometry(axisPixelWidth, 100000, 0.01)
|
|
const xAxisMaterial = new MeshBasicMaterial({
|
|
color: baseXColor,
|
|
depthTest: false,
|
|
})
|
|
const yAxisMaterial = new MeshBasicMaterial({
|
|
color: baseYColor,
|
|
depthTest: false,
|
|
})
|
|
const xAxisMesh = new Mesh(xAxisGeometry, xAxisMaterial)
|
|
const yAxisMesh = new Mesh(yAxisGeometry, yAxisMaterial)
|
|
xAxisMesh.renderOrder = -2
|
|
yAxisMesh.renderOrder = -1
|
|
xAxisMesh.userData = {
|
|
type: X_AXIS,
|
|
baseColor: baseXColor,
|
|
isSelected: false,
|
|
}
|
|
yAxisMesh.userData = {
|
|
type: Y_AXIS,
|
|
baseColor: baseYColor,
|
|
isSelected: false,
|
|
}
|
|
xAxisMesh.name = X_AXIS
|
|
yAxisMesh.name = Y_AXIS
|
|
|
|
this.axisGroup = new Group()
|
|
const gridHelper = createGridHelper({ size: 100, divisions: 10 })
|
|
gridHelper.position.z = -0.01
|
|
gridHelper.renderOrder = -3 // is this working?
|
|
gridHelper.name = 'gridHelper'
|
|
const sceneScale = getSceneScale(
|
|
sceneInfra.camControls.camera,
|
|
sceneInfra.camControls.target
|
|
)
|
|
gridHelper.scale.set(sceneScale, sceneScale, sceneScale)
|
|
|
|
const factor =
|
|
sceneInfra.camControls.camera instanceof OrthographicCamera
|
|
? orthoFactor
|
|
: perspScale(sceneInfra.camControls.camera, this.axisGroup)
|
|
xAxisMesh?.scale.set(1, factor / sceneInfra._baseUnitMultiplier, 1)
|
|
yAxisMesh?.scale.set(factor / sceneInfra._baseUnitMultiplier, 1, 1)
|
|
|
|
this.axisGroup.add(xAxisMesh, yAxisMesh, gridHelper)
|
|
this.currentSketchQuaternion &&
|
|
this.axisGroup.setRotationFromQuaternion(this.currentSketchQuaternion)
|
|
|
|
this.axisGroup.userData = { type: AXIS_GROUP }
|
|
this.axisGroup.name = AXIS_GROUP
|
|
this.axisGroup.layers.set(SKETCH_LAYER)
|
|
this.axisGroup.traverse((child) => {
|
|
child.layers.set(SKETCH_LAYER)
|
|
})
|
|
|
|
const quat = quaternionFromUpNForward(
|
|
new Vector3(...up),
|
|
new Vector3(...forward)
|
|
)
|
|
this.axisGroup.setRotationFromQuaternion(quat)
|
|
sketchPosition && this.axisGroup.position.set(...sketchPosition)
|
|
this.scene.add(this.axisGroup)
|
|
}
|
|
removeIntersectionPlane() {
|
|
const intersectionPlane = this.scene.getObjectByName(RAYCASTABLE_PLANE)
|
|
if (intersectionPlane) this.scene.remove(intersectionPlane)
|
|
}
|
|
|
|
async setupSketch({
|
|
sketchPathToNode,
|
|
forward,
|
|
up,
|
|
position,
|
|
maybeModdedAst,
|
|
draftExpressionsIndices,
|
|
}: {
|
|
sketchPathToNode: PathToNode
|
|
maybeModdedAst: Program
|
|
draftExpressionsIndices?: { start: number; end: number }
|
|
forward: [number, number, number]
|
|
up: [number, number, number]
|
|
position?: [number, number, number]
|
|
}): Promise<{
|
|
truncatedAst: Program
|
|
programMemoryOverride: ProgramMemory
|
|
sketchGroup: SketchGroup
|
|
variableDeclarationName: string
|
|
}> {
|
|
this.createIntersectionPlane()
|
|
|
|
const { truncatedAst, programMemoryOverride, variableDeclarationName } =
|
|
this.prepareTruncatedMemoryAndAst(sketchPathToNode || [], maybeModdedAst)
|
|
const { programMemory } = await executeAst({
|
|
ast: truncatedAst,
|
|
useFakeExecutor: true,
|
|
engineCommandManager: this.engineCommandManager,
|
|
programMemoryOverride,
|
|
})
|
|
const sketchGroup = sketchGroupFromPathToNode({
|
|
pathToNode: sketchPathToNode,
|
|
ast: maybeModdedAst,
|
|
programMemory,
|
|
})
|
|
if (!Array.isArray(sketchGroup?.value))
|
|
return {
|
|
truncatedAst,
|
|
programMemoryOverride,
|
|
sketchGroup,
|
|
variableDeclarationName,
|
|
}
|
|
this.sceneProgramMemory = programMemory
|
|
const group = new Group()
|
|
position && group.position.set(...position)
|
|
group.userData = {
|
|
type: SKETCH_GROUP_SEGMENTS,
|
|
pathToNode: sketchPathToNode,
|
|
}
|
|
const dummy = new Mesh()
|
|
dummy.position.set(
|
|
sketchGroup.position[0],
|
|
sketchGroup.position[1],
|
|
sketchGroup.position[2]
|
|
)
|
|
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
|
const factor =
|
|
(sceneInfra.camControls.camera instanceof OrthographicCamera
|
|
? orthoFactor
|
|
: perspScale(sceneInfra.camControls.camera, dummy)) /
|
|
sceneInfra._baseUnitMultiplier
|
|
|
|
const segPathToNode = getNodePathFromSourceRange(
|
|
maybeModdedAst,
|
|
sketchGroup.start.__geoMeta.sourceRange
|
|
)
|
|
const _profileStart = profileStart({
|
|
from: sketchGroup.start.from,
|
|
id: sketchGroup.start.__geoMeta.id,
|
|
pathToNode: segPathToNode,
|
|
scale: factor,
|
|
})
|
|
_profileStart.layers.set(SKETCH_LAYER)
|
|
_profileStart.traverse((child) => {
|
|
child.layers.set(SKETCH_LAYER)
|
|
})
|
|
group.add(_profileStart)
|
|
this.activeSegments[JSON.stringify(segPathToNode)] = _profileStart
|
|
|
|
sketchGroup.value.forEach((segment, index) => {
|
|
let segPathToNode = getNodePathFromSourceRange(
|
|
maybeModdedAst,
|
|
segment.__geoMeta.sourceRange
|
|
)
|
|
if (
|
|
draftExpressionsIndices &&
|
|
(sketchGroup.value[index - 1] || sketchGroup.start)
|
|
) {
|
|
const previousSegment =
|
|
sketchGroup.value[index - 1] || sketchGroup.start
|
|
const previousSegmentPathToNode = getNodePathFromSourceRange(
|
|
maybeModdedAst,
|
|
previousSegment.__geoMeta.sourceRange
|
|
)
|
|
const bodyIndex = previousSegmentPathToNode[1][0]
|
|
segPathToNode = getNodePathFromSourceRange(
|
|
truncatedAst,
|
|
segment.__geoMeta.sourceRange
|
|
)
|
|
segPathToNode[1][0] = bodyIndex
|
|
}
|
|
const isDraftSegment =
|
|
draftExpressionsIndices &&
|
|
index <= draftExpressionsIndices.end &&
|
|
index >= draftExpressionsIndices.start
|
|
let seg
|
|
const callExpName = getNodeFromPath<CallExpression>(
|
|
maybeModdedAst,
|
|
segPathToNode,
|
|
'CallExpression'
|
|
)?.node?.callee?.name
|
|
if (segment.type === 'TangentialArcTo') {
|
|
seg = tangentialArcToSegment({
|
|
prevSegment: sketchGroup.value[index - 1],
|
|
from: segment.from,
|
|
to: segment.to,
|
|
id: segment.__geoMeta.id,
|
|
pathToNode: segPathToNode,
|
|
isDraftSegment,
|
|
scale: factor,
|
|
texture: sceneInfra.extraSegmentTexture,
|
|
})
|
|
} else {
|
|
seg = straightSegment({
|
|
from: segment.from,
|
|
to: segment.to,
|
|
id: segment.__geoMeta.id,
|
|
pathToNode: segPathToNode,
|
|
isDraftSegment,
|
|
scale: factor,
|
|
callExpName,
|
|
texture: sceneInfra.extraSegmentTexture,
|
|
})
|
|
}
|
|
seg.layers.set(SKETCH_LAYER)
|
|
seg.traverse((child) => {
|
|
child.layers.set(SKETCH_LAYER)
|
|
})
|
|
|
|
group.add(seg)
|
|
this.activeSegments[JSON.stringify(segPathToNode)] = seg
|
|
})
|
|
|
|
this.currentSketchQuaternion = quaternionFromUpNForward(
|
|
new Vector3(...up),
|
|
new Vector3(...forward)
|
|
)
|
|
group.setRotationFromQuaternion(this.currentSketchQuaternion)
|
|
this.intersectionPlane &&
|
|
this.intersectionPlane.setRotationFromQuaternion(
|
|
this.currentSketchQuaternion
|
|
)
|
|
this.intersectionPlane &&
|
|
position &&
|
|
this.intersectionPlane.position.set(...position)
|
|
this.scene.add(group)
|
|
sceneInfra.camControls.enableRotate = false
|
|
|
|
return {
|
|
truncatedAst,
|
|
programMemoryOverride,
|
|
sketchGroup,
|
|
variableDeclarationName,
|
|
}
|
|
}
|
|
updateAstAndRejigSketch = async (
|
|
sketchPathToNode: PathToNode,
|
|
modifiedAst: Program,
|
|
forward: [number, number, number],
|
|
up: [number, number, number],
|
|
origin: [number, number, number]
|
|
) => {
|
|
await kclManager.updateAst(modifiedAst, false)
|
|
await this.tearDownSketch({ removeAxis: false })
|
|
sceneInfra.resetMouseListeners()
|
|
await this.setupSketch({
|
|
sketchPathToNode,
|
|
forward,
|
|
up,
|
|
position: origin,
|
|
maybeModdedAst: kclManager.ast,
|
|
})
|
|
this.setupSketchIdleCallbacks({
|
|
forward,
|
|
up,
|
|
position: origin,
|
|
pathToNode: sketchPathToNode,
|
|
})
|
|
}
|
|
setUpDraftSegment = async (
|
|
sketchPathToNode: PathToNode,
|
|
forward: [number, number, number],
|
|
up: [number, number, number],
|
|
origin: [number, number, number],
|
|
segmentName: 'line' | 'tangentialArcTo' = 'line',
|
|
shouldTearDown = true
|
|
) => {
|
|
const _ast = JSON.parse(JSON.stringify(kclManager.ast))
|
|
|
|
const variableDeclarationName =
|
|
getNodeFromPath<VariableDeclaration>(
|
|
_ast,
|
|
sketchPathToNode || [],
|
|
'VariableDeclaration'
|
|
)?.node?.declarations?.[0]?.id?.name || ''
|
|
const sg = kclManager.programMemory.root[
|
|
variableDeclarationName
|
|
] as SketchGroup
|
|
const lastSeg = sg.value.slice(-1)[0] || sg.start
|
|
|
|
const index = sg.value.length // because we've added a new segment that's not in the memory yet, no need for `-1`
|
|
|
|
const mod = addNewSketchLn({
|
|
node: _ast,
|
|
programMemory: kclManager.programMemory,
|
|
to: [lastSeg.to[0], lastSeg.to[1]],
|
|
from: [lastSeg.to[0], lastSeg.to[1]],
|
|
fnName: segmentName,
|
|
pathToNode: sketchPathToNode,
|
|
})
|
|
const modifiedAst = parse(recast(mod.modifiedAst))
|
|
|
|
const draftExpressionsIndices = { start: index, end: index }
|
|
|
|
if (shouldTearDown) await this.tearDownSketch({ removeAxis: false })
|
|
sceneInfra.resetMouseListeners()
|
|
const { truncatedAst, programMemoryOverride, sketchGroup } =
|
|
await this.setupSketch({
|
|
sketchPathToNode,
|
|
forward,
|
|
up,
|
|
position: origin,
|
|
maybeModdedAst: modifiedAst,
|
|
draftExpressionsIndices,
|
|
})
|
|
sceneInfra.setCallbacks({
|
|
onClick: async (args) => {
|
|
if (!args) return
|
|
if (args.mouseEvent.which !== 1) return
|
|
const { intersectionPoint } = args
|
|
let intersection2d = intersectionPoint?.twoD
|
|
const profileStart = args.intersects
|
|
.map(({ object }) => getParentGroup(object, [PROFILE_START]))
|
|
.find((a) => a?.name === PROFILE_START)
|
|
|
|
let modifiedAst
|
|
if (profileStart) {
|
|
modifiedAst = addCloseToPipe({
|
|
node: kclManager.ast,
|
|
programMemory: kclManager.programMemory,
|
|
pathToNode: sketchPathToNode,
|
|
})
|
|
} else if (intersection2d) {
|
|
const lastSegment = sketchGroup.value.slice(-1)[0]
|
|
modifiedAst = addNewSketchLn({
|
|
node: kclManager.ast,
|
|
programMemory: kclManager.programMemory,
|
|
to: [intersection2d.x, intersection2d.y],
|
|
from: [lastSegment.to[0], lastSegment.to[1]],
|
|
fnName:
|
|
lastSegment.type === 'TangentialArcTo'
|
|
? 'tangentialArcTo'
|
|
: 'line',
|
|
pathToNode: sketchPathToNode,
|
|
}).modifiedAst
|
|
} else {
|
|
// return early as we didn't modify the ast
|
|
return
|
|
}
|
|
|
|
await kclManager.executeAstMock(modifiedAst)
|
|
this.setUpDraftSegment(
|
|
sketchPathToNode,
|
|
forward,
|
|
up,
|
|
origin,
|
|
segmentName
|
|
)
|
|
},
|
|
onMove: (args) => {
|
|
this.onDragSegment({
|
|
intersection2d: args.intersectionPoint.twoD,
|
|
object: Object.values(this.activeSegments).slice(-1)[0],
|
|
intersects: args.intersects,
|
|
sketchPathToNode,
|
|
draftInfo: {
|
|
truncatedAst,
|
|
programMemoryOverride,
|
|
variableDeclarationName,
|
|
},
|
|
})
|
|
},
|
|
...this.mouseEnterLeaveCallbacks(),
|
|
})
|
|
}
|
|
setupRectangleOriginListener = () => {
|
|
sceneInfra.setCallbacks({
|
|
onClick: (args) => {
|
|
const twoD = args.intersectionPoint?.twoD
|
|
if (!twoD) {
|
|
console.warn(`This click didn't have a 2D intersection`, args)
|
|
return
|
|
}
|
|
sceneInfra.modelingSend({
|
|
type: 'Add rectangle origin',
|
|
data: [twoD.x, twoD.y],
|
|
})
|
|
},
|
|
})
|
|
}
|
|
setupDraftRectangle = async (
|
|
sketchPathToNode: PathToNode,
|
|
forward: [number, number, number],
|
|
up: [number, number, number],
|
|
sketchOrigin: [number, number, number],
|
|
rectangleOrigin: [x: number, y: number]
|
|
) => {
|
|
let _ast = JSON.parse(JSON.stringify(kclManager.ast))
|
|
|
|
const variableDeclarationName =
|
|
getNodeFromPath<VariableDeclaration>(
|
|
_ast,
|
|
sketchPathToNode || [],
|
|
'VariableDeclaration'
|
|
)?.node?.declarations?.[0]?.id?.name || ''
|
|
|
|
const tags: [string, string, string] = [
|
|
findUniqueName(_ast, 'rectangleSegmentA'),
|
|
findUniqueName(_ast, 'rectangleSegmentB'),
|
|
findUniqueName(_ast, 'rectangleSegmentC'),
|
|
]
|
|
|
|
const startSketchOn = getNodeFromPath<VariableDeclaration>(
|
|
_ast,
|
|
sketchPathToNode || [],
|
|
'VariableDeclaration'
|
|
)?.node?.declarations
|
|
|
|
const startSketchOnInit = startSketchOn?.[0]?.init
|
|
startSketchOn[0].init = createPipeExpression([
|
|
startSketchOnInit,
|
|
...getRectangleCallExpressions(rectangleOrigin, tags),
|
|
])
|
|
|
|
_ast = parse(recast(_ast))
|
|
|
|
const { programMemoryOverride, truncatedAst } = await this.setupSketch({
|
|
sketchPathToNode,
|
|
forward,
|
|
up,
|
|
position: sketchOrigin,
|
|
maybeModdedAst: _ast,
|
|
draftExpressionsIndices: { start: 0, end: 3 },
|
|
})
|
|
|
|
sceneInfra.setCallbacks({
|
|
onMove: async (args) => {
|
|
// Update the width and height of the draft rectangle
|
|
const pathToNodeTwo = JSON.parse(JSON.stringify(sketchPathToNode))
|
|
pathToNodeTwo[1][0] = 0
|
|
|
|
const sketchInit = getNodeFromPath<VariableDeclaration>(
|
|
truncatedAst,
|
|
pathToNodeTwo || [],
|
|
'VariableDeclaration'
|
|
)?.node?.declarations?.[0]?.init
|
|
|
|
const x = (args.intersectionPoint.twoD.x || 0) - rectangleOrigin[0]
|
|
const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1]
|
|
|
|
if (sketchInit.type === 'PipeExpression') {
|
|
updateRectangleSketch(sketchInit, x, y, tags[0])
|
|
}
|
|
|
|
const { programMemory } = await executeAst({
|
|
ast: truncatedAst,
|
|
useFakeExecutor: true,
|
|
engineCommandManager: this.engineCommandManager,
|
|
programMemoryOverride,
|
|
})
|
|
this.sceneProgramMemory = programMemory
|
|
const sketchGroup = programMemory.root[
|
|
variableDeclarationName
|
|
] as SketchGroup
|
|
const sgPaths = sketchGroup.value
|
|
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
|
|
|
this.updateSegment(
|
|
sketchGroup.start,
|
|
0,
|
|
0,
|
|
_ast,
|
|
orthoFactor,
|
|
sketchGroup
|
|
)
|
|
sgPaths.forEach((seg, index) =>
|
|
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketchGroup)
|
|
)
|
|
},
|
|
onClick: async (args) => {
|
|
// Commit the rectangle to the full AST/code and return to sketch.idle
|
|
const cornerPoint = args.intersectionPoint?.twoD
|
|
if (!cornerPoint || args.mouseEvent.button !== 0) return
|
|
|
|
const x = roundOff((cornerPoint.x || 0) - rectangleOrigin[0])
|
|
const y = roundOff((cornerPoint.y || 0) - rectangleOrigin[1])
|
|
|
|
const sketchInit = getNodeFromPath<VariableDeclaration>(
|
|
_ast,
|
|
sketchPathToNode || [],
|
|
'VariableDeclaration'
|
|
)?.node?.declarations?.[0]?.init
|
|
|
|
if (sketchInit.type === 'PipeExpression') {
|
|
updateRectangleSketch(sketchInit, x, y, tags[0])
|
|
|
|
_ast = parse(recast(_ast))
|
|
|
|
console.log('onClick', {
|
|
sketchInit: sketchInit,
|
|
_ast,
|
|
x,
|
|
y,
|
|
truncatedAst,
|
|
})
|
|
|
|
// Update the primary AST and unequip the rectangle tool
|
|
await kclManager.executeAstMock(_ast)
|
|
sceneInfra.modelingSend({ type: 'CancelSketch' })
|
|
|
|
const { programMemory } = await executeAst({
|
|
ast: _ast,
|
|
useFakeExecutor: true,
|
|
engineCommandManager: this.engineCommandManager,
|
|
programMemoryOverride,
|
|
})
|
|
|
|
// Prepare to update the THREEjs scene
|
|
this.sceneProgramMemory = programMemory
|
|
const sketchGroup = programMemory.root[
|
|
variableDeclarationName
|
|
] as SketchGroup
|
|
const sgPaths = sketchGroup.value
|
|
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
|
|
|
// Update the starting segment of the THREEjs scene
|
|
this.updateSegment(
|
|
sketchGroup.start,
|
|
0,
|
|
0,
|
|
_ast,
|
|
orthoFactor,
|
|
sketchGroup
|
|
)
|
|
// Update the rest of the segments of the THREEjs scene
|
|
sgPaths.forEach((seg, index) =>
|
|
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketchGroup)
|
|
)
|
|
}
|
|
},
|
|
})
|
|
}
|
|
setupSketchIdleCallbacks = ({
|
|
pathToNode,
|
|
up,
|
|
forward,
|
|
position,
|
|
}: {
|
|
pathToNode: PathToNode
|
|
forward: [number, number, number]
|
|
up: [number, number, number]
|
|
position?: [number, number, number]
|
|
}) => {
|
|
let addingNewSegmentStatus: 'nothing' | 'pending' | 'added' = 'nothing'
|
|
sceneInfra.setCallbacks({
|
|
onDragEnd: async () => {
|
|
if (addingNewSegmentStatus !== 'nothing') {
|
|
await this.tearDownSketch({ removeAxis: false })
|
|
this.setupSketch({
|
|
sketchPathToNode: pathToNode,
|
|
maybeModdedAst: kclManager.ast,
|
|
up,
|
|
forward,
|
|
position,
|
|
})
|
|
// setting up the callbacks again resets value in closures
|
|
this.setupSketchIdleCallbacks({
|
|
pathToNode,
|
|
up,
|
|
forward,
|
|
position,
|
|
})
|
|
}
|
|
},
|
|
onDrag: async ({
|
|
selected,
|
|
intersectionPoint,
|
|
mouseEvent,
|
|
intersects,
|
|
}) => {
|
|
if (mouseEvent.which !== 1) return
|
|
|
|
const group = getParentGroup(selected, [EXTRA_SEGMENT_HANDLE])
|
|
if (group?.name === EXTRA_SEGMENT_HANDLE) {
|
|
const segGroup = getParentGroup(selected)
|
|
const pathToNode: PathToNode = segGroup?.userData?.pathToNode
|
|
const pathToNodeIndex = pathToNode.findIndex(
|
|
(x) => x[1] === 'PipeExpression'
|
|
)
|
|
|
|
const sketchGroup = sketchGroupFromPathToNode({
|
|
pathToNode,
|
|
ast: kclManager.ast,
|
|
programMemory: kclManager.programMemory,
|
|
})
|
|
|
|
const pipeIndex = pathToNode[pathToNodeIndex + 1][0] as number
|
|
if (addingNewSegmentStatus === 'nothing') {
|
|
const prevSegment = sketchGroup.value[pipeIndex - 2]
|
|
const mod = addNewSketchLn({
|
|
node: kclManager.ast,
|
|
programMemory: kclManager.programMemory,
|
|
to: [intersectionPoint.twoD.x, intersectionPoint.twoD.y],
|
|
from: [prevSegment.from[0], prevSegment.from[1]],
|
|
// TODO assuming it's always a straight segments being added
|
|
// as this is easiest, and we'll need to add "tabbing" behavior
|
|
// to support other segment types
|
|
fnName: 'line',
|
|
pathToNode: pathToNode,
|
|
spliceBetween: true,
|
|
})
|
|
addingNewSegmentStatus = 'pending'
|
|
await kclManager.executeAstMock(mod.modifiedAst)
|
|
await this.tearDownSketch({ removeAxis: false })
|
|
this.setupSketch({
|
|
sketchPathToNode: pathToNode,
|
|
maybeModdedAst: kclManager.ast,
|
|
up,
|
|
forward,
|
|
position,
|
|
})
|
|
addingNewSegmentStatus = 'added'
|
|
} else if (addingNewSegmentStatus === 'added') {
|
|
const pathToNodeForNewSegment = pathToNode.slice(0, pathToNodeIndex)
|
|
pathToNodeForNewSegment.push([pipeIndex - 2, 'index'])
|
|
this.onDragSegment({
|
|
sketchPathToNode: pathToNodeForNewSegment,
|
|
object: selected,
|
|
intersection2d: intersectionPoint.twoD,
|
|
intersects,
|
|
})
|
|
}
|
|
return
|
|
}
|
|
|
|
this.onDragSegment({
|
|
object: selected,
|
|
intersection2d: intersectionPoint.twoD,
|
|
intersects,
|
|
sketchPathToNode: pathToNode,
|
|
})
|
|
},
|
|
onMove: () => {},
|
|
onClick: (args) => {
|
|
if (args?.mouseEvent.which !== 1) return
|
|
if (!args || !args.selected) {
|
|
sceneInfra.modelingSend({
|
|
type: 'Set selection',
|
|
data: {
|
|
selectionType: 'singleCodeCursor',
|
|
},
|
|
})
|
|
return
|
|
}
|
|
const { selected } = args
|
|
const event = getEventForSegmentSelection(selected)
|
|
if (!event) return
|
|
sceneInfra.modelingSend(event)
|
|
},
|
|
...this.mouseEnterLeaveCallbacks(),
|
|
})
|
|
}
|
|
prepareTruncatedMemoryAndAst = (
|
|
sketchPathToNode: PathToNode,
|
|
ast?: Program,
|
|
draftSegment?: DraftSegment
|
|
) =>
|
|
prepareTruncatedMemoryAndAst(
|
|
sketchPathToNode,
|
|
ast || kclManager.ast,
|
|
kclManager.programMemory,
|
|
draftSegment
|
|
)
|
|
onDragSegment({
|
|
object,
|
|
intersection2d: _intersection2d,
|
|
sketchPathToNode,
|
|
draftInfo,
|
|
intersects,
|
|
}: {
|
|
object: any
|
|
intersection2d: Vector2
|
|
sketchPathToNode: PathToNode
|
|
intersects: Intersection<Object3D<Object3DEventMap>>[]
|
|
draftInfo?: {
|
|
truncatedAst: Program
|
|
programMemoryOverride: ProgramMemory
|
|
variableDeclarationName: string
|
|
}
|
|
}) {
|
|
const profileStart =
|
|
draftInfo &&
|
|
intersects
|
|
.map(({ object }) => getParentGroup(object, [PROFILE_START]))
|
|
.find((a) => a?.name === PROFILE_START)
|
|
const intersection2d = profileStart
|
|
? new Vector2(profileStart.position.x, profileStart.position.y)
|
|
: _intersection2d
|
|
|
|
const group = getParentGroup(object, [
|
|
STRAIGHT_SEGMENT,
|
|
TANGENTIAL_ARC_TO_SEGMENT,
|
|
PROFILE_START,
|
|
])
|
|
if (!group) return
|
|
const pathToNode: PathToNode = JSON.parse(
|
|
JSON.stringify(group.userData.pathToNode)
|
|
)
|
|
const varDecIndex = JSON.parse(JSON.stringify(pathToNode[1][0]))
|
|
if (draftInfo) {
|
|
pathToNode[1][0] = 0
|
|
}
|
|
|
|
const from: [number, number] = [
|
|
group.userData.from[0],
|
|
group.userData.from[1],
|
|
]
|
|
const to: [number, number] = [intersection2d.x, intersection2d.y]
|
|
let modifiedAst = draftInfo ? draftInfo.truncatedAst : { ...kclManager.ast }
|
|
|
|
const node = getNodeFromPath<CallExpression>(
|
|
modifiedAst,
|
|
pathToNode,
|
|
'CallExpression'
|
|
).node
|
|
if (node.type !== 'CallExpression') return
|
|
|
|
let modded: {
|
|
modifiedAst: Program
|
|
pathToNode: PathToNode
|
|
}
|
|
if (group.name === PROFILE_START) {
|
|
modded = updateStartProfileAtArgs({
|
|
node: modifiedAst,
|
|
pathToNode,
|
|
to,
|
|
from,
|
|
previousProgramMemory: kclManager.programMemory,
|
|
})
|
|
} else {
|
|
modded = changeSketchArguments(
|
|
modifiedAst,
|
|
kclManager.programMemory,
|
|
[node.start, node.end],
|
|
to,
|
|
from
|
|
)
|
|
}
|
|
|
|
modifiedAst = modded.modifiedAst
|
|
const { truncatedAst, programMemoryOverride, variableDeclarationName } =
|
|
draftInfo
|
|
? draftInfo
|
|
: this.prepareTruncatedMemoryAndAst(sketchPathToNode || [])
|
|
;(async () => {
|
|
const code = recast(modifiedAst)
|
|
if (!draftInfo)
|
|
// don't want to mod the user's code yet as they have't committed to the change yet
|
|
// plus this would be the truncated ast being recast, it would be wrong
|
|
codeManager.updateCodeStateEditor(code)
|
|
const { programMemory } = await executeAst({
|
|
ast: truncatedAst,
|
|
useFakeExecutor: true,
|
|
engineCommandManager: this.engineCommandManager,
|
|
programMemoryOverride,
|
|
})
|
|
this.sceneProgramMemory = programMemory
|
|
const sketchGroup = programMemory.root[
|
|
variableDeclarationName
|
|
] as SketchGroup
|
|
const sgPaths = sketchGroup.value
|
|
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
|
|
|
this.updateSegment(
|
|
sketchGroup.start,
|
|
0,
|
|
varDecIndex,
|
|
modifiedAst,
|
|
orthoFactor,
|
|
sketchGroup
|
|
)
|
|
sgPaths.forEach((group, index) =>
|
|
this.updateSegment(
|
|
group,
|
|
index,
|
|
varDecIndex,
|
|
modifiedAst,
|
|
orthoFactor,
|
|
sketchGroup
|
|
)
|
|
)
|
|
})()
|
|
}
|
|
|
|
/**
|
|
* Update the THREEjs sketch entities with new segment data
|
|
* mapping them back to the AST
|
|
* @param segment
|
|
* @param index
|
|
* @param varDecIndex
|
|
* @param modifiedAst
|
|
* @param orthoFactor
|
|
* @param sketchGroup
|
|
*/
|
|
updateSegment = (
|
|
segment: Path | SketchGroup['start'],
|
|
index: number,
|
|
varDecIndex: number,
|
|
modifiedAst: Program,
|
|
orthoFactor: number,
|
|
sketchGroup: SketchGroup
|
|
) => {
|
|
const segPathToNode = getNodePathFromSourceRange(
|
|
modifiedAst,
|
|
segment.__geoMeta.sourceRange
|
|
)
|
|
const sgPaths = sketchGroup.value
|
|
const originalPathToNodeStr = JSON.stringify(segPathToNode)
|
|
segPathToNode[1][0] = varDecIndex
|
|
const pathToNodeStr = JSON.stringify(segPathToNode)
|
|
// more hacks to hopefully be solved by proper pathToNode info in memory/sketchGroup segments
|
|
const group =
|
|
this.activeSegments[pathToNodeStr] ||
|
|
this.activeSegments[originalPathToNodeStr]
|
|
// const prevSegment = sketchGroup.slice(index - 1)[0]
|
|
const type = group?.userData?.type
|
|
const factor =
|
|
(sceneInfra.camControls.camera instanceof OrthographicCamera
|
|
? orthoFactor
|
|
: perspScale(sceneInfra.camControls.camera, group)) /
|
|
sceneInfra._baseUnitMultiplier
|
|
if (type === TANGENTIAL_ARC_TO_SEGMENT) {
|
|
this.updateTangentialArcToSegment({
|
|
prevSegment: sgPaths[index - 1],
|
|
from: segment.from,
|
|
to: segment.to,
|
|
group: group,
|
|
scale: factor,
|
|
})
|
|
} else if (type === STRAIGHT_SEGMENT) {
|
|
this.updateStraightSegment({
|
|
from: segment.from,
|
|
to: segment.to,
|
|
group,
|
|
scale: factor,
|
|
})
|
|
} else if (type === PROFILE_START) {
|
|
group.position.set(segment.from[0], segment.from[1], 0)
|
|
group.scale.set(factor, factor, factor)
|
|
}
|
|
}
|
|
|
|
updateTangentialArcToSegment({
|
|
prevSegment,
|
|
from,
|
|
to,
|
|
group,
|
|
scale = 1,
|
|
}: {
|
|
prevSegment: SketchGroup['value'][number]
|
|
from: [number, number]
|
|
to: [number, number]
|
|
group: Group
|
|
scale?: number
|
|
}) {
|
|
group.userData.from = from
|
|
group.userData.to = to
|
|
group.userData.prevSegment = prevSegment
|
|
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
|
const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
|
|
|
const previousPoint =
|
|
prevSegment?.type === 'TangentialArcTo'
|
|
? getTangentPointFromPreviousArc(
|
|
prevSegment.center,
|
|
prevSegment.ccw,
|
|
prevSegment.to
|
|
)
|
|
: prevSegment.from
|
|
|
|
const arcInfo = getTangentialArcToInfo({
|
|
arcStartPoint: from,
|
|
arcEndPoint: to,
|
|
tanPreviousPoint: previousPoint,
|
|
obtuse: true,
|
|
})
|
|
|
|
const pxLength = arcInfo.arcLength / scale
|
|
const shouldHideIdle = pxLength < HIDE_SEGMENT_LENGTH
|
|
const shouldHideHover = pxLength < HIDE_HOVER_SEGMENT_LENGTH
|
|
|
|
const hoveredParent =
|
|
sceneInfra.hoveredObject &&
|
|
getParentGroup(sceneInfra.hoveredObject, [TANGENTIAL_ARC_TO_SEGMENT])
|
|
let isHandlesVisible = !shouldHideIdle
|
|
if (hoveredParent && hoveredParent?.uuid === group?.uuid) {
|
|
isHandlesVisible = !shouldHideHover
|
|
}
|
|
|
|
if (arrowGroup) {
|
|
arrowGroup.position.set(to[0], to[1], 0)
|
|
|
|
const arrowheadAngle =
|
|
arcInfo.endAngle + (Math.PI / 2) * (arcInfo.ccw ? 1 : -1)
|
|
arrowGroup.quaternion.setFromUnitVectors(
|
|
new Vector3(0, 1, 0),
|
|
new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0)
|
|
)
|
|
arrowGroup.scale.set(scale, scale, scale)
|
|
arrowGroup.visible = isHandlesVisible
|
|
}
|
|
|
|
if (extraSegmentGroup) {
|
|
const circumferenceInPx = (2 * Math.PI * arcInfo.radius) / scale
|
|
const extraSegmentAngleDelta =
|
|
(EXTRA_SEGMENT_OFFSET_PX / circumferenceInPx) * Math.PI * 2
|
|
const extraSegmentAngle =
|
|
arcInfo.startAngle + (arcInfo.ccw ? 1 : -1) * extraSegmentAngleDelta
|
|
const extraSegmentOffset = new Vector2(
|
|
Math.cos(extraSegmentAngle) * arcInfo.radius,
|
|
Math.sin(extraSegmentAngle) * arcInfo.radius
|
|
)
|
|
extraSegmentGroup.position.set(
|
|
arcInfo.center[0] + extraSegmentOffset.x,
|
|
arcInfo.center[1] + extraSegmentOffset.y,
|
|
0
|
|
)
|
|
extraSegmentGroup.scale.set(scale, scale, scale)
|
|
extraSegmentGroup.visible = isHandlesVisible
|
|
}
|
|
|
|
const tangentialArcToSegmentBody = group.children.find(
|
|
(child) => child.userData.type === TANGENTIAL_ARC_TO_SEGMENT_BODY
|
|
) as Mesh
|
|
|
|
if (tangentialArcToSegmentBody) {
|
|
const newGeo = createArcGeometry({ ...arcInfo, scale })
|
|
tangentialArcToSegmentBody.geometry = newGeo
|
|
}
|
|
const tangentialArcToSegmentBodyDashed = group.children.find(
|
|
(child) => child.userData.type === TANGENTIAL_ARC_TO__SEGMENT_DASH
|
|
) as Mesh
|
|
if (tangentialArcToSegmentBodyDashed) {
|
|
// consider throttling the whole updateTangentialArcToSegment
|
|
// if there are more perf considerations going forward
|
|
this.throttledUpdateDashedArcGeo({
|
|
...arcInfo,
|
|
mesh: tangentialArcToSegmentBodyDashed,
|
|
isDashed: true,
|
|
scale,
|
|
})
|
|
}
|
|
}
|
|
throttledUpdateDashedArcGeo = throttle(
|
|
(
|
|
args: Parameters<typeof createArcGeometry>[0] & {
|
|
mesh: Mesh
|
|
scale: number
|
|
}
|
|
) => (args.mesh.geometry = createArcGeometry(args)),
|
|
1000 / 30
|
|
)
|
|
updateStraightSegment({
|
|
from,
|
|
to,
|
|
group,
|
|
scale = 1,
|
|
}: {
|
|
from: [number, number]
|
|
to: [number, number]
|
|
group: Group
|
|
scale?: number
|
|
}) {
|
|
group.userData.from = from
|
|
group.userData.to = to
|
|
const shape = new Shape()
|
|
shape.moveTo(0, (-SEGMENT_WIDTH_PX / 2) * scale) // The width of the line in px (2.4px in this case)
|
|
shape.lineTo(0, (SEGMENT_WIDTH_PX / 2) * scale)
|
|
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
|
|
|
const length = Math.sqrt(
|
|
Math.pow(to[0] - from[0], 2) + Math.pow(to[1] - from[1], 2)
|
|
)
|
|
|
|
const pxLength = length / scale
|
|
const shouldHideIdle = pxLength < HIDE_SEGMENT_LENGTH
|
|
const shouldHideHover = pxLength < HIDE_HOVER_SEGMENT_LENGTH
|
|
|
|
const hoveredParent =
|
|
sceneInfra.hoveredObject &&
|
|
getParentGroup(sceneInfra.hoveredObject, [STRAIGHT_SEGMENT])
|
|
let isHandlesVisible = !shouldHideIdle
|
|
if (hoveredParent && hoveredParent?.uuid === group?.uuid) {
|
|
isHandlesVisible = !shouldHideHover
|
|
}
|
|
|
|
if (arrowGroup) {
|
|
arrowGroup.position.set(to[0], to[1], 0)
|
|
|
|
const dir = new Vector3()
|
|
.subVectors(
|
|
new Vector3(to[0], to[1], 0),
|
|
new Vector3(from[0], from[1], 0)
|
|
)
|
|
.normalize()
|
|
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
|
|
arrowGroup.scale.set(scale, scale, scale)
|
|
arrowGroup.visible = isHandlesVisible
|
|
}
|
|
|
|
const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
|
if (extraSegmentGroup) {
|
|
const offsetFromBase = new Vector2(to[0] - from[0], to[1] - from[1])
|
|
.normalize()
|
|
.multiplyScalar(EXTRA_SEGMENT_OFFSET_PX * scale)
|
|
extraSegmentGroup.position.set(
|
|
from[0] + offsetFromBase.x,
|
|
from[1] + offsetFromBase.y,
|
|
0
|
|
)
|
|
extraSegmentGroup.scale.set(scale, scale, scale)
|
|
extraSegmentGroup.visible = isHandlesVisible
|
|
}
|
|
|
|
const straightSegmentBody = group.children.find(
|
|
(child) => child.userData.type === STRAIGHT_SEGMENT_BODY
|
|
) as Mesh
|
|
if (straightSegmentBody) {
|
|
const line = new LineCurve3(
|
|
new Vector3(from[0], from[1], 0),
|
|
new Vector3(to[0], to[1], 0)
|
|
)
|
|
straightSegmentBody.geometry = new ExtrudeGeometry(shape, {
|
|
steps: 2,
|
|
bevelEnabled: false,
|
|
extrudePath: line,
|
|
})
|
|
}
|
|
const straightSegmentBodyDashed = group.children.find(
|
|
(child) => child.userData.type === STRAIGHT_SEGMENT_DASH
|
|
) as Mesh
|
|
if (straightSegmentBodyDashed) {
|
|
straightSegmentBodyDashed.geometry = dashedStraight(
|
|
from,
|
|
to,
|
|
shape,
|
|
scale
|
|
)
|
|
}
|
|
}
|
|
async animateAfterSketch() {
|
|
// if (isReducedMotion()) {
|
|
// sceneInfra.camControls.usePerspectiveCamera()
|
|
// return
|
|
// }
|
|
await sceneInfra.camControls.animateToPerspective()
|
|
}
|
|
removeSketchGrid() {
|
|
if (this.axisGroup) this.scene.remove(this.axisGroup)
|
|
}
|
|
private _tearDownSketch(
|
|
callDepth = 0,
|
|
resolve: (val: unknown) => void,
|
|
reject: () => void,
|
|
{ removeAxis = true }: { removeAxis?: boolean }
|
|
) {
|
|
if (this.intersectionPlane) this.scene.remove(this.intersectionPlane)
|
|
if (this.axisGroup && removeAxis) this.scene.remove(this.axisGroup)
|
|
const sketchSegments = this.scene.children.find(
|
|
({ userData }) => userData?.type === SKETCH_GROUP_SEGMENTS
|
|
)
|
|
let shouldResolve = false
|
|
if (sketchSegments) {
|
|
this.scene.remove(sketchSegments)
|
|
shouldResolve = true
|
|
} else {
|
|
const delay = 100
|
|
const maxTimeRetries = 3000 // 3 seconds
|
|
const maxCalls = maxTimeRetries / delay
|
|
if (callDepth < maxCalls) {
|
|
setTimeout(() => {
|
|
this._tearDownSketch(callDepth + 1, resolve, reject, { removeAxis })
|
|
}, delay)
|
|
} else {
|
|
reject()
|
|
}
|
|
}
|
|
sceneInfra.camControls.enableRotate = true
|
|
this.activeSegments = {}
|
|
// maybe should reset onMove etc handlers
|
|
if (shouldResolve) resolve(true)
|
|
}
|
|
async tearDownSketch({
|
|
removeAxis = true,
|
|
}: {
|
|
removeAxis?: boolean
|
|
} = {}) {
|
|
// I think promisifying this is mostly a side effect of not having
|
|
// "setupSketch" correctly capture a promise when it's done
|
|
// so we're effectively waiting for to be finished setting up the scene just to tear it down
|
|
// TODO is to fix that
|
|
return new Promise((resolve, reject) => {
|
|
this._tearDownSketch(0, resolve, reject, { removeAxis })
|
|
})
|
|
}
|
|
setupDefaultPlaneHover() {
|
|
sceneInfra.setCallbacks({
|
|
onMouseEnter: ({ selected }) => {
|
|
if (!(selected instanceof Mesh && selected.parent)) return
|
|
if (selected.parent.userData.type !== DEFAULT_PLANES) return
|
|
const type: DefaultPlane = selected.userData.type
|
|
selected.material.color = defaultPlaneColor(type, 0.5, 1)
|
|
},
|
|
onMouseLeave: ({ selected }) => {
|
|
if (!(selected instanceof Mesh && selected.parent)) return
|
|
if (selected.parent.userData.type !== DEFAULT_PLANES) return
|
|
const type: DefaultPlane = selected.userData.type
|
|
selected.material.color = defaultPlaneColor(type)
|
|
},
|
|
onClick: async (args) => {
|
|
const checkExtrudeFaceClick = async (): Promise<
|
|
['face' | 'plane' | 'other', string]
|
|
> => {
|
|
const { streamDimensions } = useStore.getState()
|
|
const { entity_id } = await sendSelectEventToEngine(
|
|
args?.mouseEvent,
|
|
document.getElementById('video-stream') as HTMLVideoElement,
|
|
streamDimensions
|
|
)
|
|
if (!entity_id) return ['other', '']
|
|
if (
|
|
engineCommandManager.defaultPlanes?.xy === entity_id ||
|
|
engineCommandManager.defaultPlanes?.xz === entity_id ||
|
|
engineCommandManager.defaultPlanes?.yz === entity_id
|
|
) {
|
|
return ['plane', entity_id]
|
|
}
|
|
const artifact = this.engineCommandManager.artifactMap[entity_id]
|
|
if (artifact?.commandType !== 'solid3d_get_extrusion_face_info')
|
|
return ['other', entity_id]
|
|
|
|
const faceInfo = await getFaceDetails(entity_id)
|
|
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
|
|
return ['other', entity_id]
|
|
const { z_axis, y_axis, origin } = faceInfo
|
|
const pathToNode = getNodePathFromSourceRange(
|
|
kclManager.ast,
|
|
artifact.range
|
|
)
|
|
|
|
sceneInfra.modelingSend({
|
|
type: 'Select default plane',
|
|
data: {
|
|
type: 'extrudeFace',
|
|
zAxis: [z_axis.x, z_axis.y, z_axis.z],
|
|
yAxis: [y_axis.x, y_axis.y, y_axis.z],
|
|
position: [origin.x, origin.y, origin.z].map(
|
|
(num) => num / sceneInfra._baseUnitMultiplier
|
|
) as [number, number, number],
|
|
extrudeSegmentPathToNode: pathToNode,
|
|
cap:
|
|
artifact?.additionalData?.type === 'cap'
|
|
? artifact.additionalData.info
|
|
: 'none',
|
|
faceId: entity_id,
|
|
},
|
|
})
|
|
return ['face', entity_id]
|
|
}
|
|
|
|
const faceResult = await checkExtrudeFaceClick()
|
|
console.log('faceResult', faceResult)
|
|
if (faceResult[0] === 'face') return
|
|
|
|
if (!args || !args.intersects?.[0]) return
|
|
if (args.mouseEvent.which !== 1) return
|
|
const { intersects } = args
|
|
const type = intersects?.[0].object.name || ''
|
|
const posNorm = Number(intersects?.[0]?.normal?.z) > 0
|
|
let planeString: DefaultPlaneStr = posNorm ? 'XY' : '-XY'
|
|
let zAxis: [number, number, number] = posNorm ? [0, 0, 1] : [0, 0, -1]
|
|
let yAxis: [number, number, number] = [0, 1, 0]
|
|
if (type === YZ_PLANE) {
|
|
planeString = posNorm ? 'YZ' : '-YZ'
|
|
zAxis = posNorm ? [1, 0, 0] : [-1, 0, 0]
|
|
yAxis = [0, 0, 1]
|
|
} else if (type === XZ_PLANE) {
|
|
planeString = posNorm ? 'XZ' : '-XZ'
|
|
zAxis = posNorm ? [0, 1, 0] : [0, -1, 0]
|
|
yAxis = [0, 0, 1]
|
|
}
|
|
sceneInfra.modelingSend({
|
|
type: 'Select default plane',
|
|
data: {
|
|
type: 'defaultPlane',
|
|
plane: planeString,
|
|
zAxis,
|
|
yAxis,
|
|
planeId: faceResult[1],
|
|
},
|
|
})
|
|
},
|
|
})
|
|
}
|
|
mouseEnterLeaveCallbacks() {
|
|
return {
|
|
onMouseEnter: ({ selected, dragSelected }: OnMouseEnterLeaveArgs) => {
|
|
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
|
|
const obj = selected as Mesh
|
|
const mat = obj.material as MeshBasicMaterial
|
|
mat.color.set(obj.userData.baseColor)
|
|
mat.color.offsetHSL(0, 0, 0.5)
|
|
}
|
|
const parent = getParentGroup(selected, [
|
|
STRAIGHT_SEGMENT,
|
|
TANGENTIAL_ARC_TO_SEGMENT,
|
|
PROFILE_START,
|
|
])
|
|
if (parent?.userData?.pathToNode) {
|
|
const updatedAst = parse(recast(kclManager.ast))
|
|
const node = getNodeFromPath<CallExpression>(
|
|
updatedAst,
|
|
parent.userData.pathToNode,
|
|
'CallExpression'
|
|
).node
|
|
editorManager.setHighlightRange([node.start, node.end])
|
|
const yellow = 0xffff00
|
|
colorSegment(selected, yellow)
|
|
const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
|
if (extraSegmentGroup) {
|
|
extraSegmentGroup.traverse((child) => {
|
|
if (child instanceof Points || child instanceof Mesh) {
|
|
child.material.opacity = dragSelected ? 0 : 1
|
|
}
|
|
})
|
|
}
|
|
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
|
|
|
const factor =
|
|
(sceneInfra.camControls.camera instanceof OrthographicCamera
|
|
? orthoFactor
|
|
: perspScale(sceneInfra.camControls.camera, parent)) /
|
|
sceneInfra._baseUnitMultiplier
|
|
if (parent.name === STRAIGHT_SEGMENT) {
|
|
this.updateStraightSegment({
|
|
from: parent.userData.from,
|
|
to: parent.userData.to,
|
|
group: parent,
|
|
scale: factor,
|
|
})
|
|
} else if (parent.name === TANGENTIAL_ARC_TO_SEGMENT) {
|
|
this.updateTangentialArcToSegment({
|
|
prevSegment: parent.userData.prevSegment,
|
|
from: parent.userData.from,
|
|
to: parent.userData.to,
|
|
group: parent,
|
|
scale: factor,
|
|
})
|
|
}
|
|
return
|
|
}
|
|
editorManager.setHighlightRange([0, 0])
|
|
},
|
|
onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => {
|
|
editorManager.setHighlightRange([0, 0])
|
|
const parent = getParentGroup(selected, [
|
|
STRAIGHT_SEGMENT,
|
|
TANGENTIAL_ARC_TO_SEGMENT,
|
|
PROFILE_START,
|
|
])
|
|
if (parent) {
|
|
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
|
|
|
const factor =
|
|
(sceneInfra.camControls.camera instanceof OrthographicCamera
|
|
? orthoFactor
|
|
: perspScale(sceneInfra.camControls.camera, parent)) /
|
|
sceneInfra._baseUnitMultiplier
|
|
if (parent.name === STRAIGHT_SEGMENT) {
|
|
this.updateStraightSegment({
|
|
from: parent.userData.from,
|
|
to: parent.userData.to,
|
|
group: parent,
|
|
scale: factor,
|
|
})
|
|
} else if (parent.name === TANGENTIAL_ARC_TO_SEGMENT) {
|
|
this.updateTangentialArcToSegment({
|
|
prevSegment: parent.userData.prevSegment,
|
|
from: parent.userData.from,
|
|
to: parent.userData.to,
|
|
group: parent,
|
|
scale: factor,
|
|
})
|
|
}
|
|
}
|
|
const isSelected = parent?.userData?.isSelected
|
|
colorSegment(
|
|
selected,
|
|
isSelected ? 0x0000ff : parent?.userData?.baseColor || 0xffffff
|
|
)
|
|
const extraSegmentGroup = parent?.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
|
if (extraSegmentGroup) {
|
|
extraSegmentGroup.traverse((child) => {
|
|
if (child instanceof Points || child instanceof Mesh) {
|
|
child.material.opacity = 0
|
|
}
|
|
})
|
|
}
|
|
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
|
|
const obj = selected as Mesh
|
|
const mat = obj.material as MeshBasicMaterial
|
|
mat.color.set(obj.userData.baseColor)
|
|
if (obj.userData.isSelected) mat.color.offsetHSL(0, 0, 0.2)
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
export type DefaultPlaneStr = 'XY' | 'XZ' | 'YZ' | '-XY' | '-XZ' | '-YZ'
|
|
|
|
// calculations/pure-functions/easy to test so no excuse not to
|
|
|
|
function prepareTruncatedMemoryAndAst(
|
|
sketchPathToNode: PathToNode,
|
|
ast: Program,
|
|
programMemory: ProgramMemory,
|
|
draftSegment?: DraftSegment
|
|
): {
|
|
truncatedAst: Program
|
|
programMemoryOverride: ProgramMemory
|
|
variableDeclarationName: string
|
|
} {
|
|
const bodyIndex = Number(sketchPathToNode?.[1]?.[0]) || 0
|
|
const _ast = JSON.parse(JSON.stringify(ast))
|
|
|
|
const variableDeclarationName =
|
|
getNodeFromPath<VariableDeclaration>(
|
|
_ast,
|
|
sketchPathToNode || [],
|
|
'VariableDeclaration'
|
|
)?.node?.declarations?.[0]?.id?.name || ''
|
|
const lastSeg = (
|
|
programMemory.root[variableDeclarationName] as SketchGroup
|
|
).value.slice(-1)[0]
|
|
if (draftSegment) {
|
|
// truncatedAst needs to setup with another segment at the end
|
|
let newSegment
|
|
if (draftSegment === 'line') {
|
|
newSegment = createCallExpressionStdLib('line', [
|
|
createArrayExpression([createLiteral(0), createLiteral(0)]),
|
|
createPipeSubstitution(),
|
|
])
|
|
} else {
|
|
newSegment = createCallExpressionStdLib('tangentialArcTo', [
|
|
createArrayExpression([
|
|
createLiteral(lastSeg.to[0]),
|
|
createLiteral(lastSeg.to[1]),
|
|
]),
|
|
createPipeSubstitution(),
|
|
])
|
|
}
|
|
;(
|
|
(_ast.body[bodyIndex] as VariableDeclaration).declarations[0]
|
|
.init as PipeExpression
|
|
).body.push(newSegment)
|
|
// update source ranges to section we just added.
|
|
// hacks like this wouldn't be needed if the AST put pathToNode info in memory/sketchGroup segments
|
|
const updatedSrcRangeAst = parse(recast(_ast)) // get source ranges correct since unfortunately we still rely on them
|
|
const lastPipeItem = (
|
|
(updatedSrcRangeAst.body[bodyIndex] as VariableDeclaration)
|
|
.declarations[0].init as PipeExpression
|
|
).body.slice(-1)[0]
|
|
|
|
;(
|
|
(_ast.body[bodyIndex] as VariableDeclaration).declarations[0]
|
|
.init as PipeExpression
|
|
).body.slice(-1)[0].start = lastPipeItem.start
|
|
|
|
_ast.end = lastPipeItem.end
|
|
const varDec = _ast.body[bodyIndex] as VariableDeclaration
|
|
varDec.end = lastPipeItem.end
|
|
const declarator = varDec.declarations[0]
|
|
declarator.end = lastPipeItem.end
|
|
const init = declarator.init as PipeExpression
|
|
init.end = lastPipeItem.end
|
|
init.body.slice(-1)[0].end = lastPipeItem.end
|
|
}
|
|
const truncatedAst: Program = {
|
|
..._ast,
|
|
body: [JSON.parse(JSON.stringify(_ast.body[bodyIndex]))],
|
|
}
|
|
const programMemoryOverride: ProgramMemory = programMemoryInit()
|
|
for (let i = 0; i < bodyIndex; i++) {
|
|
const node = _ast.body[i]
|
|
if (node.type !== 'VariableDeclaration') {
|
|
continue
|
|
}
|
|
const name = node.declarations[0].id.name
|
|
// const memoryItem = kclManager.programMemory.root[name]
|
|
const memoryItem = programMemory.root[name]
|
|
if (!memoryItem) {
|
|
continue
|
|
}
|
|
programMemoryOverride.root[name] = JSON.parse(JSON.stringify(memoryItem))
|
|
}
|
|
return {
|
|
truncatedAst,
|
|
programMemoryOverride,
|
|
variableDeclarationName,
|
|
}
|
|
}
|
|
|
|
export function getParentGroup(
|
|
object: any,
|
|
stopAt: string[] = [STRAIGHT_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT]
|
|
): Group | null {
|
|
if (stopAt.includes(object?.userData?.type)) {
|
|
return object
|
|
} else if (object?.parent) {
|
|
return getParentGroup(object.parent, stopAt)
|
|
}
|
|
return null
|
|
}
|
|
|
|
export function sketchGroupFromPathToNode({
|
|
pathToNode,
|
|
ast,
|
|
programMemory,
|
|
}: {
|
|
pathToNode: PathToNode
|
|
ast: Program
|
|
programMemory: ProgramMemory
|
|
}): SketchGroup {
|
|
const varDec = getNodeFromPath<VariableDeclarator>(
|
|
kclManager.ast,
|
|
pathToNode,
|
|
'VariableDeclarator'
|
|
).node
|
|
return programMemory.root[varDec?.id?.name || ''] as SketchGroup
|
|
}
|
|
|
|
function colorSegment(object: any, color: number) {
|
|
const segmentHead = getParentGroup(object, [ARROWHEAD, PROFILE_START])
|
|
if (segmentHead) {
|
|
segmentHead.traverse((child) => {
|
|
if (child instanceof Mesh) {
|
|
child.material.color.set(color)
|
|
}
|
|
})
|
|
return
|
|
}
|
|
const straightSegmentBody = getParentGroup(object, [
|
|
STRAIGHT_SEGMENT,
|
|
TANGENTIAL_ARC_TO_SEGMENT,
|
|
])
|
|
if (straightSegmentBody) {
|
|
straightSegmentBody.traverse((child) => {
|
|
if (child instanceof Mesh && !child.userData.ignoreColorChange) {
|
|
child.material.color.set(color)
|
|
}
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
export function getSketchQuaternion(
|
|
sketchPathToNode: PathToNode,
|
|
sketchNormalBackUp: [number, number, number] | null
|
|
): Quaternion {
|
|
const sketchGroup = sketchGroupFromPathToNode({
|
|
pathToNode: sketchPathToNode,
|
|
ast: kclManager.ast,
|
|
programMemory: kclManager.programMemory,
|
|
})
|
|
const zAxis = sketchGroup?.zAxis || sketchNormalBackUp
|
|
return getQuaternionFromZAxis(massageFormats(zAxis))
|
|
}
|
|
export async function getSketchOrientationDetails(
|
|
sketchPathToNode: PathToNode
|
|
): Promise<{
|
|
quat: Quaternion
|
|
sketchDetails: SketchDetails & { faceId?: string }
|
|
}> {
|
|
const sketchGroup = sketchGroupFromPathToNode({
|
|
pathToNode: sketchPathToNode,
|
|
ast: kclManager.ast,
|
|
programMemory: kclManager.programMemory,
|
|
})
|
|
if (sketchGroup.on.type === 'plane') {
|
|
const zAxis = sketchGroup?.zAxis
|
|
return {
|
|
quat: getQuaternionFromZAxis(massageFormats(zAxis)),
|
|
sketchDetails: {
|
|
sketchPathToNode,
|
|
zAxis: [zAxis.x, zAxis.y, zAxis.z],
|
|
yAxis: [sketchGroup.yAxis.x, sketchGroup.yAxis.y, sketchGroup.yAxis.z],
|
|
origin: [0, 0, 0],
|
|
faceId: sketchGroup.on.id,
|
|
},
|
|
}
|
|
}
|
|
if (sketchGroup.on.type === 'face') {
|
|
const faceInfo = await getFaceDetails(sketchGroup.on.faceId)
|
|
|
|
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
|
|
throw new Error('faceInfo')
|
|
const { z_axis, y_axis, origin } = faceInfo
|
|
const quaternion = quaternionFromUpNForward(
|
|
new Vector3(y_axis.x, y_axis.y, y_axis.z),
|
|
new Vector3(z_axis.x, z_axis.y, z_axis.z)
|
|
)
|
|
return {
|
|
quat: quaternion,
|
|
sketchDetails: {
|
|
sketchPathToNode,
|
|
zAxis: [z_axis.x, z_axis.y, z_axis.z],
|
|
yAxis: [y_axis.x, y_axis.y, y_axis.z],
|
|
origin: [origin.x, origin.y, origin.z],
|
|
faceId: sketchGroup.on.faceId,
|
|
},
|
|
}
|
|
}
|
|
throw new Error(
|
|
'sketchGroup.on.type not recognized, has a new type been added?'
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Retrieves orientation details for a given entity representing a face (brep face or default plane).
|
|
* This function asynchronously fetches and returns the origin, x-axis, y-axis, and z-axis details
|
|
* for a specified entity ID. It is primarily used to obtain the orientation of a face in the scene,
|
|
* which is essential for calculating the correct positioning and alignment of the client side sketch.
|
|
*
|
|
* @param entityId - The ID of the entity for which orientation details are being fetched.
|
|
* @returns A promise that resolves with the orientation details of the face.
|
|
*/
|
|
async function getFaceDetails(
|
|
entityId: string
|
|
): Promise<Models['FaceIsPlanar_type']> {
|
|
// TODO mode engine connection to allow batching returns and batch the following
|
|
await engineCommandManager.sendSceneCommand({
|
|
type: 'modeling_cmd_req',
|
|
cmd_id: uuidv4(),
|
|
cmd: {
|
|
type: 'enable_sketch_mode',
|
|
adjust_camera: false,
|
|
animated: false,
|
|
ortho: false,
|
|
entity_id: entityId,
|
|
},
|
|
})
|
|
// TODO change typing to get_sketch_mode_plane once lib is updated
|
|
const faceInfo: Models['FaceIsPlanar_type'] = (
|
|
await engineCommandManager.sendSceneCommand({
|
|
type: 'modeling_cmd_req',
|
|
cmd_id: uuidv4(),
|
|
cmd: { type: 'get_sketch_mode_plane' },
|
|
})
|
|
)?.data?.data
|
|
await engineCommandManager.sendSceneCommand({
|
|
type: 'modeling_cmd_req',
|
|
cmd_id: uuidv4(),
|
|
cmd: { type: 'sketch_mode_disable' },
|
|
})
|
|
return faceInfo
|
|
}
|
|
|
|
export function getQuaternionFromZAxis(zAxis: Vector3): Quaternion {
|
|
const dummyCam = new PerspectiveCamera()
|
|
dummyCam.up.set(0, 0, 1)
|
|
dummyCam.position.copy(zAxis)
|
|
dummyCam.lookAt(0, 0, 0)
|
|
dummyCam.updateMatrix()
|
|
const quaternion = dummyCam.quaternion.clone()
|
|
|
|
const isVert = isQuaternionVertical(quaternion)
|
|
|
|
// because vertical quaternions are a gimbal lock, for the orbit controls
|
|
// it's best to set them explicitly to the vertical position with a known good camera up
|
|
if (isVert && zAxis.z < 0) {
|
|
quaternion.set(0, 1, 0, 0)
|
|
} else if (isVert) {
|
|
quaternion.set(0, 0, 0, 1)
|
|
}
|
|
return quaternion
|
|
}
|
|
|
|
function massageFormats(a: any): Vector3 {
|
|
return Array.isArray(a)
|
|
? new Vector3(a[0], a[1], a[2])
|
|
: new Vector3(a.x, a.y, a.z)
|
|
}
|