Files
modeling-app/src/clientSideScene/sceneEntities.ts
max 4585671a5d Refactor getNodePathFromSourceRange (#5139)
* Refactor getNodePathFromSourceRange

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-27 08:24:28 -05:00

2734 lines
88 KiB
TypeScript

import {
BoxGeometry,
Color,
DoubleSide,
Group,
Intersection,
Line,
LineDashedMaterial,
BufferGeometry,
Mesh,
MeshBasicMaterial,
Object3D,
Object3DEventMap,
OrthographicCamera,
PerspectiveCamera,
PlaneGeometry,
Points,
Quaternion,
Scene,
SphereGeometry,
Vector2,
Vector3,
} from 'three'
import {
ANGLE_SNAP_THRESHOLD_DEGREES,
ARROWHEAD,
AXIS_GROUP,
DRAFT_POINT,
DRAFT_POINT_GROUP,
getSceneScale,
INTERSECTION_PLANE_LAYER,
OnClickCallbackArgs,
OnMouseEnterLeaveArgs,
RAYCASTABLE_PLANE,
SKETCH_GROUP_SEGMENTS,
SKETCH_LAYER,
X_AXIS,
Y_AXIS,
CIRCLE_3_POINT_DRAFT_POINT,
CIRCLE_3_POINT_DRAFT_CIRCLE,
} from './sceneInfra'
import { isQuaternionVertical, quaternionFromUpNForward } from './helpers'
import {
CallExpression,
parse,
Path,
PathToNode,
PipeExpression,
Program,
ProgramMemory,
recast,
Sketch,
Solid,
VariableDeclaration,
VariableDeclarator,
sketchFromKclValue,
sketchFromKclValueOptional,
defaultSourceRange,
sourceRangeFromRust,
resultIsOk,
SourceRange,
topLevelRange,
} from 'lang/wasm'
import { calculate_circle_from_3_points } from '../wasm-lib/pkg/wasm_lib'
import {
engineCommandManager,
kclManager,
sceneInfra,
codeManager,
editorManager,
} from 'lib/singletons'
import { getNodeFromPath } from 'lang/queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { executeAst, ToolTip } from 'lang/langHelpers'
import {
createProfileStartHandle,
createCircleGeometry,
SegmentUtils,
segmentUtils,
} from './segments'
import {
addCallExpressionsToPipe,
addCloseToPipe,
addNewSketchLn,
changeSketchArguments,
updateStartProfileAtArgs,
} from 'lang/std/sketch'
import { isArray, isOverlap, roundOff } from 'lib/utils'
import {
addStartProfileAt,
createArrayExpression,
createCallExpressionStdLib,
createLiteral,
createObjectExpression,
createPipeExpression,
createPipeSubstitution,
findUniqueName,
} from 'lang/modifyAst'
import { Selections, getEventForSegmentSelection } from 'lib/selections'
import { createGridHelper, orthoScale, perspScale } from './helpers'
import { Models } from '@kittycad/lib'
import { uuidv4 } from 'lib/utils'
import { SegmentOverlayPayload, SketchDetails } from 'machines/modelingMachine'
import { EngineCommandManager } from 'lang/std/engineConnection'
import {
getRectangleCallExpressions,
updateRectangleSketch,
updateCenterRectangleSketch,
} from 'lib/rectangleTool'
import { getThemeColorForThreeJs, Themes } from 'lib/theme'
import { err, Reason, reportRejection, trap } from 'lib/trap'
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
import { SegmentInputs } from 'lang/std/stdTypes'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { LabeledArg } from 'wasm-lib/kcl/bindings/LabeledArg'
import { Literal } from 'wasm-lib/kcl/bindings/Literal'
import { radToDeg } from 'three/src/math/MathUtils'
import { getArtifactFromRange, codeRefFromRange } from 'lang/std/artifactGraph'
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 CIRCLE_SEGMENT = 'circle-segment'
export const CIRCLE_SEGMENT_BODY = 'circle-segment-body'
export const CIRCLE_SEGMENT_DASH = 'circle-segment-body-dashed'
export const CIRCLE_CENTER_HANDLE = 'circle-center-handle'
export const SEGMENT_WIDTH_PX = 1.6
export const HIDE_SEGMENT_LENGTH = 75 // in pixels
export const HIDE_HOVER_SEGMENT_LENGTH = 60 // in pixels
export const SEGMENT_BODIES = [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
CIRCLE_SEGMENT,
]
export const SEGMENT_BODIES_PLUS_PROFILE_START = [
...SEGMENT_BODIES,
PROFILE_START,
]
type Vec3Array = [number, number, number]
// 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 = ProgramMemory.empty()
activeSegments: { [key: string]: Group } = {}
intersectionPlane: Mesh | null = null
axisGroup: Group | null = null
draftPointGroups: Group[] = []
currentSketchQuaternion: Quaternion | null = null
constructor(engineCommandManager: EngineCommandManager) {
this.engineCommandManager = engineCommandManager
this.scene = sceneInfra?.scene
sceneInfra?.camControls.subscribeToCamChange(this.onCamChange)
window.addEventListener('resize', this.onWindowResize)
this.createIntersectionPlane()
}
onWindowResize = () => {
this.onCamChange()
}
onCamChange = () => {
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
const callbacks: (() => SegmentOverlayPayload | null)[] = []
Object.values(this.activeSegments).forEach((segment, index) => {
const factor =
(sceneInfra.camControls.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(sceneInfra.camControls.camera, segment)) /
sceneInfra._baseUnitMultiplier
let input: SegmentInputs = {
type: 'straight-segment',
from: segment.userData.from,
to: segment.userData.to,
}
let update: SegmentUtils['update'] | null = null
if (
segment.userData.from &&
segment.userData.to &&
segment.userData.type === STRAIGHT_SEGMENT
) {
update = segmentUtils.straight.update
}
if (
segment.userData.from &&
segment.userData.to &&
segment.userData.prevSegment &&
segment.userData.type === TANGENTIAL_ARC_TO_SEGMENT
) {
update = segmentUtils.tangentialArcTo.update
}
if (
segment.userData.from &&
segment.userData.center &&
segment.userData.radius &&
segment.userData.type === CIRCLE_SEGMENT
) {
update = segmentUtils.circle.update
input = {
type: 'arc-segment',
from: segment.userData.from,
center: segment.userData.center,
radius: segment.userData.radius,
}
}
const callBack = update?.({
prevSegment: segment.userData.prevSegment,
input,
group: segment,
scale: factor,
sceneInfra,
})
callBack && !err(callBack) && callbacks.push(callBack)
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)
}
sceneInfra.overlayCallbacks(callbacks)
}
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)
}
getDraftPoint() {
return this.scene.getObjectByName(DRAFT_POINT)
}
createDraftPoint({ point, group }: { point: Vector2; group: Group }) {
const dummy = new Mesh()
dummy.position.set(0, 0, 0)
const scale = sceneInfra.getClientSceneScaleFactor(dummy)
const draftPoint = createProfileStartHandle({
isDraft: true,
from: [point.x, point.y],
scale,
theme: sceneInfra._theme,
})
draftPoint.layers.set(SKETCH_LAYER)
group.add(draftPoint)
}
removeDraftPoint() {
const draftPoint = this.getDraftPoint()
if (draftPoint) draftPoint.removeFromParent()
}
setupNoPointsListener({
sketchDetails,
afterClick,
}: {
sketchDetails: SketchDetails
afterClick: (args: OnClickCallbackArgs) => void
}) {
// TODO: Consolidate shared logic between this and setupSketch
// Which should just fire when the sketch mode is entered,
// instead of in these two separate XState states.
const draftPointGroup = new Group()
this.draftPointGroups.push(draftPointGroup)
draftPointGroup.name = DRAFT_POINT_GROUP
sketchDetails.origin &&
draftPointGroup.position.set(...sketchDetails.origin)
if (!(sketchDetails.yAxis && sketchDetails)) {
console.error('No sketch quaternion or sketch details found')
return
}
this.currentSketchQuaternion = quaternionFromUpNForward(
new Vector3(...sketchDetails.yAxis),
new Vector3(...sketchDetails.zAxis)
)
draftPointGroup.setRotationFromQuaternion(this.currentSketchQuaternion)
this.scene.add(draftPointGroup)
const quaternion = quaternionFromUpNForward(
new Vector3(...sketchDetails.yAxis),
new Vector3(...sketchDetails.zAxis)
)
// Position the click raycast plane
this.intersectionPlane!.setRotationFromQuaternion(quaternion)
this.intersectionPlane!.position.copy(
new Vector3(...(sketchDetails?.origin || [0, 0, 0]))
)
sceneInfra.setCallbacks({
onMove: (args) => {
if (!args.intersects.length) return
const axisIntersection = args.intersects.find(
(sceneObject) =>
sceneObject.object.name === X_AXIS ||
sceneObject.object.name === Y_AXIS
)
if (!axisIntersection) return
const { intersectionPoint } = args
// We're hovering over an axis, so we should show a draft point
const snappedPoint = intersectionPoint.twoD.clone()
if (axisIntersection.object.name === X_AXIS) {
snappedPoint.setComponent(1, 0)
} else {
snappedPoint.setComponent(0, 0)
}
// Either create a new one or update the existing one
const draftPoint = this.getDraftPoint()
if (!draftPoint) {
this.createDraftPoint({
point: snappedPoint,
group: draftPointGroup,
})
} else {
// Ignore if there are huge jumps in the mouse position,
// that is likely a strange behavior
if (
draftPoint.position.distanceTo(
new Vector3(snappedPoint.x, snappedPoint.y, 0)
) > 100
) {
return
}
draftPoint.position.set(snappedPoint.x, snappedPoint.y, 0)
}
},
onMouseLeave: () => {
this.removeDraftPoint()
},
onClick: async (args) => {
this.removeDraftPoint()
if (!args) return
// If there is a valid camera interaction that matches, do that instead
const interaction = sceneInfra.camControls.getInteractionType(
args.mouseEvent
)
if (interaction !== 'none') return
if (args.mouseEvent.which !== 1) return
const { intersectionPoint } = args
if (!intersectionPoint?.twoD || !sketchDetails?.sketchPathToNode) return
// Snap to either or both axes
// if the click intersects their meshes
const yAxisIntersection = args.intersects.find(
(sceneObject) => sceneObject.object.name === Y_AXIS
)
const xAxisIntersection = args.intersects.find(
(sceneObject) => sceneObject.object.name === X_AXIS
)
const snappedClickPoint = {
x: yAxisIntersection ? 0 : intersectionPoint.twoD.x,
y: xAxisIntersection ? 0 : intersectionPoint.twoD.y,
}
const addStartProfileAtRes = addStartProfileAt(
kclManager.ast,
sketchDetails.sketchPathToNode,
[snappedClickPoint.x, snappedClickPoint.y]
)
if (trap(addStartProfileAtRes)) return
const { modifiedAst } = addStartProfileAtRes
await kclManager.updateAst(modifiedAst, false)
this.scene.remove(draftPointGroup)
// Now perform the caller-specified action
afterClick(args)
},
})
}
async setupSketch({
sketchPathToNode,
forward,
up,
position,
maybeModdedAst,
draftExpressionsIndices,
selectionRanges,
}: {
sketchPathToNode: PathToNode
maybeModdedAst: Node<Program>
draftExpressionsIndices?: { start: number; end: number }
forward: [number, number, number]
up: [number, number, number]
position?: [number, number, number]
selectionRanges?: Selections
}): Promise<{
truncatedAst: Node<Program>
programMemoryOverride: ProgramMemory
sketch: Sketch
variableDeclarationName: string
}> {
const prepared = this.prepareTruncatedMemoryAndAst(
sketchPathToNode || [],
maybeModdedAst
)
if (err(prepared)) return Promise.reject(prepared)
const { truncatedAst, programMemoryOverride, variableDeclarationName } =
prepared
const { execState } = await executeAst({
ast: truncatedAst,
engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride,
})
const programMemory = execState.memory
const sketch = sketchFromPathToNode({
pathToNode: sketchPathToNode,
ast: maybeModdedAst,
programMemory,
})
if (err(sketch)) return Promise.reject(sketch)
if (!sketch) return Promise.reject('sketch not found')
if (!isArray(sketch?.paths))
return {
truncatedAst,
programMemoryOverride,
sketch,
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()
// TODO: When we actually have sketch positions and rotations we can use them here.
dummy.position.set(0, 0, 0)
const scale = sceneInfra.getClientSceneScaleFactor(dummy)
const segPathToNode = getNodePathFromSourceRange(
maybeModdedAst,
sourceRangeFromRust(sketch.start.__geoMeta.sourceRange)
)
if (sketch?.paths?.[0]?.type !== 'Circle') {
const _profileStart = createProfileStartHandle({
from: sketch.start.from,
id: sketch.start.__geoMeta.id,
pathToNode: segPathToNode,
scale,
theme: sceneInfra._theme,
isDraft: false,
})
_profileStart.layers.set(SKETCH_LAYER)
_profileStart.traverse((child) => {
child.layers.set(SKETCH_LAYER)
})
group.add(_profileStart)
this.activeSegments[JSON.stringify(segPathToNode)] = _profileStart
}
const callbacks: (() => SegmentOverlayPayload | null)[] = []
sketch.paths.forEach((segment, index) => {
let segPathToNode = getNodePathFromSourceRange(
maybeModdedAst,
sourceRangeFromRust(segment.__geoMeta.sourceRange)
)
if (
draftExpressionsIndices &&
(sketch.paths[index - 1] || sketch.start)
) {
const previousSegment = sketch.paths[index - 1] || sketch.start
const previousSegmentPathToNode = getNodePathFromSourceRange(
maybeModdedAst,
sourceRangeFromRust(previousSegment.__geoMeta.sourceRange)
)
const bodyIndex = previousSegmentPathToNode[1][0]
segPathToNode = getNodePathFromSourceRange(
truncatedAst,
sourceRangeFromRust(segment.__geoMeta.sourceRange)
)
segPathToNode[1][0] = bodyIndex
}
const isDraftSegment =
draftExpressionsIndices &&
index <= draftExpressionsIndices.end &&
index >= draftExpressionsIndices.start
const isSelected = selectionRanges?.graphSelections.some((selection) =>
isOverlap(
selection?.codeRef?.range,
sourceRangeFromRust(segment.__geoMeta.sourceRange)
)
)
let seg: Group
const _node1 = getNodeFromPath<Node<CallExpression>>(
maybeModdedAst,
segPathToNode,
'CallExpression'
)
if (err(_node1)) return
const callExpName = _node1.node?.callee?.name
const initSegment =
segment.type === 'TangentialArcTo'
? segmentUtils.tangentialArcTo.init
: segment.type === 'Circle'
? segmentUtils.circle.init
: segmentUtils.straight.init
const input: SegmentInputs =
segment.type === 'Circle'
? {
type: 'arc-segment',
from: segment.from,
center: segment.center,
radius: segment.radius,
}
: {
type: 'straight-segment',
from: segment.from,
to: segment.to,
}
const startRange = _node1.node.start
const endRange = _node1.node.end
const sourceRange = topLevelRange(startRange, endRange)
const selection: Selections = computeSelectionFromSourceRangeAndAST(
sourceRange,
maybeModdedAst
)
const result = initSegment({
prevSegment: sketch.paths[index - 1],
callExpName,
input,
id: segment.__geoMeta.id,
pathToNode: segPathToNode,
isDraftSegment,
scale,
texture: sceneInfra.extraSegmentTexture,
theme: sceneInfra._theme,
isSelected,
sceneInfra,
selection,
})
if (err(result)) return
const { group: _group, updateOverlaysCallback } = result
seg = _group
callbacks.push(updateOverlaysCallback)
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
sceneInfra.overlayCallbacks(callbacks)
return {
truncatedAst,
programMemoryOverride,
sketch,
variableDeclarationName,
}
}
updateAstAndRejigSketch = async (
sketchPathToNode: PathToNode,
modifiedAst: Node<Program>,
forward: [number, number, number],
up: [number, number, number],
origin: [number, number, number]
) => {
const nextAst = await kclManager.updateAst(modifiedAst, false)
await this.tearDownSketch({ removeAxis: false })
sceneInfra.resetMouseListeners()
await this.setupSketch({
sketchPathToNode,
forward,
up,
position: origin,
maybeModdedAst: nextAst.newAst,
})
this.setupSketchIdleCallbacks({
forward,
up,
position: origin,
pathToNode: sketchPathToNode,
})
return nextAst
}
setupDraftSegment = async (
sketchPathToNode: PathToNode,
forward: [number, number, number],
up: [number, number, number],
origin: [number, number, number],
segmentName: 'line' | 'tangentialArcTo' = 'line',
shouldTearDown = true
) => {
const _ast = structuredClone(kclManager.ast)
const _node1 = getNodeFromPath<VariableDeclaration>(
_ast,
sketchPathToNode || [],
'VariableDeclaration'
)
if (trap(_node1)) return Promise.reject(_node1)
const variableDeclarationName = _node1.node?.declaration.id?.name || ''
const sg = sketchFromKclValue(
kclManager.programMemory.get(variableDeclarationName),
variableDeclarationName
)
if (err(sg)) return Promise.reject(sg)
const lastSeg = sg?.paths?.slice(-1)[0] || sg.start
const index = sg.paths.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,
input: {
type: 'straight-segment',
to: lastSeg.to,
from: lastSeg.to,
},
fnName: segmentName,
pathToNode: sketchPathToNode,
})
if (trap(mod)) return Promise.reject(mod)
const pResult = parse(recast(mod.modifiedAst))
if (trap(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult)
const modifiedAst = pResult.program
const draftExpressionsIndices = { start: index, end: index }
if (shouldTearDown) await this.tearDownSketch({ removeAxis: false })
sceneInfra.resetMouseListeners()
const { truncatedAst, programMemoryOverride, sketch } =
await this.setupSketch({
sketchPathToNode,
forward,
up,
position: origin,
maybeModdedAst: modifiedAst,
draftExpressionsIndices,
})
sceneInfra.setCallbacks({
onClick: async (args) => {
if (!args) return
// If there is a valid camera interaction that matches, do that instead
const interaction = sceneInfra.camControls.getInteractionType(
args.mouseEvent
)
if (interaction !== 'none') return
if (args.mouseEvent.which !== 1) return
const { intersectionPoint } = args
let intersection2d = intersectionPoint?.twoD
const intersectsProfileStart = args.intersects
.map(({ object }) => getParentGroup(object, [PROFILE_START]))
.find((a) => a?.name === PROFILE_START)
let modifiedAst
// Snapping logic for the profile start handle
if (intersectsProfileStart) {
const lastSegment = sketch.paths.slice(-1)[0]
modifiedAst = addCallExpressionsToPipe({
node: kclManager.ast,
programMemory: kclManager.programMemory,
pathToNode: sketchPathToNode,
expressions: [
createCallExpressionStdLib(
lastSegment.type === 'TangentialArcTo'
? 'tangentialArcTo'
: 'lineTo',
[
createArrayExpression([
createCallExpressionStdLib('profileStartX', [
createPipeSubstitution(),
]),
createCallExpressionStdLib('profileStartY', [
createPipeSubstitution(),
]),
]),
createPipeSubstitution(),
]
),
],
})
if (trap(modifiedAst)) return Promise.reject(modifiedAst)
modifiedAst = addCloseToPipe({
node: modifiedAst,
programMemory: kclManager.programMemory,
pathToNode: sketchPathToNode,
})
if (trap(modifiedAst)) return Promise.reject(modifiedAst)
} else if (intersection2d) {
const intersectsYAxis = args.intersects.find(
(sceneObject) => sceneObject.object.name === Y_AXIS
)
const intersectsXAxis = args.intersects.find(
(sceneObject) => sceneObject.object.name === X_AXIS
)
const lastSegment = sketch.paths.slice(-1)[0] || sketch.start
const snappedPoint = {
x: intersectsYAxis ? 0 : intersection2d.x,
y: intersectsXAxis ? 0 : intersection2d.y,
}
// Get the angle between the previous segment (or sketch start)'s end and this one's
const angle = Math.atan2(
snappedPoint.y - lastSegment.to[1],
snappedPoint.x - lastSegment.to[0]
)
const isHorizontal =
radToDeg(Math.abs(angle)) < ANGLE_SNAP_THRESHOLD_DEGREES ||
Math.abs(radToDeg(Math.abs(angle) - Math.PI)) <
ANGLE_SNAP_THRESHOLD_DEGREES
const isVertical =
Math.abs(radToDeg(Math.abs(angle) - Math.PI / 2)) <
ANGLE_SNAP_THRESHOLD_DEGREES
let resolvedFunctionName: ToolTip = 'line'
// This might need to become its own function if we want more
// case-based logic for different segment types
if (lastSegment.type === 'TangentialArcTo') {
resolvedFunctionName = 'tangentialArcTo'
} else if (isHorizontal) {
// If the angle between is 0 or 180 degrees (+/- the snapping angle), make the line an xLine
resolvedFunctionName = 'xLine'
} else if (isVertical) {
// If the angle between is 90 or 270 degrees (+/- the snapping angle), make the line a yLine
resolvedFunctionName = 'yLine'
} else if (snappedPoint.x === 0 || snappedPoint.y === 0) {
// We consider a point placed on axes or origin to be absolute
resolvedFunctionName = 'lineTo'
}
const tmp = addNewSketchLn({
node: kclManager.ast,
programMemory: kclManager.programMemory,
input: {
type: 'straight-segment',
from: [lastSegment.to[0], lastSegment.to[1]],
to: [snappedPoint.x, snappedPoint.y],
},
fnName: resolvedFunctionName,
pathToNode: sketchPathToNode,
})
if (trap(tmp)) return Promise.reject(tmp)
modifiedAst = tmp.modifiedAst
if (trap(modifiedAst)) return Promise.reject(modifiedAst)
} else {
// return early as we didn't modify the ast
return
}
await kclManager.executeAstMock(modifiedAst)
if (intersectsProfileStart) {
sceneInfra.modelingSend({ type: 'CancelSketch' })
} else {
await this.setupDraftSegment(
sketchPathToNode,
forward,
up,
origin,
segmentName
)
}
await codeManager.updateEditorWithAstAndWriteToFile(modifiedAst)
},
onMove: (args) => {
this.onDragSegment({
intersection2d: args.intersectionPoint.twoD,
object: Object.values(this.activeSegments).slice(-1)[0],
intersects: args.intersects,
sketchPathToNode,
draftInfo: {
truncatedAst,
programMemoryOverride,
variableDeclarationName,
},
})
},
})
}
setupDraftRectangle = async (
sketchPathToNode: PathToNode,
forward: [number, number, number],
up: [number, number, number],
sketchOrigin: [number, number, number],
rectangleOrigin: [x: number, y: number]
) => {
let _ast = structuredClone(kclManager.ast)
const _node1 = getNodeFromPath<VariableDeclaration>(
_ast,
sketchPathToNode || [],
'VariableDeclaration'
)
if (trap(_node1)) return Promise.reject(_node1)
const variableDeclarationName = _node1.node?.declaration.id?.name || ''
const startSketchOn = _node1.node?.declaration
const startSketchOnInit = startSketchOn?.init
const tags: [string, string, string] = [
findUniqueName(_ast, 'rectangleSegmentA'),
findUniqueName(_ast, 'rectangleSegmentB'),
findUniqueName(_ast, 'rectangleSegmentC'),
]
startSketchOn.init = createPipeExpression([
startSketchOnInit,
...getRectangleCallExpressions(rectangleOrigin, tags),
])
const pResult = parse(recast(_ast))
if (trap(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult)
_ast = pResult.program
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 = structuredClone(sketchPathToNode)
pathToNodeTwo[1][0] = 0
const _node = getNodeFromPath<VariableDeclaration>(
truncatedAst,
pathToNodeTwo || [],
'VariableDeclaration'
)
if (trap(_node)) return Promise.reject(_node)
const sketchInit = _node.node?.declaration.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 { execState } = await executeAst({
ast: truncatedAst,
engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride,
})
const programMemory = execState.memory
this.sceneProgramMemory = programMemory
const sketch = sketchFromKclValue(
programMemory.get(variableDeclarationName),
variableDeclarationName
)
if (err(sketch)) return Promise.reject(sketch)
const sgPaths = sketch.paths
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
sgPaths.forEach((seg, index) =>
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch)
)
},
onClick: async (args) => {
// If there is a valid camera interaction that matches, do that instead
const interaction = sceneInfra.camControls.getInteractionType(
args.mouseEvent
)
if (interaction !== 'none') return
// 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 _node = getNodeFromPath<VariableDeclaration>(
_ast,
sketchPathToNode || [],
'VariableDeclaration'
)
if (trap(_node)) return
const sketchInit = _node.node?.declaration.init
if (sketchInit.type !== 'PipeExpression') {
return
}
updateRectangleSketch(sketchInit, x, y, tags[0])
const newCode = recast(_ast)
const pResult = parse(newCode)
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(pResult)
_ast = pResult.program
// Update the primary AST and unequip the rectangle tool
await kclManager.executeAstMock(_ast)
sceneInfra.modelingSend({ type: 'Finish rectangle' })
// lee: I had this at the bottom of the function, but it's
// possible sketchFromKclValue "fails" when sketching on a face,
// and this couldn't wouldn't run.
await codeManager.updateEditorWithAstAndWriteToFile(_ast)
const { execState } = await executeAst({
ast: _ast,
engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride,
})
const programMemory = execState.memory
// Prepare to update the THREEjs scene
this.sceneProgramMemory = programMemory
const sketch = sketchFromKclValue(
programMemory.get(variableDeclarationName),
variableDeclarationName
)
if (err(sketch)) return
const sgPaths = sketch.paths
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
// Update the starting segment of the THREEjs scene
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
// Update the rest of the segments of the THREEjs scene
sgPaths.forEach((seg, index) =>
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch)
)
},
})
}
setupDraftCenterRectangle = async (
sketchPathToNode: PathToNode,
forward: [number, number, number],
up: [number, number, number],
sketchOrigin: [number, number, number],
rectangleOrigin: [x: number, y: number]
) => {
let _ast = structuredClone(kclManager.ast)
const _node1 = getNodeFromPath<VariableDeclaration>(
_ast,
sketchPathToNode || [],
'VariableDeclaration'
)
if (trap(_node1)) return Promise.reject(_node1)
// startSketchOn already exists
const variableDeclarationName = _node1.node?.declaration.id?.name || ''
const startSketchOn = _node1.node?.declaration
const startSketchOnInit = startSketchOn?.init
const tags: [string, string, string] = [
findUniqueName(_ast, 'rectangleSegmentA'),
findUniqueName(_ast, 'rectangleSegmentB'),
findUniqueName(_ast, 'rectangleSegmentC'),
]
startSketchOn.init = createPipeExpression([
startSketchOnInit,
...getRectangleCallExpressions(rectangleOrigin, tags),
])
const pResult = parse(recast(_ast))
if (trap(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult)
_ast = pResult.program
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 = structuredClone(sketchPathToNode)
pathToNodeTwo[1][0] = 0
const _node = getNodeFromPath<VariableDeclaration>(
truncatedAst,
pathToNodeTwo || [],
'VariableDeclaration'
)
if (trap(_node)) return Promise.reject(_node)
const sketchInit = _node.node?.declaration.init
const x = (args.intersectionPoint.twoD.x || 0) - rectangleOrigin[0]
const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1]
if (sketchInit.type === 'PipeExpression') {
updateCenterRectangleSketch(
sketchInit,
x,
y,
tags[0],
rectangleOrigin[0],
rectangleOrigin[1]
)
}
const { execState } = await executeAst({
ast: truncatedAst,
engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride,
})
const programMemory = execState.memory
this.sceneProgramMemory = programMemory
const sketch = sketchFromKclValue(
programMemory.get(variableDeclarationName),
variableDeclarationName
)
if (err(sketch)) return Promise.reject(sketch)
const sgPaths = sketch.paths
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
sgPaths.forEach((seg, index) =>
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch)
)
},
onClick: async (args) => {
// If there is a valid camera interaction that matches, do that instead
const interaction = sceneInfra.camControls.getInteractionType(
args.mouseEvent
)
if (interaction !== 'none') return
// 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 _node = getNodeFromPath<VariableDeclaration>(
_ast,
sketchPathToNode || [],
'VariableDeclaration'
)
if (trap(_node)) return
const sketchInit = _node.node?.declaration.init
if (sketchInit.type === 'PipeExpression') {
updateCenterRectangleSketch(
sketchInit,
x,
y,
tags[0],
rectangleOrigin[0],
rectangleOrigin[1]
)
const pResult = parse(recast(_ast))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(pResult)
_ast = pResult.program
// Update the primary AST and unequip the rectangle tool
await kclManager.executeAstMock(_ast)
sceneInfra.modelingSend({ type: 'Finish center rectangle' })
// lee: I had this at the bottom of the function, but it's
// possible sketchFromKclValue "fails" when sketching on a face,
// and this couldn't wouldn't run.
await codeManager.updateEditorWithAstAndWriteToFile(_ast)
const { execState } = await executeAst({
ast: _ast,
engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride,
})
const programMemory = execState.memory
// Prepare to update the THREEjs scene
this.sceneProgramMemory = programMemory
const sketch = sketchFromKclValue(
programMemory.get(variableDeclarationName),
variableDeclarationName
)
if (err(sketch)) return
const sgPaths = sketch.paths
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
// Update the starting segment of the THREEjs scene
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
// Update the rest of the segments of the THREEjs scene
sgPaths.forEach((seg, index) =>
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch)
)
}
},
})
}
// lee: Well, it appears all our code in sceneEntities each act as their own
// kind of classes. In this case, I'll keep utility functions pertaining to
// circle3Point here. Feel free to extract as needed.
entryDraftCircle3Point = (
done: () => void,
startSketchOnASTNodePath: PathToNode,
forward: Vector3,
up: Vector3,
sketchOrigin: Vector3
): (() => void) => {
// lee: Not a fan we need to re-iterate this dummy object all over the place
// just to get the scale but okie dokie.
const dummy = new Mesh()
dummy.position.set(0, 0, 0)
const scale = sceneInfra.getClientSceneScaleFactor(dummy)
const orientation = quaternionFromUpNForward(up, forward)
// Reminder: the intersection plane is the primary way to derive a XY
// position from a mouse click in ThreeJS.
// Here, we position and orient so it's facing the viewer.
this.intersectionPlane!.setRotationFromQuaternion(orientation)
this.intersectionPlane!.position.copy(sketchOrigin)
// Keep track of points in the scene with their ThreeJS ids.
const points: Map<number, Vector2> = new Map()
// Keep a reference so we can destroy and recreate as needed.
let groupCircle: Group | undefined
// Add our new group to the list of groups to render
const groupOfDrafts = new Group()
groupOfDrafts.name = 'circle-3-point-group'
groupOfDrafts.position.copy(sketchOrigin)
// lee: I'm keeping this here as a developer gotchya:
// If you use 3D points, do not rotate anything.
// If you use 2D points (easier to deal with, generally do this!), then
// rotate the group just like this! Remember to rotate other groups too!
groupOfDrafts.setRotationFromQuaternion(orientation)
this.scene.add(groupOfDrafts)
// How large the points on the circle will render as
const DRAFT_POINT_RADIUS = 10 // px
// The target of our dragging
let target: Object3D | undefined = undefined
// The KCL this will generate.
const kclCircle3Point = parse(`circleThreePoint(
p1 = [0.0, 0.0],
p2 = [0.0, 0.0],
p3 = [0.0, 0.0],
)`)
const createPoint = (
center: Vector3,
opts?: { noInteraction?: boolean }
): Mesh => {
const geometry = new SphereGeometry(DRAFT_POINT_RADIUS)
const color = getThemeColorForThreeJs(sceneInfra._theme)
const material = new MeshBasicMaterial({
color: opts?.noInteraction
? sceneInfra._theme === 'light'
? new Color(color).multiplyScalar(0.15)
: new Color(0x010101).multiplyScalar(2000)
: color,
})
const mesh = new Mesh(geometry, material)
mesh.userData = {
type: opts?.noInteraction ? 'ghost' : CIRCLE_3_POINT_DRAFT_POINT,
}
mesh.renderOrder = 1000
mesh.layers.set(SKETCH_LAYER)
mesh.position.copy(center)
mesh.scale.set(scale, scale, scale)
mesh.renderOrder = 100
return mesh
}
const createCircle3PointGraphic = async (
points: Vector2[],
center: Vector2,
radius: number
) => {
if (
Number.isNaN(radius) ||
Number.isNaN(center.x) ||
Number.isNaN(center.y)
)
return
const color = getThemeColorForThreeJs(sceneInfra._theme)
const lineCircle = createCircleGeometry({
center: [center.x, center.y],
radius,
color,
isDashed: false,
scale: 1,
})
lineCircle.userData = { type: CIRCLE_3_POINT_DRAFT_CIRCLE }
lineCircle.layers.set(SKETCH_LAYER)
// devnote: it's a mistake to use these with EllipseCurve :)
// lineCircle.position.set(center.x, center.y, 0)
// lineCircle.scale.set(scale, scale, scale)
if (groupCircle) groupOfDrafts.remove(groupCircle)
groupCircle = new Group()
groupCircle.renderOrder = 1
groupCircle.add(lineCircle)
const pointMesh = createPoint(new Vector3(center.x, center.y, 0), {
noInteraction: true,
})
groupCircle.add(pointMesh)
const geometryPolyLine = new BufferGeometry().setFromPoints([
...points.map((p) => new Vector3(p.x, p.y, 0)),
new Vector3(points[0].x, points[0].y, 0),
])
const materialPolyLine = new LineDashedMaterial({
color,
scale: 1 / scale,
dashSize: 6,
gapSize: 6,
})
const meshPolyLine = new Line(geometryPolyLine, materialPolyLine)
meshPolyLine.computeLineDistances()
groupCircle.add(meshPolyLine)
groupOfDrafts.add(groupCircle)
}
const insertCircle3PointKclIntoAstSnapshot = (
points: Vector2[]
): Program => {
if (err(kclCircle3Point) || kclCircle3Point.program === null)
return kclManager.ast
if (kclCircle3Point.program.body[0].type !== 'ExpressionStatement')
return kclManager.ast
if (
kclCircle3Point.program.body[0].expression.type !== 'CallExpressionKw'
)
return kclManager.ast
const arg = (x: LabeledArg): Literal[] | undefined => {
if (
'arg' in x &&
'elements' in x.arg &&
x.arg.type === 'ArrayExpression'
) {
if (x.arg.elements.every((x) => x.type === 'Literal')) {
return x.arg.elements
}
}
return undefined
}
const kclCircle3PointArgs =
kclCircle3Point.program.body[0].expression.arguments
const arg0 = arg(kclCircle3PointArgs[0])
if (!arg0) return kclManager.ast
arg0[0].value = { value: points[0].x, suffix: 'None' }
arg0[0].raw = points[0].x.toString()
arg0[1].value = { value: points[0].y, suffix: 'None' }
arg0[1].raw = points[0].y.toString()
const arg1 = arg(kclCircle3PointArgs[1])
if (!arg1) return kclManager.ast
arg1[0].value = { value: points[1].x, suffix: 'None' }
arg1[0].raw = points[1].x.toString()
arg1[1].value = { value: points[1].y, suffix: 'None' }
arg1[1].raw = points[1].y.toString()
const arg2 = arg(kclCircle3PointArgs[2])
if (!arg2) return kclManager.ast
arg2[0].value = { value: points[2].x, suffix: 'None' }
arg2[0].raw = points[2].x.toString()
arg2[1].value = { value: points[2].y, suffix: 'None' }
arg2[1].raw = points[2].y.toString()
const astSnapshot = structuredClone(kclManager.ast)
const startSketchOnASTNode = getNodeFromPath<VariableDeclaration>(
astSnapshot,
startSketchOnASTNodePath,
'VariableDeclaration'
)
if (err(startSketchOnASTNode)) return astSnapshot
// It's possible we're already dealing with a PipeExpression.
// Modify the current one.
if (
startSketchOnASTNode.node.declaration.init.type === 'PipeExpression' &&
startSketchOnASTNode.node.declaration.init.body[1].type ===
'CallExpressionKw' &&
startSketchOnASTNode.node.declaration.init.body.length >= 2
) {
startSketchOnASTNode.node.declaration.init.body[1].arguments =
kclCircle3Point.program.body[0].expression.arguments
} else {
// Clone a new node based on the old, and replace the old with the new.
const clonedStartSketchOnASTNode = structuredClone(startSketchOnASTNode)
startSketchOnASTNode.node.declaration.init = createPipeExpression([
clonedStartSketchOnASTNode.node.declaration.init,
kclCircle3Point.program.body[0].expression,
])
}
// Return the `Program`
return astSnapshot
}
const updateCircle3Point = async (opts?: { execute?: true }) => {
const points_ = Array.from(points.values())
const circleParams = calculate_circle_from_3_points(
points_[0].x,
points_[0].y,
points_[1].x,
points_[1].y,
points_[2].x,
points_[2].y
)
if (Number.isNaN(circleParams.radius)) return
await createCircle3PointGraphic(
points_,
new Vector2(circleParams.center_x, circleParams.center_y),
circleParams.radius
)
const astWithNewCode = insertCircle3PointKclIntoAstSnapshot(points_)
const codeAsString = recast(astWithNewCode)
if (err(codeAsString)) return
codeManager.updateCodeStateEditor(codeAsString)
}
const cleanupFn = () => {
this.scene.remove(groupOfDrafts)
}
// The AST node we extracted earlier may already have a circleThreePoint!
// Use the points in the AST as starting points.
const astSnapshot = structuredClone(kclManager.ast)
const maybeVariableDeclaration = getNodeFromPath<VariableDeclaration>(
astSnapshot,
startSketchOnASTNodePath,
'VariableDeclaration'
)
if (err(maybeVariableDeclaration))
return () => {
done()
}
const maybeCallExpressionKw = maybeVariableDeclaration.node.declaration.init
if (
maybeCallExpressionKw.type === 'PipeExpression' &&
maybeCallExpressionKw.body[1].type === 'CallExpressionKw' &&
maybeCallExpressionKw.body[1]?.callee.name === 'circleThreePoint'
) {
maybeCallExpressionKw?.body[1].arguments
.map(
({ arg }: any) =>
new Vector2(arg.elements[0].value, arg.elements[1].value)
)
.forEach((point: Vector2) => {
const pointMesh = createPoint(new Vector3(point.x, point.y, 0))
groupOfDrafts.add(pointMesh)
points.set(pointMesh.id, point)
})
void updateCircle3Point()
}
sceneInfra.setCallbacks({
async onDrag(args) {
const draftPointsIntersected = args.intersects.filter(
(intersected) =>
intersected.object.userData.type === CIRCLE_3_POINT_DRAFT_POINT
)
const firstPoint = draftPointsIntersected[0]
if (firstPoint && !target) {
target = firstPoint.object
}
// The user was off their mark! Missed the object to select.
if (!target) return
target.position.copy(
new Vector3(
args.intersectionPoint.twoD.x,
args.intersectionPoint.twoD.y,
0
)
)
points.set(target.id, args.intersectionPoint.twoD)
if (points.size <= 2) return
await updateCircle3Point()
},
async onDragEnd(_args) {
target = undefined
},
async onClick(args) {
if (points.size >= 3) return
if (!args.intersectionPoint) return
const pointMesh = createPoint(
new Vector3(
args.intersectionPoint.twoD.x,
args.intersectionPoint.twoD.y,
0
)
)
groupOfDrafts.add(pointMesh)
points.set(pointMesh.id, args.intersectionPoint.twoD)
if (points.size <= 2) return
await updateCircle3Point()
},
})
return cleanupFn
}
setupDraftCircle = async (
sketchPathToNode: PathToNode,
forward: [number, number, number],
up: [number, number, number],
sketchOrigin: [number, number, number],
circleCenter: [x: number, y: number]
) => {
let _ast = structuredClone(kclManager.ast)
const _node1 = getNodeFromPath<VariableDeclaration>(
_ast,
sketchPathToNode || [],
'VariableDeclaration'
)
if (trap(_node1)) return Promise.reject(_node1)
const variableDeclarationName = _node1.node?.declaration.id?.name || ''
const startSketchOn = _node1.node?.declaration
const startSketchOnInit = startSketchOn?.init
startSketchOn.init = createPipeExpression([
startSketchOnInit,
createCallExpressionStdLib('circle', [
createObjectExpression({
center: createArrayExpression([
createLiteral(roundOff(circleCenter[0])),
createLiteral(roundOff(circleCenter[1])),
]),
radius: createLiteral(1),
}),
createPipeSubstitution(),
]),
])
const pResult = parse(recast(_ast))
if (trap(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult)
_ast = pResult.program
// do a quick mock execution to get the program memory up-to-date
await kclManager.executeAstMock(_ast)
const { programMemoryOverride, truncatedAst } = await this.setupSketch({
sketchPathToNode,
forward,
up,
position: sketchOrigin,
maybeModdedAst: _ast,
draftExpressionsIndices: { start: 0, end: 0 },
})
sceneInfra.setCallbacks({
onMove: async (args) => {
const pathToNodeTwo = structuredClone(sketchPathToNode)
pathToNodeTwo[1][0] = 0
const _node = getNodeFromPath<VariableDeclaration>(
truncatedAst,
pathToNodeTwo || [],
'VariableDeclaration'
)
let modded = structuredClone(truncatedAst)
if (trap(_node)) return
const sketchInit = _node.node.declaration.init
const x = (args.intersectionPoint.twoD.x || 0) - circleCenter[0]
const y = (args.intersectionPoint.twoD.y || 0) - circleCenter[1]
if (sketchInit.type === 'PipeExpression') {
const moddedResult = changeSketchArguments(
modded,
kclManager.programMemory,
{
type: 'path',
pathToNode: [
..._node.deepPath,
['body', 'PipeExpression'],
[1, 'index'],
],
},
{
type: 'arc-segment',
center: circleCenter,
radius: Math.sqrt(x ** 2 + y ** 2),
from: circleCenter,
}
)
if (err(moddedResult)) return
modded = moddedResult.modifiedAst
}
const { execState } = await executeAst({
ast: modded,
engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride,
})
const programMemory = execState.memory
this.sceneProgramMemory = programMemory
const sketch = sketchFromKclValue(
programMemory.get(variableDeclarationName),
variableDeclarationName
)
if (err(sketch)) return
const sgPaths = sketch.paths
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
sgPaths.forEach((seg, index) =>
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch)
)
},
onClick: async (args) => {
// If there is a valid camera interaction that matches, do that instead
const interaction = sceneInfra.camControls.getInteractionType(
args.mouseEvent
)
if (interaction !== 'none') return
// 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) - circleCenter[0])
const y = roundOff((cornerPoint.y || 0) - circleCenter[1])
const _node = getNodeFromPath<VariableDeclaration>(
_ast,
sketchPathToNode || [],
'VariableDeclaration'
)
if (trap(_node)) return
const sketchInit = _node.node?.declaration.init
let modded = structuredClone(_ast)
if (sketchInit.type === 'PipeExpression') {
const moddedResult = changeSketchArguments(
modded,
kclManager.programMemory,
{
type: 'path',
pathToNode: [
..._node.deepPath,
['body', 'PipeExpression'],
[1, 'index'],
],
},
{
type: 'arc-segment',
center: circleCenter,
radius: Math.sqrt(x ** 2 + y ** 2),
from: circleCenter,
}
)
if (err(moddedResult)) return
modded = moddedResult.modifiedAst
const newCode = recast(modded)
if (err(newCode)) return
const pResult = parse(newCode)
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(pResult)
_ast = pResult.program
// Update the primary AST and unequip the rectangle tool
await kclManager.executeAstMock(_ast)
sceneInfra.modelingSend({ type: 'Finish circle' })
await codeManager.updateEditorWithAstAndWriteToFile(_ast)
}
},
})
}
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 })
// eslint-disable-next-line @typescript-eslint/no-floating-promises
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,
})
await codeManager.writeToFile()
}
},
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 sketch = sketchFromPathToNode({
pathToNode,
ast: kclManager.ast,
programMemory: kclManager.programMemory,
})
if (trap(sketch)) return
if (!sketch) {
trap(new Error('sketch not found'))
return
}
const pipeIndex = pathToNode[pathToNodeIndex + 1][0] as number
if (addingNewSegmentStatus === 'nothing') {
const prevSegment = sketch.paths[pipeIndex - 2]
const mod = addNewSketchLn({
node: kclManager.ast,
programMemory: kclManager.programMemory,
input: {
type: 'straight-segment',
to: [intersectionPoint.twoD.x, intersectionPoint.twoD.y],
from: prevSegment.from,
},
// 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'
if (trap(mod)) return
await kclManager.executeAstMock(mod.modifiedAst)
await this.tearDownSketch({ removeAxis: false })
// eslint-disable-next-line @typescript-eslint/no-floating-promises
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 there is a valid camera interaction that matches, do that instead
const interaction = sceneInfra.camControls.getInteractionType(
args.mouseEvent
)
if (interaction !== 'none') return
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?: Node<Program>,
draftSegment?: DraftSegment
) =>
prepareTruncatedMemoryAndAst(
sketchPathToNode,
ast || kclManager.ast,
kclManager.lastSuccessfulProgramMemory,
draftSegment
)
onDragSegment({
object,
intersection2d: _intersection2d,
sketchPathToNode,
draftInfo,
intersects,
}: {
object: any
intersection2d: Vector2
sketchPathToNode: PathToNode
intersects: Intersection<Object3D<Object3DEventMap>>[]
draftInfo?: {
truncatedAst: Node<Program>
programMemoryOverride: ProgramMemory
variableDeclarationName: string
}
}) {
const intersectsProfileStart =
draftInfo &&
intersects
.map(({ object }) => getParentGroup(object, [PROFILE_START]))
.find((a) => a?.name === PROFILE_START)
const intersection2d = intersectsProfileStart
? new Vector2(
intersectsProfileStart.position.x,
intersectsProfileStart.position.y
)
: _intersection2d
const intersectsYAxis = intersects.find(
(sceneObject) => sceneObject.object.name === Y_AXIS
)
const intersectsXAxis = intersects.find(
(sceneObject) => sceneObject.object.name === X_AXIS
)
const snappedPoint = new Vector2(
intersectsYAxis ? 0 : intersection2d.x,
intersectsXAxis ? 0 : intersection2d.y
)
const group = getParentGroup(object, SEGMENT_BODIES_PLUS_PROFILE_START)
const subGroup = getParentGroup(object, [ARROWHEAD, CIRCLE_CENTER_HANDLE])
if (!group) return
const pathToNode: PathToNode = structuredClone(group.userData.pathToNode)
const varDecIndex = pathToNode[1][0]
if (typeof varDecIndex !== 'number') {
console.error(
`Expected varDecIndex to be a number, but found: ${typeof varDecIndex} ${varDecIndex}`
)
return
}
if (draftInfo) {
pathToNode[1][0] = 0
}
const from: [number, number] = [
group.userData.from[0],
group.userData.from[1],
]
const dragTo: [number, number] = [snappedPoint.x, snappedPoint.y]
let modifiedAst = draftInfo ? draftInfo.truncatedAst : { ...kclManager.ast }
const _node = getNodeFromPath<Node<CallExpression>>(
modifiedAst,
pathToNode,
'CallExpression'
)
if (trap(_node)) return
const node = _node.node
if (node.type !== 'CallExpression') return
let modded:
| {
modifiedAst: Node<Program>
pathToNode: PathToNode
}
| Error
const getChangeSketchInput = (): SegmentInputs => {
if (
group.name === CIRCLE_SEGMENT &&
// !subGroup treats grabbing the outer circumference of the circle
// as a drag of the center handle
(!subGroup || subGroup?.name === ARROWHEAD)
)
return {
type: 'arc-segment',
from,
center: group.userData.center,
// distance between the center and the drag point
radius: Math.sqrt(
(group.userData.center[0] - dragTo[0]) ** 2 +
(group.userData.center[1] - dragTo[1]) ** 2
),
}
if (
group.name === CIRCLE_SEGMENT &&
subGroup?.name === CIRCLE_CENTER_HANDLE
)
return {
type: 'arc-segment',
from,
center: dragTo,
radius: group.userData.radius,
}
// straight segment is the default
return {
type: 'straight-segment',
from,
to: dragTo,
}
}
if (group.name === PROFILE_START) {
modded = updateStartProfileAtArgs({
node: modifiedAst,
pathToNode,
input: {
type: 'straight-segment',
to: dragTo,
from,
},
previousProgramMemory: kclManager.programMemory,
})
} else {
modded = changeSketchArguments(
modifiedAst,
kclManager.programMemory,
{
type: 'sourceRange',
sourceRange: topLevelRange(node.start, node.end),
},
getChangeSketchInput()
)
}
if (trap(modded)) return
modifiedAst = modded.modifiedAst
const info = draftInfo
? draftInfo
: this.prepareTruncatedMemoryAndAst(pathToNode || [])
if (trap(info, { suppress: true })) return
const { truncatedAst, programMemoryOverride, variableDeclarationName } =
info
;(async () => {
const code = recast(modifiedAst)
if (trap(code)) return
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.updateCodeEditor(code)
const { execState } = await executeAst({
ast: truncatedAst,
engineCommandManager: this.engineCommandManager,
// We make sure to send an empty program memory to denote we mean mock mode.
programMemoryOverride,
})
const programMemory = execState.memory
this.sceneProgramMemory = programMemory
const maybeSketch = programMemory.get(variableDeclarationName)
let sketch: Sketch | undefined
const sk = sketchFromKclValueOptional(
maybeSketch,
variableDeclarationName
)
if (!(sk instanceof Reason)) {
sketch = sk
} else if (maybeSketch && (maybeSketch.value as Solid)?.sketch) {
sketch = (maybeSketch.value as Solid).sketch
}
if (!sketch) return
const sgPaths = sketch.paths
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
this.updateSegment(
sketch.start,
0,
varDecIndex,
modifiedAst,
orthoFactor,
sketch
)
const callBacks = sgPaths.map((group, index) =>
this.updateSegment(
group,
index,
varDecIndex,
modifiedAst,
orthoFactor,
sketch
)
)
sceneInfra.overlayCallbacks(callBacks)
})().catch(reportRejection)
}
/**
* 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 sketch
*/
updateSegment = (
segment: Path | Sketch['start'],
index: number,
varDecIndex: number,
modifiedAst: Program,
orthoFactor: number,
sketch: Sketch
): (() => SegmentOverlayPayload | null) => {
const segPathToNode = getNodePathFromSourceRange(
modifiedAst,
sourceRangeFromRust(segment.__geoMeta.sourceRange)
)
const sgPaths = sketch.paths
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/sketch segments
const group =
this.activeSegments[pathToNodeStr] ||
this.activeSegments[originalPathToNodeStr]
// const prevSegment = sketch.slice(index - 1)[0]
const type = group?.userData?.type
const factor =
(sceneInfra.camControls.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(sceneInfra.camControls.camera, group)) /
sceneInfra._baseUnitMultiplier
let input: SegmentInputs = {
type: 'straight-segment',
from: segment.from,
to: segment.to,
}
let update: SegmentUtils['update'] | null = null
if (type === TANGENTIAL_ARC_TO_SEGMENT) {
update = segmentUtils.tangentialArcTo.update
} else if (type === STRAIGHT_SEGMENT) {
update = segmentUtils.straight.update
} else if (
type === CIRCLE_SEGMENT &&
'type' in segment &&
segment.type === 'Circle'
) {
update = segmentUtils.circle.update
input = {
type: 'arc-segment',
from: segment.from,
center: segment.center,
radius: segment.radius,
}
}
const callBack =
update &&
!err(update) &&
update({
input,
group,
scale: factor,
prevSegment: sgPaths[index - 1],
sceneInfra,
})
if (callBack && !err(callBack)) return callBack
if (type === PROFILE_START) {
group.position.set(segment.from[0], segment.from[1], 0)
group.scale.set(factor, factor, factor)
}
return () => null
}
/**
* Update the base color of each of the THREEjs meshes
* that represent each of the sketch segments, to get the
* latest value from `sceneInfra._theme`
*/
updateSegmentBaseColor(newColor: Themes.Light | Themes.Dark) {
const newColorThreeJs = getThemeColorForThreeJs(newColor)
Object.values(this.activeSegments).forEach((group) => {
group.userData.baseColor = newColorThreeJs
group.traverse((child) => {
if (
child instanceof Mesh &&
child.material instanceof MeshBasicMaterial
) {
child.material.color.set(newColorThreeJs)
}
})
})
}
removeSketchGrid() {
if (this.axisGroup) this.scene.remove(this.axisGroup)
}
private _tearDownSketch(
callDepth = 0,
resolve: (val: unknown) => void,
reject: () => void,
{ removeAxis = true }: { removeAxis?: boolean }
) {
// Remove all draft groups
this.draftPointGroups.forEach((draftPointGroup) => {
this.scene.remove(draftPointGroup)
})
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) {
// We have to manually remove the CSS2DObjects
// as they don't get removed when the group is removed
sketchSegments.traverse((object) => {
if (object instanceof CSS2DObject) {
object.element.remove()
object.remove()
}
})
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 {
resolve(true)
}
}
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 })
})
}
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,
SEGMENT_BODIES_PLUS_PROFILE_START
)
if (parent?.userData?.pathToNode) {
const pResult = parse(recast(kclManager.ast))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(pResult)
const updatedAst = pResult.program
const _node = getNodeFromPath<Node<CallExpression>>(
updatedAst,
parent.userData.pathToNode,
'CallExpression'
)
if (trap(_node, { suppress: true })) return
const node = _node.node
editorManager.setHighlightRange([topLevelRange(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)
let input: SegmentInputs = {
type: 'straight-segment',
from: parent.userData.from,
to: parent.userData.to,
}
const factor =
(sceneInfra.camControls.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(sceneInfra.camControls.camera, parent)) /
sceneInfra._baseUnitMultiplier
let update: SegmentUtils['update'] | null = null
if (parent.name === STRAIGHT_SEGMENT) {
update = segmentUtils.straight.update
} else if (parent.name === TANGENTIAL_ARC_TO_SEGMENT) {
update = segmentUtils.tangentialArcTo.update
input = {
type: 'arc-segment',
from: parent.userData.from,
radius: parent.userData.radius,
center: parent.userData.center,
}
}
update &&
update({
prevSegment: parent.userData.prevSegment,
input,
group: parent,
scale: factor,
sceneInfra,
})
return
}
editorManager.setHighlightRange([defaultSourceRange()])
},
onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => {
editorManager.setHighlightRange([defaultSourceRange()])
const parent = getParentGroup(
selected,
SEGMENT_BODIES_PLUS_PROFILE_START
)
if (parent) {
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
let input: SegmentInputs = {
type: 'straight-segment',
from: parent.userData.from,
to: parent.userData.to,
}
const factor =
(sceneInfra.camControls.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(sceneInfra.camControls.camera, parent)) /
sceneInfra._baseUnitMultiplier
let update: SegmentUtils['update'] | null = null
if (parent.name === STRAIGHT_SEGMENT) {
update = segmentUtils.straight.update
} else if (parent.name === TANGENTIAL_ARC_TO_SEGMENT) {
update = segmentUtils.tangentialArcTo.update
input = {
type: 'arc-segment',
from: parent.userData.from,
radius: parent.userData.radius,
center: parent.userData.center,
}
}
update &&
update({
prevSegment: parent.userData.prevSegment,
input,
group: parent,
scale: factor,
sceneInfra,
})
}
const isSelected = parent?.userData?.isSelected
colorSegment(
selected,
isSelected
? 0x0000ff
: parent?.userData?.baseColor ||
getThemeColorForThreeJs(sceneInfra._theme)
)
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)
}
},
}
}
resetOverlays() {
sceneInfra.modelingSend({
type: 'Set Segment Overlays',
data: {
type: 'clear',
},
})
}
}
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: Node<Program>,
programMemory: ProgramMemory,
draftSegment?: DraftSegment
):
| {
truncatedAst: Node<Program>
programMemoryOverride: ProgramMemory
variableDeclarationName: string
}
| Error {
const bodyIndex = Number(sketchPathToNode?.[1]?.[0]) || 0
const _ast = structuredClone(ast)
const _node = getNodeFromPath<Node<VariableDeclaration>>(
_ast,
sketchPathToNode || [],
'VariableDeclaration'
)
if (err(_node)) return _node
const variableDeclarationName = _node.node?.declaration.id?.name || ''
const sg = sketchFromKclValue(
programMemory.get(variableDeclarationName),
variableDeclarationName
)
if (err(sg)) return sg
const lastSeg = sg?.paths.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).declaration
.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/sketch segments
const pResult = parse(recast(_ast)) // get source ranges correct since unfortunately we still rely on them
if (trap(pResult) || !resultIsOk(pResult))
return Error('Unexpected compilation error')
const updatedSrcRangeAst = pResult.program
const lastPipeItem = (
(updatedSrcRangeAst.body[bodyIndex] as VariableDeclaration).declaration
.init as PipeExpression
).body.slice(-1)[0]
;(
(_ast.body[bodyIndex] as VariableDeclaration).declaration
.init as PipeExpression
).body.slice(-1)[0].start = lastPipeItem.start
_ast.end = lastPipeItem.end
const varDec = _ast.body[bodyIndex] as Node<VariableDeclaration>
varDec.end = lastPipeItem.end
const declarator = varDec.declaration
declarator.end = lastPipeItem.end
const init = declarator.init as Node<PipeExpression>
init.end = lastPipeItem.end
init.body.slice(-1)[0].end = lastPipeItem.end
}
const truncatedAst: Node<Program> = {
..._ast,
body: [structuredClone(_ast.body[bodyIndex])],
}
// Grab all the TagDeclarators and TagIdentifiers from memory.
let start = _node.node.start
const programMemoryOverride = programMemory.filterVariables(true, (value) => {
if (
!('__meta' in value) ||
value.__meta === undefined ||
value.__meta.length === 0 ||
value.__meta[0].sourceRange === undefined
) {
return false
}
if (value.__meta[0].sourceRange[0] >= start) {
// We only want things before our start point.
return false
}
return value.type === 'TagIdentifier'
})
if (err(programMemoryOverride)) return programMemoryOverride
for (let i = 0; i < bodyIndex; i++) {
const node = _ast.body[i]
if (node.type !== 'VariableDeclaration') {
continue
}
const name = node.declaration.id.name
const memoryItem = programMemory.get(name)
if (!memoryItem) {
continue
}
const error = programMemoryOverride.set(name, structuredClone(memoryItem))
if (err(error)) return error
}
return {
truncatedAst,
programMemoryOverride,
variableDeclarationName,
}
}
export function getParentGroup(
object: any,
stopAt: string[] = SEGMENT_BODIES
): Group | null {
if (stopAt.includes(object?.userData?.type)) {
return object
} else if (object?.parent) {
return getParentGroup(object.parent, stopAt)
}
return null
}
export function sketchFromPathToNode({
pathToNode,
ast,
programMemory,
}: {
pathToNode: PathToNode
ast: Program
programMemory: ProgramMemory
}): Sketch | null | Error {
const _varDec = getNodeFromPath<VariableDeclarator>(
kclManager.ast,
pathToNode,
'VariableDeclarator'
)
if (err(_varDec)) return _varDec
const varDec = _varDec.node
const result = programMemory.get(varDec?.id?.name || '')
if (result?.type === 'Solid') {
return result.value.sketch
}
const sg = sketchFromKclValue(result, varDec?.id?.name)
if (err(sg)) {
return null
}
return sg
}
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, SEGMENT_BODIES)
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 | Error {
const sketch = sketchFromPathToNode({
pathToNode: sketchPathToNode,
ast: kclManager.ast,
programMemory: kclManager.programMemory,
})
if (err(sketch)) return sketch
const zAxis = sketch?.on.zAxis || sketchNormalBackUp
if (!zAxis) return Error('Sketch zAxis not found')
return getQuaternionFromZAxis(massageFormats(zAxis))
}
export async function getSketchOrientationDetails(
sketchPathToNode: PathToNode
): Promise<{
quat: Quaternion
sketchDetails: SketchDetails & { faceId?: string }
}> {
const sketch = sketchFromPathToNode({
pathToNode: sketchPathToNode,
ast: kclManager.ast,
programMemory: kclManager.programMemory,
})
if (err(sketch)) return Promise.reject(sketch)
if (!sketch) return Promise.reject('sketch not found')
if (sketch.on.type === 'plane') {
const zAxis = sketch?.on.zAxis
return {
quat: getQuaternionFromZAxis(massageFormats(zAxis)),
sketchDetails: {
sketchPathToNode,
zAxis: [zAxis.x, zAxis.y, zAxis.z],
yAxis: [sketch.on.yAxis.x, sketch.on.yAxis.y, sketch.on.yAxis.z],
origin: [0, 0, 0],
faceId: sketch.on.id,
},
}
}
if (sketch.on.type === 'face') {
const faceInfo = await getFaceDetails(sketch.on.id)
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
return Promise.reject('face info')
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: sketch.on.id,
},
}
}
return Promise.reject(
'sketch.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.
*/
export async function getFaceDetails(
entityId: string
): Promise<Models['GetSketchModePlane_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,
},
})
const resp = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'get_sketch_mode_plane' },
})
const faceInfo =
resp?.success &&
resp?.resp.type === 'modeling' &&
resp?.resp?.data?.modeling_response?.type === 'get_sketch_mode_plane'
? resp?.resp?.data?.modeling_response.data
: ({} as Models['GetSketchModePlane_type'])
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: Vec3Array | Point3d): Vector3 {
return isArray(a) ? new Vector3(a[0], a[1], a[2]) : new Vector3(a.x, a.y, a.z)
}
/**
* Given a SourceRange [x,y,boolean] create a Selections object which contains
* graphSelections with the artifact and codeRef.
* This can be passed to 'Set selection' to internally set the selection of the
* modelingMachine from code.
*/
function computeSelectionFromSourceRangeAndAST(
sourceRange: SourceRange,
ast: Node<Program>
): Selections {
const artifactGraph = engineCommandManager.artifactGraph
const artifact = getArtifactFromRange(sourceRange, artifactGraph) || undefined
const selection: Selections = {
graphSelections: [
{
artifact,
codeRef: codeRefFromRange(sourceRange, ast),
},
],
otherSelections: [],
}
return selection
}