* random other cahnges Signed-off-by: Jess Frazelle <github@jessfraz.com> * turn back on test Signed-off-by: Jess Frazelle <github@jessfraz.com> * docs Signed-off-by: Jess Frazelle <github@jessfraz.com> * lots of enhancements Signed-off-by: Jess Frazelle <github@jessfraz.com> * cleanup Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * mesh test Signed-off-by: Jess Frazelle <github@jessfraz.com> * mesh test Signed-off-by: Jess Frazelle <github@jessfraz.com> * check panics Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * check panics Signed-off-by: Jess Frazelle <github@jessfraz.com> * check panics Signed-off-by: Jess Frazelle <github@jessfraz.com> * cleanup Signed-off-by: Jess Frazelle <github@jessfraz.com> * if running in vitest make single threadedd Signed-off-by: Jess Frazelle <github@jessfraz.com> * check if running in vitest Signed-off-by: Jess Frazelle <github@jessfraz.com> * console logs Signed-off-by: Jess Frazelle <github@jessfraz.com> --------- Signed-off-by: Jess Frazelle <github@jessfraz.com>
3990 lines
129 KiB
TypeScript
3990 lines
129 KiB
TypeScript
import toast from 'react-hot-toast'
|
|
import type {
|
|
Intersection,
|
|
Object3D,
|
|
Object3DEventMap,
|
|
Quaternion,
|
|
} from 'three'
|
|
|
|
import {
|
|
BoxGeometry,
|
|
DoubleSide,
|
|
ExtrudeGeometry,
|
|
Group,
|
|
LineCurve3,
|
|
Mesh,
|
|
MeshBasicMaterial,
|
|
OrthographicCamera,
|
|
PerspectiveCamera,
|
|
PlaneGeometry,
|
|
Points,
|
|
Shape,
|
|
Vector2,
|
|
Vector3,
|
|
} from 'three'
|
|
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
|
|
import { radToDeg } from 'three/src/math/MathUtils'
|
|
|
|
import type { Models } from '@kittycad/lib/dist/types/src'
|
|
import type { CallExpression } from '@rust/kcl-lib/bindings/CallExpression'
|
|
import type { CallExpressionKw } from '@rust/kcl-lib/bindings/CallExpressionKw'
|
|
import type { Node } from '@rust/kcl-lib/bindings/Node'
|
|
import type { Path } from '@rust/kcl-lib/bindings/Path'
|
|
import type { PipeExpression } from '@rust/kcl-lib/bindings/PipeExpression'
|
|
import type { Program } from '@rust/kcl-lib/bindings/Program'
|
|
import type { Sketch } from '@rust/kcl-lib/bindings/Sketch'
|
|
import type { SourceRange } from '@rust/kcl-lib/bindings/SourceRange'
|
|
import type { VariableDeclaration } from '@rust/kcl-lib/bindings/VariableDeclaration'
|
|
import type { VariableDeclarator } from '@rust/kcl-lib/bindings/VariableDeclarator'
|
|
import type { SafeArray } from '@src/lib/utils'
|
|
import { getAngle, getLength, uuidv4 } from '@src/lib/utils'
|
|
|
|
import {
|
|
createGridHelper,
|
|
isQuaternionVertical,
|
|
orthoScale,
|
|
perspScale,
|
|
quaternionFromUpNForward,
|
|
} from '@src/clientSideScene/helpers'
|
|
import {
|
|
ARC_ANGLE_END,
|
|
ARC_SEGMENT,
|
|
ARC_SEGMENT_TYPES,
|
|
CIRCLE_CENTER_HANDLE,
|
|
CIRCLE_SEGMENT,
|
|
CIRCLE_THREE_POINT_HANDLE1,
|
|
CIRCLE_THREE_POINT_HANDLE2,
|
|
CIRCLE_THREE_POINT_HANDLE3,
|
|
CIRCLE_THREE_POINT_SEGMENT,
|
|
DRAFT_DASHED_LINE,
|
|
EXTRA_SEGMENT_HANDLE,
|
|
PROFILE_START,
|
|
SEGMENT_BODIES,
|
|
SEGMENT_BODIES_PLUS_PROFILE_START,
|
|
SEGMENT_WIDTH_PX,
|
|
STRAIGHT_SEGMENT,
|
|
STRAIGHT_SEGMENT_DASH,
|
|
TANGENTIAL_ARC_TO_SEGMENT,
|
|
THREE_POINT_ARC_HANDLE2,
|
|
THREE_POINT_ARC_HANDLE3,
|
|
THREE_POINT_ARC_SEGMENT,
|
|
getParentGroup,
|
|
} from '@src/clientSideScene/sceneConstants'
|
|
import type {
|
|
OnClickCallbackArgs,
|
|
OnMouseEnterLeaveArgs,
|
|
SceneInfra,
|
|
} from '@src/clientSideScene/sceneInfra'
|
|
|
|
import {
|
|
ANGLE_SNAP_THRESHOLD_DEGREES,
|
|
ARROWHEAD,
|
|
AXIS_GROUP,
|
|
DRAFT_POINT,
|
|
DRAFT_POINT_GROUP,
|
|
INTERSECTION_PLANE_LAYER,
|
|
RAYCASTABLE_PLANE,
|
|
SKETCH_GROUP_SEGMENTS,
|
|
SKETCH_LAYER,
|
|
X_AXIS,
|
|
Y_AXIS,
|
|
getSceneScale,
|
|
} from '@src/clientSideScene/sceneUtils'
|
|
import type { SegmentUtils } from '@src/clientSideScene/segments'
|
|
import {
|
|
createProfileStartHandle,
|
|
dashedStraight,
|
|
getTanPreviousPoint,
|
|
segmentUtils,
|
|
} from '@src/clientSideScene/segments'
|
|
import type EditorManager from '@src/editor/manager'
|
|
import type { KclManager } from '@src/lang/KclSingleton'
|
|
import type CodeManager from '@src/lang/codeManager'
|
|
import { ARG_END, ARG_AT, ARG_END_ABSOLUTE } from '@src/lang/constants'
|
|
import {
|
|
createArrayExpression,
|
|
createCallExpressionStdLibKw,
|
|
createLabeledArg,
|
|
createLiteral,
|
|
createLocalName,
|
|
createPipeExpression,
|
|
createPipeSubstitution,
|
|
createVariableDeclaration,
|
|
findUniqueName,
|
|
} from '@src/lang/create'
|
|
import type { ToolTip } from '@src/lang/langHelpers'
|
|
import { executeAstMock } from '@src/lang/langHelpers'
|
|
import { updateModelingState } from '@src/lang/modelingWorkflows'
|
|
import {
|
|
createNodeFromExprSnippet,
|
|
getInsertIndex,
|
|
insertNewStartProfileAt,
|
|
updateSketchNodePathsWithInsertIndex,
|
|
} from '@src/lang/modifyAst'
|
|
import { mutateAstWithTagForSketchSegment } from '@src/lang/modifyAst/addEdgeTreatment'
|
|
import { getNodeFromPath } from '@src/lang/queryAst'
|
|
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
|
|
import {
|
|
codeRefFromRange,
|
|
getArtifactFromRange,
|
|
} from '@src/lang/std/artifactGraph'
|
|
import type { EngineCommandManager } from '@src/lang/std/engineConnection'
|
|
import type { Coords2d } from '@src/lang/std/sketch'
|
|
import {
|
|
addCallExpressionsToPipe,
|
|
addCloseToPipe,
|
|
addNewSketchLn,
|
|
changeSketchArguments,
|
|
updateStartProfileAtArgs,
|
|
} from '@src/lang/std/sketch'
|
|
import type { SegmentInputs } from '@src/lang/std/stdTypes'
|
|
import { crossProduct, topLevelRange } from '@src/lang/util'
|
|
import type { PathToNode, VariableMap } from '@src/lang/wasm'
|
|
import {
|
|
defaultSourceRange,
|
|
getTangentialArcToInfo,
|
|
parse,
|
|
recast,
|
|
resultIsOk,
|
|
sketchFromKclValue,
|
|
sourceRangeFromRust,
|
|
} from '@src/lang/wasm'
|
|
import { EXECUTION_TYPE_MOCK } from '@src/lib/constants'
|
|
import {
|
|
getRectangleCallExpressions,
|
|
updateCenterRectangleSketch,
|
|
updateRectangleSketch,
|
|
} from '@src/lib/rectangleTool'
|
|
import type RustContext from '@src/lib/rustContext'
|
|
import type { Selections } from '@src/lib/selections'
|
|
import { getEventForSegmentSelection } from '@src/lib/selections'
|
|
import type { Themes } from '@src/lib/theme'
|
|
import { getThemeColorForThreeJs } from '@src/lib/theme'
|
|
import { err, reportRejection, trap } from '@src/lib/trap'
|
|
import { isArray, isOverlap, roundOff } from '@src/lib/utils'
|
|
import { closestPointOnRay, deg2Rad } from '@src/lib/utils2d'
|
|
import type {
|
|
SegmentOverlayPayload,
|
|
SketchDetails,
|
|
SketchDetailsUpdate,
|
|
SketchTool,
|
|
} from '@src/machines/modelingMachine'
|
|
import { calculateIntersectionOfTwoLines } from 'sketch-helpers'
|
|
|
|
type DraftSegment = 'line' | 'tangentialArc'
|
|
|
|
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 {
|
|
readonly engineCommandManager: EngineCommandManager
|
|
readonly sceneInfra: SceneInfra
|
|
readonly editorManager: EditorManager
|
|
readonly codeManager: CodeManager
|
|
readonly kclManager: KclManager
|
|
readonly rustContext: RustContext
|
|
activeSegments: { [key: string]: Group } = {}
|
|
readonly intersectionPlane: Mesh
|
|
axisGroup: Group | null = null
|
|
draftPointGroups: Group[] = []
|
|
currentSketchQuaternion: Quaternion | null = null
|
|
|
|
constructor(
|
|
engineCommandManager: EngineCommandManager,
|
|
sceneInfra: SceneInfra,
|
|
editorManager: EditorManager,
|
|
codeManager: CodeManager,
|
|
kclManager: KclManager,
|
|
rustContext: RustContext
|
|
) {
|
|
this.engineCommandManager = engineCommandManager
|
|
this.sceneInfra = sceneInfra
|
|
this.editorManager = editorManager
|
|
this.codeManager = codeManager
|
|
this.kclManager = kclManager
|
|
this.rustContext = rustContext
|
|
this.intersectionPlane = SceneEntities.createIntersectionPlane(
|
|
this.sceneInfra
|
|
)
|
|
this.sceneInfra.camControls.subscribeToCamChange(this.onCamChange)
|
|
window.addEventListener('resize', this.onWindowResize)
|
|
}
|
|
|
|
onWindowResize = () => {
|
|
this.onCamChange()
|
|
}
|
|
onCamChange = () => {
|
|
const orthoFactor = orthoScale(this.sceneInfra.camControls.camera)
|
|
const callbacks: (() => SegmentOverlayPayload | null)[] = []
|
|
Object.values(this.activeSegments).forEach((segment, index) => {
|
|
const factor =
|
|
(this.sceneInfra.camControls.camera instanceof OrthographicCamera
|
|
? orthoFactor
|
|
: perspScale(this.sceneInfra.camControls.camera, segment)) /
|
|
this.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.tangentialArc.update
|
|
}
|
|
if (
|
|
segment.userData &&
|
|
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,
|
|
to: segment.userData.from,
|
|
center: segment.userData.center,
|
|
radius: segment.userData.radius,
|
|
ccw: true,
|
|
}
|
|
}
|
|
if (
|
|
segment.userData &&
|
|
segment.userData.from &&
|
|
segment.userData.center &&
|
|
segment.userData.radius &&
|
|
segment.userData.to &&
|
|
segment.userData.type === ARC_SEGMENT
|
|
) {
|
|
update = segmentUtils.arc.update
|
|
input = {
|
|
type: 'arc-segment',
|
|
from: segment.userData.from,
|
|
to: segment.userData.to,
|
|
center: segment.userData.center,
|
|
radius: segment.userData.radius,
|
|
ccw: segment.userData.ccw,
|
|
}
|
|
}
|
|
if (
|
|
segment.userData.p1 &&
|
|
segment.userData.p2 &&
|
|
segment.userData.p3 &&
|
|
segment.userData.type === CIRCLE_THREE_POINT_SEGMENT
|
|
) {
|
|
update = segmentUtils.circleThreePoint.update
|
|
input = {
|
|
type: 'circle-three-point-segment',
|
|
p1: segment.userData.p1,
|
|
p2: segment.userData.p2,
|
|
p3: segment.userData.p3,
|
|
}
|
|
}
|
|
if (
|
|
segment.userData &&
|
|
segment.userData.from &&
|
|
segment.userData.center &&
|
|
segment.userData.radius &&
|
|
segment.userData.to &&
|
|
segment.userData.type === THREE_POINT_ARC_SEGMENT
|
|
) {
|
|
update = segmentUtils.threePointArc.update
|
|
input = {
|
|
type: 'circle-three-point-segment',
|
|
p1: segment.userData.p1,
|
|
p2: segment.userData.p2,
|
|
p3: segment.userData.p3,
|
|
}
|
|
}
|
|
|
|
const callBack = update?.({
|
|
prevSegment: segment.userData.prevSegment,
|
|
input,
|
|
group: segment,
|
|
scale: factor,
|
|
sceneInfra: this.sceneInfra,
|
|
})
|
|
callBack && !err(callBack) && callbacks.push(callBack)
|
|
if (segment.name === PROFILE_START) {
|
|
segment.scale.set(factor, factor, factor)
|
|
}
|
|
})
|
|
if (this.axisGroup) {
|
|
const factor =
|
|
this.sceneInfra.camControls.camera instanceof OrthographicCamera
|
|
? orthoFactor
|
|
: perspScale(this.sceneInfra.camControls.camera, this.axisGroup)
|
|
const x = this.axisGroup.getObjectByName(X_AXIS)
|
|
x?.scale.set(1, factor / this.sceneInfra._baseUnitMultiplier, 1)
|
|
const y = this.axisGroup.getObjectByName(Y_AXIS)
|
|
y?.scale.set(factor / this.sceneInfra._baseUnitMultiplier, 1, 1)
|
|
}
|
|
this.sceneInfra.overlayCallbacks(callbacks)
|
|
}
|
|
|
|
private static createIntersectionPlane(sceneInfra: SceneInfra) {
|
|
const hundredM = 100_0000
|
|
const planeGeometry = new PlaneGeometry(hundredM, hundredM)
|
|
const planeMaterial = new MeshBasicMaterial({
|
|
color: 0xff0000,
|
|
side: DoubleSide,
|
|
transparent: true,
|
|
opacity: 0.5,
|
|
})
|
|
const intersectionPlane = new Mesh(planeGeometry, planeMaterial)
|
|
intersectionPlane.userData = { type: RAYCASTABLE_PLANE }
|
|
intersectionPlane.name = RAYCASTABLE_PLANE
|
|
intersectionPlane.layers.set(INTERSECTION_PLANE_LAYER)
|
|
sceneInfra.scene.add(intersectionPlane)
|
|
return intersectionPlane
|
|
}
|
|
|
|
createSketchAxis(
|
|
sketchPathToNode: PathToNode,
|
|
forward: [number, number, number],
|
|
up: [number, number, number],
|
|
sketchPosition?: [number, number, number]
|
|
) {
|
|
const orthoFactor = orthoScale(this.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
|
|
|
|
// This makes sure axis lines are picked after segment lines in case of overlapping
|
|
xAxisMesh.position.z = -0.1
|
|
yAxisMesh.position.z = -0.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(
|
|
this.sceneInfra.camControls.camera,
|
|
this.sceneInfra.camControls.target
|
|
)
|
|
gridHelper.scale.set(sceneScale, sceneScale, sceneScale)
|
|
|
|
const factor =
|
|
this.sceneInfra.camControls.camera instanceof OrthographicCamera
|
|
? orthoFactor
|
|
: perspScale(this.sceneInfra.camControls.camera, this.axisGroup)
|
|
xAxisMesh?.scale.set(1, factor / this.sceneInfra._baseUnitMultiplier, 1)
|
|
yAxisMesh?.scale.set(factor / this.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.sceneInfra.scene.add(this.axisGroup)
|
|
}
|
|
|
|
getDraftPoint() {
|
|
return this.sceneInfra.scene.getObjectByName(DRAFT_POINT)
|
|
}
|
|
|
|
createDraftPoint({
|
|
point,
|
|
origin,
|
|
yAxis,
|
|
zAxis,
|
|
}: {
|
|
point: Vector2
|
|
origin: SketchDetails['origin']
|
|
yAxis: SketchDetails['yAxis']
|
|
zAxis: SketchDetails['zAxis']
|
|
}) {
|
|
const draftPointGroup = new Group()
|
|
this.draftPointGroups.push(draftPointGroup)
|
|
draftPointGroup.name = DRAFT_POINT_GROUP
|
|
origin && draftPointGroup.position.set(...origin)
|
|
if (!yAxis) {
|
|
console.error('No sketch quaternion or sketch details found')
|
|
return
|
|
}
|
|
const currentSketchQuaternion = quaternionFromUpNForward(
|
|
new Vector3(...yAxis),
|
|
new Vector3(...zAxis)
|
|
)
|
|
draftPointGroup.setRotationFromQuaternion(currentSketchQuaternion)
|
|
this.sceneInfra.scene.add(draftPointGroup)
|
|
const dummy = new Mesh()
|
|
dummy.position.set(0, 0, 0)
|
|
const scale = this.sceneInfra.getClientSceneScaleFactor(dummy)
|
|
|
|
const draftPoint = createProfileStartHandle({
|
|
isDraft: true,
|
|
from: [point.x, point.y],
|
|
scale,
|
|
theme: this.sceneInfra._theme,
|
|
// default is 12, this makes the draft point pop a bit more,
|
|
// especially when snapping to the startProfile handle as it's it was the exact same size
|
|
size: 16,
|
|
})
|
|
draftPoint.layers.set(SKETCH_LAYER)
|
|
draftPointGroup.add(draftPoint)
|
|
}
|
|
|
|
removeDraftPoint() {
|
|
const draftPoint = this.getDraftPoint()
|
|
if (draftPoint) draftPoint.removeFromParent()
|
|
}
|
|
|
|
setupNoPointsListener({
|
|
sketchDetails,
|
|
afterClick,
|
|
currentTool,
|
|
}: {
|
|
sketchDetails: SketchDetails
|
|
currentTool: SketchTool
|
|
afterClick: (
|
|
args: OnClickCallbackArgs,
|
|
updatedPaths: {
|
|
sketchNodePaths: PathToNode[]
|
|
sketchEntryNodePath: PathToNode
|
|
}
|
|
) => 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.
|
|
this.currentSketchQuaternion = quaternionFromUpNForward(
|
|
new Vector3(...sketchDetails.yAxis),
|
|
new Vector3(...sketchDetails.zAxis)
|
|
)
|
|
|
|
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]))
|
|
)
|
|
this.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
|
|
)
|
|
|
|
const arrowHead = getParentGroup(args.intersects[0].object, [
|
|
ARROWHEAD,
|
|
ARC_ANGLE_END,
|
|
THREE_POINT_ARC_HANDLE3,
|
|
])
|
|
const parent = getParentGroup(
|
|
args.intersects[0].object,
|
|
SEGMENT_BODIES_PLUS_PROFILE_START
|
|
)
|
|
if (
|
|
!axisIntersection &&
|
|
!(
|
|
parent?.userData?.isLastInProfile &&
|
|
(arrowHead || parent?.name === PROFILE_START)
|
|
)
|
|
)
|
|
return
|
|
const { intersectionPoint } = args
|
|
// We're hovering over an axis, so we should show a draft point
|
|
const snappedPoint = intersectionPoint.twoD.clone()
|
|
let intersectsXY = { x: false, y: false }
|
|
args.intersects.forEach((intersect) => {
|
|
const parent = getParentGroup(intersect.object, [X_AXIS, Y_AXIS])
|
|
if (parent?.name === X_AXIS) {
|
|
intersectsXY.x = true
|
|
} else if (parent?.name === Y_AXIS) {
|
|
intersectsXY.y = true
|
|
}
|
|
})
|
|
if (intersectsXY.x && intersectsXY.y) {
|
|
snappedPoint.setComponent(0, 0)
|
|
snappedPoint.setComponent(1, 0)
|
|
} else if (intersectsXY.x) {
|
|
snappedPoint.setComponent(1, 0)
|
|
} else if (intersectsXY.y) {
|
|
snappedPoint.setComponent(0, 0)
|
|
} else if (arrowHead) {
|
|
snappedPoint.set(arrowHead.position.x, arrowHead.position.y)
|
|
} else if (parent?.name === PROFILE_START) {
|
|
snappedPoint.set(parent.position.x, parent.position.y)
|
|
}
|
|
|
|
this.positionDraftPoint({
|
|
snappedPoint,
|
|
origin: sketchDetails.origin,
|
|
yAxis: sketchDetails.yAxis,
|
|
zAxis: sketchDetails.zAxis,
|
|
})
|
|
},
|
|
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 = this.sceneInfra.camControls.getInteractionType(
|
|
args.mouseEvent
|
|
)
|
|
if (interaction !== 'none') return
|
|
if (args.mouseEvent.which !== 1) return
|
|
const { intersectionPoint } = args
|
|
if (!intersectionPoint?.twoD) return
|
|
|
|
const parent = getParentGroup(
|
|
args?.intersects?.[0]?.object,
|
|
SEGMENT_BODIES_PLUS_PROFILE_START
|
|
)
|
|
if (parent?.userData?.isLastInProfile) {
|
|
afterClick(args, {
|
|
sketchNodePaths: sketchDetails.sketchNodePaths,
|
|
sketchEntryNodePath: parent.userData.pathToNode,
|
|
})
|
|
return
|
|
} else if (currentTool === 'tangentialArc') {
|
|
toast.error(
|
|
'Tangential Arc must continue an existing profile, please click on the last segment of the profile'
|
|
)
|
|
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 inserted = insertNewStartProfileAt(
|
|
this.kclManager.ast,
|
|
sketchDetails.sketchEntryNodePath || [],
|
|
sketchDetails.sketchNodePaths,
|
|
sketchDetails.planeNodePath,
|
|
[snappedClickPoint.x, snappedClickPoint.y],
|
|
'end'
|
|
)
|
|
|
|
if (trap(inserted)) return
|
|
const { modifiedAst } = inserted
|
|
|
|
await this.kclManager.updateAst(modifiedAst, false)
|
|
|
|
// Now perform the caller-specified action
|
|
afterClick(args, {
|
|
sketchNodePaths: inserted.updatedSketchNodePaths,
|
|
sketchEntryNodePath: inserted.updatedEntryNodePath,
|
|
})
|
|
},
|
|
})
|
|
}
|
|
|
|
async setupSketch({
|
|
sketchEntryNodePath,
|
|
sketchNodePaths,
|
|
forward,
|
|
up,
|
|
position,
|
|
maybeModdedAst,
|
|
draftExpressionsIndices,
|
|
selectionRanges,
|
|
}: {
|
|
sketchEntryNodePath: PathToNode
|
|
sketchNodePaths: 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>
|
|
variableDeclarationName: string
|
|
}> {
|
|
const prepared = this.prepareTruncatedAst(sketchNodePaths, maybeModdedAst)
|
|
if (err(prepared)) {
|
|
this.tearDownSketch({ removeAxis: false })
|
|
return Promise.reject(prepared)
|
|
}
|
|
const { truncatedAst, variableDeclarationName } = prepared
|
|
|
|
const { execState } = await executeAstMock({
|
|
ast: truncatedAst,
|
|
rustContext: this.rustContext,
|
|
})
|
|
const sketchesInfo = getSketchesInfo({
|
|
sketchNodePaths,
|
|
ast: maybeModdedAst,
|
|
variables: execState.variables,
|
|
kclManager: this.kclManager,
|
|
})
|
|
|
|
const group = new Group()
|
|
position && group.position.set(...position)
|
|
group.userData = {
|
|
type: SKETCH_GROUP_SEGMENTS,
|
|
pathToNode: sketchEntryNodePath,
|
|
}
|
|
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 = this.sceneInfra.getClientSceneScaleFactor(dummy)
|
|
|
|
const callbacks: (() => SegmentOverlayPayload | null)[] = []
|
|
this.sceneInfra.pauseRendering()
|
|
this.tearDownSketch({ removeAxis: false })
|
|
|
|
for (const sketchInfo of sketchesInfo) {
|
|
const { sketch } = sketchInfo
|
|
const segPathToNode = getNodePathFromSourceRange(
|
|
maybeModdedAst,
|
|
sourceRangeFromRust(sketch.start.__geoMeta.sourceRange)
|
|
)
|
|
if (
|
|
['Circle', 'CircleThreePoint'].includes(sketch?.paths?.[0]?.type) ===
|
|
false
|
|
) {
|
|
const _profileStart = createProfileStartHandle({
|
|
from: sketch.start.from,
|
|
id: sketch.start.__geoMeta.id,
|
|
pathToNode: segPathToNode,
|
|
scale,
|
|
theme: this.sceneInfra._theme,
|
|
isDraft: false,
|
|
})
|
|
_profileStart.layers.set(SKETCH_LAYER)
|
|
_profileStart.traverse((child) => {
|
|
child.layers.set(SKETCH_LAYER)
|
|
})
|
|
if (!sketch.paths.length) {
|
|
_profileStart.userData.isLastInProfile = true
|
|
}
|
|
group.add(_profileStart)
|
|
this.activeSegments[JSON.stringify(segPathToNode)] = _profileStart
|
|
}
|
|
sketch.paths.forEach((segment, index) => {
|
|
const isLastInProfile =
|
|
index === sketch.paths.length - 1 && segment.type !== 'Circle'
|
|
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 &&
|
|
// the following line is not robust to sketches defined within a function
|
|
sketchInfo.pathToNode[1][0] === sketchEntryNodePath[1][0]
|
|
const isSelected = selectionRanges?.graphSelections.some((selection) =>
|
|
isOverlap(
|
|
selection?.codeRef?.range,
|
|
sourceRangeFromRust(segment.__geoMeta.sourceRange)
|
|
)
|
|
)
|
|
|
|
let seg: Group
|
|
const _node1 = getNodeFromPath<Node<CallExpression | CallExpressionKw>>(
|
|
maybeModdedAst,
|
|
segPathToNode,
|
|
['CallExpression', 'CallExpressionKw']
|
|
)
|
|
if (err(_node1)) {
|
|
this.tearDownSketch({ removeAxis: false })
|
|
this.sceneInfra.resumeRendering()
|
|
return
|
|
}
|
|
const callExpName = _node1.node?.callee?.name.name
|
|
|
|
const initSegment =
|
|
segment.type === 'TangentialArcTo'
|
|
? segmentUtils.tangentialArc.init
|
|
: segment.type === 'Circle'
|
|
? segmentUtils.circle.init
|
|
: segment.type === 'Arc'
|
|
? segmentUtils.arc.init
|
|
: segment.type === 'CircleThreePoint'
|
|
? segmentUtils.circleThreePoint.init
|
|
: segment.type === 'ArcThreePoint'
|
|
? segmentUtils.threePointArc.init
|
|
: segmentUtils.straight.init
|
|
const input: SegmentInputs =
|
|
segment.type === 'Circle'
|
|
? {
|
|
type: 'arc-segment',
|
|
from: segment.from,
|
|
to: segment.from,
|
|
ccw: true,
|
|
center: segment.center,
|
|
radius: segment.radius,
|
|
}
|
|
: segment.type === 'CircleThreePoint' ||
|
|
segment.type === 'ArcThreePoint'
|
|
? {
|
|
type: 'circle-three-point-segment',
|
|
p1: segment.p1,
|
|
p2: segment.p2,
|
|
p3: segment.p3,
|
|
}
|
|
: segment.type === 'Arc'
|
|
? {
|
|
type: 'arc-segment',
|
|
from: segment.from,
|
|
center: segment.center,
|
|
to: segment.to,
|
|
ccw: segment.ccw,
|
|
radius: segment.radius,
|
|
}
|
|
: {
|
|
type: 'straight-segment',
|
|
from: segment.from,
|
|
to: segment.to,
|
|
}
|
|
const startRange = _node1.node.start
|
|
const endRange = _node1.node.end
|
|
const sourceRange: SourceRange = [startRange, endRange, 0]
|
|
const selection: Selections = computeSelectionFromSourceRangeAndAST(
|
|
sourceRange,
|
|
maybeModdedAst,
|
|
this.kclManager
|
|
)
|
|
const result = initSegment({
|
|
prevSegment: sketch.paths[index - 1],
|
|
callExpName,
|
|
input,
|
|
id: segment.__geoMeta.id,
|
|
pathToNode: segPathToNode,
|
|
isDraftSegment,
|
|
scale,
|
|
texture: this.sceneInfra.extraSegmentTexture,
|
|
theme: this.sceneInfra._theme,
|
|
isSelected,
|
|
sceneInfra: this.sceneInfra,
|
|
selection,
|
|
})
|
|
if (err(result)) return
|
|
const { group: _group, updateOverlaysCallback } = result
|
|
seg = _group
|
|
if (isLastInProfile) {
|
|
seg.userData.isLastInProfile = true
|
|
}
|
|
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.setRotationFromQuaternion(
|
|
this.currentSketchQuaternion
|
|
)
|
|
position && this.intersectionPlane.position.set(...position)
|
|
this.sceneInfra.scene.add(group)
|
|
|
|
this.sceneInfra.resumeRendering()
|
|
|
|
this.sceneInfra.camControls.enableRotate = false
|
|
this.sceneInfra.overlayCallbacks(callbacks)
|
|
|
|
return {
|
|
truncatedAst,
|
|
variableDeclarationName,
|
|
}
|
|
}
|
|
|
|
updateAstAndRejigSketch = async (
|
|
sketchEntryNodePath: PathToNode,
|
|
sketchNodePaths: PathToNode[],
|
|
planeNodePath: PathToNode,
|
|
modifiedAst: Node<Program> | Error,
|
|
forward: [number, number, number],
|
|
up: [number, number, number],
|
|
origin: [number, number, number]
|
|
) => {
|
|
if (trap(modifiedAst)) return Promise.reject(modifiedAst)
|
|
const nextAst = await this.kclManager.updateAst(modifiedAst, false)
|
|
this.sceneInfra.resetMouseListeners()
|
|
await this.setupSketch({
|
|
sketchEntryNodePath,
|
|
sketchNodePaths,
|
|
forward,
|
|
up,
|
|
position: origin,
|
|
maybeModdedAst: nextAst.newAst,
|
|
})
|
|
this.setupSketchIdleCallbacks({
|
|
forward,
|
|
up,
|
|
position: origin,
|
|
sketchEntryNodePath,
|
|
sketchNodePaths,
|
|
planeNodePath,
|
|
})
|
|
return nextAst
|
|
}
|
|
didIntersectProfileStart = (
|
|
args: OnClickCallbackArgs,
|
|
nodePath: PathToNode
|
|
) => {
|
|
return args.intersects
|
|
.map(({ object }) => getParentGroup(object, [PROFILE_START]))
|
|
.find(isGroupStartProfileForCurrentProfile(nodePath))
|
|
}
|
|
setupDraftSegment = async (
|
|
sketchEntryNodePath: PathToNode,
|
|
sketchNodePaths: PathToNode[],
|
|
planeNodePath: PathToNode,
|
|
forward: [number, number, number],
|
|
up: [number, number, number],
|
|
origin: [number, number, number],
|
|
segmentName: 'line' | 'tangentialArc' = 'line',
|
|
shouldTearDown = true
|
|
) => {
|
|
const _ast = structuredClone(this.kclManager.ast)
|
|
|
|
const _node1 = getNodeFromPath<VariableDeclaration>(
|
|
_ast,
|
|
sketchEntryNodePath || [],
|
|
'VariableDeclaration'
|
|
)
|
|
if (trap(_node1)) return Promise.reject(_node1)
|
|
const variableDeclarationName = _node1.node?.declaration.id?.name || ''
|
|
|
|
const sg = sketchFromKclValue(
|
|
this.kclManager.variables[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 `.length -1`
|
|
const mod = addNewSketchLn({
|
|
node: _ast,
|
|
variables: this.kclManager.variables,
|
|
input: {
|
|
type: 'straight-segment',
|
|
to: lastSeg.to,
|
|
from: lastSeg.to,
|
|
},
|
|
fnName: segmentName,
|
|
pathToNode: sketchEntryNodePath,
|
|
})
|
|
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 }
|
|
|
|
this.sceneInfra.resetMouseListeners()
|
|
|
|
const { truncatedAst } = await this.setupSketch({
|
|
sketchEntryNodePath,
|
|
sketchNodePaths,
|
|
forward,
|
|
up,
|
|
position: origin,
|
|
maybeModdedAst: modifiedAst,
|
|
draftExpressionsIndices,
|
|
}).catch(() => {
|
|
return { truncatedAst: modifiedAst }
|
|
})
|
|
this.sceneInfra.setCallbacks({
|
|
onClick: async (args) => {
|
|
if (!args) return
|
|
// If there is a valid camera interaction that matches, do that instead
|
|
const interaction = this.sceneInfra.camControls.getInteractionType(
|
|
args.mouseEvent
|
|
)
|
|
if (interaction !== 'none') return
|
|
if (args.mouseEvent.which !== 1) return
|
|
|
|
const { intersectionPoint } = args
|
|
let intersection2d = intersectionPoint?.twoD
|
|
const intersectsProfileStart = this.didIntersectProfileStart(
|
|
args,
|
|
sketchEntryNodePath
|
|
)
|
|
|
|
let modifiedAst: Node<Program> | Error = structuredClone(
|
|
this.kclManager.ast
|
|
)
|
|
|
|
const sketch = sketchFromPathToNode({
|
|
pathToNode: sketchEntryNodePath,
|
|
ast: this.kclManager.ast,
|
|
variables: this.kclManager.variables,
|
|
kclManager: this.kclManager,
|
|
})
|
|
if (err(sketch)) return Promise.reject(sketch)
|
|
if (!sketch) return Promise.reject(new Error('No sketch found'))
|
|
|
|
// Snapping logic for the profile start handle
|
|
if (intersectsProfileStart) {
|
|
const originCoords = createArrayExpression([
|
|
createCallExpressionStdLibKw(
|
|
'profileStartX',
|
|
createPipeSubstitution(),
|
|
[]
|
|
),
|
|
createCallExpressionStdLibKw(
|
|
'profileStartY',
|
|
createPipeSubstitution(),
|
|
[]
|
|
),
|
|
])
|
|
|
|
modifiedAst = addCallExpressionsToPipe({
|
|
node: this.kclManager.ast,
|
|
variables: this.kclManager.variables,
|
|
pathToNode: sketchEntryNodePath,
|
|
expressions: [
|
|
segmentName === 'tangentialArc'
|
|
? createCallExpressionStdLibKw('tangentialArc', null, [
|
|
createLabeledArg(ARG_END_ABSOLUTE, originCoords),
|
|
])
|
|
: createCallExpressionStdLibKw('line', null, [
|
|
createLabeledArg(ARG_END_ABSOLUTE, originCoords),
|
|
]),
|
|
],
|
|
})
|
|
if (trap(modifiedAst)) return Promise.reject(modifiedAst)
|
|
modifiedAst = addCloseToPipe({
|
|
node: modifiedAst,
|
|
variables: this.kclManager.variables,
|
|
pathToNode: sketchEntryNodePath,
|
|
})
|
|
if (trap(modifiedAst)) return Promise.reject(modifiedAst)
|
|
} else if (intersection2d) {
|
|
const lastSegment = sketch.paths.slice(-1)[0] || sketch.start
|
|
|
|
let {
|
|
snappedPoint,
|
|
snappedToTangent,
|
|
intersectsXAxis,
|
|
intersectsYAxis,
|
|
negativeTangentDirection,
|
|
} = this.getSnappedDragPoint(
|
|
intersection2d,
|
|
args.intersects,
|
|
args.mouseEvent,
|
|
Object.values(this.activeSegments).at(-1)
|
|
)
|
|
|
|
// Get the angle between the previous segment (or sketch start)'s end and this one's
|
|
const angle = Math.atan2(
|
|
snappedPoint[1] - lastSegment.to[1],
|
|
snappedPoint[0] - 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'
|
|
const snaps = {
|
|
previousArcTag: '',
|
|
negativeTangentDirection,
|
|
xAxis: !!intersectsXAxis,
|
|
yAxis: !!intersectsYAxis,
|
|
}
|
|
|
|
// This might need to become its own function if we want more
|
|
// case-based logic for different segment types
|
|
if (
|
|
(lastSegment.type === 'TangentialArc' && segmentName !== 'line') ||
|
|
segmentName === 'tangentialArc'
|
|
) {
|
|
resolvedFunctionName = 'tangentialArc'
|
|
} else if (snappedToTangent) {
|
|
// Generate tag for previous arc segment and use it for the angle of angledLine:
|
|
// |> tangentialArcTo([5, -10], %, $arc001)
|
|
// |> angledLine({ angle = tangentToEnd(arc001), length = 12 }, %)
|
|
|
|
const previousSegmentPathToNode = getNodePathFromSourceRange(
|
|
modifiedAst,
|
|
sourceRangeFromRust(lastSegment.__geoMeta.sourceRange)
|
|
)
|
|
const taggedAstResult = mutateAstWithTagForSketchSegment(
|
|
modifiedAst,
|
|
previousSegmentPathToNode
|
|
)
|
|
if (trap(taggedAstResult)) return Promise.reject(taggedAstResult)
|
|
|
|
modifiedAst = taggedAstResult.modifiedAst
|
|
snaps.previousArcTag = taggedAstResult.tag
|
|
resolvedFunctionName = 'angledLine'
|
|
} 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[0] === 0 || snappedPoint[1] === 0) {
|
|
// We consider a point placed on axes or origin to be absolute
|
|
resolvedFunctionName = 'lineTo'
|
|
}
|
|
|
|
const tmp = addNewSketchLn({
|
|
node: modifiedAst,
|
|
variables: this.kclManager.variables,
|
|
input: {
|
|
type: 'straight-segment',
|
|
from: [lastSegment.to[0], lastSegment.to[1]],
|
|
to: [snappedPoint[0], snappedPoint[1]],
|
|
},
|
|
fnName: resolvedFunctionName,
|
|
pathToNode: sketchEntryNodePath,
|
|
snaps,
|
|
})
|
|
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 updateModelingState(modifiedAst, EXECUTION_TYPE_MOCK, {
|
|
kclManager: this.kclManager,
|
|
editorManager: this.editorManager,
|
|
codeManager: this.codeManager,
|
|
})
|
|
|
|
if (intersectsProfileStart) {
|
|
this.sceneInfra.modelingSend({ type: 'Close sketch' })
|
|
} else {
|
|
await this.setupDraftSegment(
|
|
sketchEntryNodePath,
|
|
sketchNodePaths,
|
|
planeNodePath,
|
|
forward,
|
|
up,
|
|
origin,
|
|
segmentName
|
|
)
|
|
}
|
|
},
|
|
onMove: (args) => {
|
|
const expressionIndex = Number(sketchEntryNodePath[1][0])
|
|
const activeSegmentsInCorrectExpression = Object.values(
|
|
this.activeSegments
|
|
).filter((seg) => {
|
|
return seg.userData.pathToNode[1][0] === expressionIndex
|
|
})
|
|
const object =
|
|
activeSegmentsInCorrectExpression[
|
|
activeSegmentsInCorrectExpression.length - 1
|
|
]
|
|
this.onDragSegment({
|
|
intersection2d: args.intersectionPoint.twoD,
|
|
object,
|
|
intersects: args.intersects,
|
|
sketchNodePaths,
|
|
sketchEntryNodePath,
|
|
draftInfo: {
|
|
truncatedAst,
|
|
variableDeclarationName,
|
|
},
|
|
mouseEvent: args.mouseEvent,
|
|
})
|
|
},
|
|
})
|
|
}
|
|
setupDraftRectangle = async (
|
|
sketchEntryNodePath: PathToNode,
|
|
sketchNodePaths: PathToNode[],
|
|
planeNodePath: PathToNode,
|
|
forward: [number, number, number],
|
|
up: [number, number, number],
|
|
sketchOrigin: [number, number, number],
|
|
rectangleOrigin: [x: number, y: number]
|
|
): Promise<SketchDetailsUpdate | Error> => {
|
|
let _ast = structuredClone(this.kclManager.ast)
|
|
|
|
const varDec = getNodeFromPath<VariableDeclarator>(
|
|
_ast,
|
|
planeNodePath,
|
|
'VariableDeclarator'
|
|
)
|
|
|
|
if (err(varDec)) return varDec
|
|
if (varDec.node.type !== 'VariableDeclarator') return new Error('not a var')
|
|
|
|
const varName = findUniqueName(_ast, 'profile')
|
|
|
|
// first create just the variable declaration, as that's
|
|
// all we want the user to see in the editor
|
|
const tag = findUniqueName(_ast, 'rectangleSegmentA')
|
|
const newDeclaration = createVariableDeclaration(
|
|
varName,
|
|
createCallExpressionStdLibKw(
|
|
'startProfile',
|
|
createLocalName(varDec.node.id.name),
|
|
[
|
|
createLabeledArg(
|
|
ARG_AT,
|
|
createArrayExpression([
|
|
createLiteral(roundOff(rectangleOrigin[0])),
|
|
createLiteral(roundOff(rectangleOrigin[1])),
|
|
])
|
|
),
|
|
]
|
|
)
|
|
)
|
|
|
|
const insertIndex = getInsertIndex(sketchNodePaths, planeNodePath, 'end')
|
|
|
|
_ast.body.splice(insertIndex, 0, newDeclaration)
|
|
const { updatedEntryNodePath, updatedSketchNodePaths } =
|
|
updateSketchNodePathsWithInsertIndex({
|
|
insertIndex,
|
|
insertType: 'end',
|
|
sketchNodePaths,
|
|
})
|
|
|
|
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
|
|
const didReParse = await this.kclManager.executeAstMock(_ast)
|
|
if (err(didReParse)) return didReParse
|
|
|
|
const justCreatedNode = getNodeFromPath<VariableDeclaration>(
|
|
_ast,
|
|
updatedEntryNodePath,
|
|
'VariableDeclaration'
|
|
)
|
|
|
|
if (trap(justCreatedNode)) return Promise.reject(justCreatedNode)
|
|
const startProfileAt = justCreatedNode.node?.declaration
|
|
// than add the rest of the profile so we can "animate" it
|
|
// as draft segments
|
|
startProfileAt.init = createPipeExpression([
|
|
startProfileAt?.init,
|
|
...getRectangleCallExpressions(rectangleOrigin, tag),
|
|
])
|
|
|
|
const code = recast(_ast)
|
|
const _recastAst = parse(code)
|
|
if (trap(_recastAst) || !resultIsOk(_recastAst))
|
|
return Promise.reject(_recastAst)
|
|
_ast = _recastAst.program
|
|
|
|
const { truncatedAst } = await this.setupSketch({
|
|
sketchEntryNodePath: updatedEntryNodePath,
|
|
sketchNodePaths: updatedSketchNodePaths,
|
|
forward,
|
|
up,
|
|
position: sketchOrigin,
|
|
maybeModdedAst: _ast,
|
|
draftExpressionsIndices: { start: 0, end: 3 },
|
|
})
|
|
|
|
this.sceneInfra.setCallbacks({
|
|
onMove: async (args) => {
|
|
// Update the width and height of the draft rectangle
|
|
|
|
const nodePathWithCorrectedIndexForTruncatedAst =
|
|
structuredClone(updatedEntryNodePath)
|
|
nodePathWithCorrectedIndexForTruncatedAst[1][0] =
|
|
Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) -
|
|
Number(planeNodePath[1][0]) -
|
|
1
|
|
|
|
const _node = getNodeFromPath<VariableDeclaration>(
|
|
truncatedAst,
|
|
nodePathWithCorrectedIndexForTruncatedAst,
|
|
'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, tag)
|
|
}
|
|
|
|
const { execState } = await executeAstMock({
|
|
ast: truncatedAst,
|
|
rustContext: this.rustContext,
|
|
})
|
|
const sketch = sketchFromKclValue(execState.variables[varName], varName)
|
|
if (err(sketch)) return Promise.reject(sketch)
|
|
const sgPaths = sketch.paths
|
|
const orthoFactor = orthoScale(this.sceneInfra.camControls.camera)
|
|
|
|
const varDecIndex = Number(updatedEntryNodePath[1][0])
|
|
|
|
this.updateSegment(
|
|
sketch.start,
|
|
0,
|
|
varDecIndex,
|
|
_ast,
|
|
orthoFactor,
|
|
sketch
|
|
)
|
|
sgPaths.forEach((seg, index) =>
|
|
this.updateSegment(seg, index, varDecIndex, _ast, orthoFactor, sketch)
|
|
)
|
|
|
|
const { intersectionPoint } = args
|
|
if (!intersectionPoint?.twoD) return
|
|
const { snappedPoint, isSnapped } = this.getSnappedDragPoint(
|
|
intersectionPoint.twoD,
|
|
args.intersects,
|
|
args.mouseEvent
|
|
)
|
|
if (isSnapped) {
|
|
this.positionDraftPoint({
|
|
snappedPoint: new Vector2(...snappedPoint),
|
|
origin: sketchOrigin,
|
|
yAxis: forward,
|
|
zAxis: up,
|
|
})
|
|
} else {
|
|
this.removeDraftPoint()
|
|
}
|
|
},
|
|
onClick: async (args) => {
|
|
// If there is a valid camera interaction that matches, do that instead
|
|
const interaction = this.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,
|
|
updatedEntryNodePath,
|
|
'VariableDeclaration'
|
|
)
|
|
if (trap(_node)) return
|
|
const sketchInit = _node.node?.declaration.init
|
|
|
|
if (sketchInit.type !== 'PipeExpression') {
|
|
return
|
|
}
|
|
|
|
updateRectangleSketch(sketchInit, x, y, tag)
|
|
|
|
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
|
|
//
|
|
// 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 updateModelingState(_ast, EXECUTION_TYPE_MOCK, {
|
|
kclManager: this.kclManager,
|
|
editorManager: this.editorManager,
|
|
codeManager: this.codeManager,
|
|
})
|
|
this.sceneInfra.modelingSend({ type: 'Finish rectangle' })
|
|
},
|
|
})
|
|
return {
|
|
updatedEntryNodePath,
|
|
updatedSketchNodePaths,
|
|
expressionIndexToDelete: insertIndex,
|
|
}
|
|
}
|
|
setupDraftCenterRectangle = async (
|
|
sketchEntryNodePath: PathToNode,
|
|
sketchNodePaths: PathToNode[],
|
|
planeNodePath: PathToNode,
|
|
forward: [number, number, number],
|
|
up: [number, number, number],
|
|
sketchOrigin: [number, number, number],
|
|
rectangleOrigin: [x: number, y: number]
|
|
): Promise<SketchDetailsUpdate | Error> => {
|
|
let _ast = structuredClone(this.kclManager.ast)
|
|
|
|
const varDec = getNodeFromPath<VariableDeclarator>(
|
|
_ast,
|
|
planeNodePath,
|
|
'VariableDeclarator'
|
|
)
|
|
|
|
if (err(varDec)) return varDec
|
|
if (varDec.node.type !== 'VariableDeclarator') return new Error('not a var')
|
|
|
|
const varName = findUniqueName(_ast, 'profile')
|
|
// first create just the variable declaration, as that's
|
|
// all we want the user to see in the editor
|
|
const tag = findUniqueName(_ast, 'rectangleSegmentA')
|
|
|
|
const newDeclaration = createVariableDeclaration(
|
|
varName,
|
|
createCallExpressionStdLibKw(
|
|
'startProfile',
|
|
createLocalName(varDec.node.id.name),
|
|
[
|
|
createLabeledArg(
|
|
ARG_AT,
|
|
createArrayExpression([
|
|
createLiteral(roundOff(rectangleOrigin[0])),
|
|
createLiteral(roundOff(rectangleOrigin[1])),
|
|
])
|
|
),
|
|
]
|
|
)
|
|
)
|
|
const insertIndex = getInsertIndex(sketchNodePaths, planeNodePath, 'end')
|
|
|
|
_ast.body.splice(insertIndex, 0, newDeclaration)
|
|
const { updatedEntryNodePath, updatedSketchNodePaths } =
|
|
updateSketchNodePathsWithInsertIndex({
|
|
insertIndex,
|
|
insertType: 'end',
|
|
sketchNodePaths,
|
|
})
|
|
|
|
let __recastAst = parse(recast(_ast))
|
|
if (trap(__recastAst) || !resultIsOk(__recastAst))
|
|
return Promise.reject(__recastAst)
|
|
_ast = __recastAst.program
|
|
|
|
// do a quick mock execution to get the program memory up-to-date
|
|
await this.kclManager.executeAstMock(_ast)
|
|
|
|
const justCreatedNode = getNodeFromPath<VariableDeclaration>(
|
|
_ast,
|
|
updatedEntryNodePath,
|
|
'VariableDeclaration'
|
|
)
|
|
|
|
if (trap(justCreatedNode)) return Promise.reject(justCreatedNode)
|
|
const startProfileAt = justCreatedNode.node?.declaration
|
|
// than add the rest of the profile so we can "animate" it
|
|
// as draft segments
|
|
startProfileAt.init = createPipeExpression([
|
|
startProfileAt?.init,
|
|
...getRectangleCallExpressions(rectangleOrigin, tag),
|
|
])
|
|
const code = recast(_ast)
|
|
__recastAst = parse(code)
|
|
if (trap(__recastAst) || !resultIsOk(__recastAst))
|
|
return Promise.reject(__recastAst)
|
|
_ast = __recastAst.program
|
|
|
|
const { truncatedAst } = await this.setupSketch({
|
|
sketchEntryNodePath: updatedEntryNodePath,
|
|
sketchNodePaths: updatedSketchNodePaths,
|
|
forward,
|
|
up,
|
|
position: sketchOrigin,
|
|
maybeModdedAst: _ast,
|
|
draftExpressionsIndices: { start: 0, end: 3 },
|
|
})
|
|
|
|
this.sceneInfra.setCallbacks({
|
|
onMove: async (args) => {
|
|
// Update the width and height of the draft rectangle
|
|
|
|
const nodePathWithCorrectedIndexForTruncatedAst =
|
|
structuredClone(updatedEntryNodePath)
|
|
nodePathWithCorrectedIndexForTruncatedAst[1][0] =
|
|
Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) -
|
|
Number(planeNodePath[1][0]) -
|
|
1
|
|
|
|
const _node = getNodeFromPath<VariableDeclaration>(
|
|
truncatedAst,
|
|
nodePathWithCorrectedIndexForTruncatedAst,
|
|
'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') {
|
|
const maybeError = updateCenterRectangleSketch(
|
|
sketchInit,
|
|
x,
|
|
y,
|
|
tag,
|
|
rectangleOrigin[0],
|
|
rectangleOrigin[1]
|
|
)
|
|
if (err(maybeError)) {
|
|
return Promise.reject(maybeError)
|
|
}
|
|
}
|
|
|
|
const { execState } = await executeAstMock({
|
|
ast: truncatedAst,
|
|
rustContext: this.rustContext,
|
|
})
|
|
const sketch = sketchFromKclValue(execState.variables[varName], varName)
|
|
if (err(sketch)) return Promise.reject(sketch)
|
|
const sgPaths = sketch.paths
|
|
const orthoFactor = orthoScale(this.sceneInfra.camControls.camera)
|
|
|
|
const varDecIndex = Number(updatedEntryNodePath[1][0])
|
|
|
|
this.updateSegment(
|
|
sketch.start,
|
|
0,
|
|
varDecIndex,
|
|
_ast,
|
|
orthoFactor,
|
|
sketch
|
|
)
|
|
sgPaths.forEach((seg, index) =>
|
|
this.updateSegment(seg, index, varDecIndex, _ast, orthoFactor, sketch)
|
|
)
|
|
},
|
|
onClick: async (args) => {
|
|
// If there is a valid camera interaction that matches, do that instead
|
|
const interaction = this.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,
|
|
updatedEntryNodePath,
|
|
'VariableDeclaration'
|
|
)
|
|
if (trap(_node)) return
|
|
const sketchInit = _node.node?.declaration.init
|
|
|
|
if (sketchInit.type === 'PipeExpression') {
|
|
const maybeError = updateCenterRectangleSketch(
|
|
sketchInit,
|
|
x,
|
|
y,
|
|
tag,
|
|
rectangleOrigin[0],
|
|
rectangleOrigin[1]
|
|
)
|
|
if (err(maybeError)) {
|
|
return Promise.reject(maybeError)
|
|
}
|
|
|
|
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
|
|
//
|
|
// 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 updateModelingState(_ast, EXECUTION_TYPE_MOCK, {
|
|
kclManager: this.kclManager,
|
|
editorManager: this.editorManager,
|
|
codeManager: this.codeManager,
|
|
})
|
|
this.sceneInfra.modelingSend({ type: 'Finish center rectangle' })
|
|
}
|
|
},
|
|
})
|
|
return {
|
|
updatedEntryNodePath,
|
|
updatedSketchNodePaths,
|
|
expressionIndexToDelete: insertIndex,
|
|
}
|
|
}
|
|
setupDraftCircleThreePoint = async (
|
|
sketchEntryNodePath: PathToNode,
|
|
sketchNodePaths: PathToNode[],
|
|
planeNodePath: PathToNode,
|
|
forward: [number, number, number],
|
|
up: [number, number, number],
|
|
sketchOrigin: [number, number, number],
|
|
point1: [x: number, y: number],
|
|
point2: [x: number, y: number]
|
|
): Promise<SketchDetailsUpdate | Error> => {
|
|
let _ast = structuredClone(this.kclManager.ast)
|
|
|
|
const varDec = getNodeFromPath<VariableDeclarator>(
|
|
_ast,
|
|
planeNodePath,
|
|
'VariableDeclarator'
|
|
)
|
|
|
|
if (err(varDec)) return varDec
|
|
if (varDec.node.type !== 'VariableDeclarator') return new Error('not a var')
|
|
|
|
const varName = findUniqueName(_ast, 'profile')
|
|
|
|
const thirdPointCloseToWhereUserLastClicked = `[${roundOff(
|
|
point2[0] + 0.1,
|
|
2
|
|
)}, ${roundOff(point2[1] + 0.1, 2)}]`
|
|
const newExpression = createNodeFromExprSnippet`${varName} = circleThreePoint(
|
|
${varDec.node.id.name},
|
|
p1 = [${roundOff(point1[0], 2)}, ${roundOff(point1[1], 2)}],
|
|
p2 = [${roundOff(point2[0], 2)}, ${roundOff(point2[1], 2)}],
|
|
p3 = ${thirdPointCloseToWhereUserLastClicked},
|
|
)`
|
|
if (err(newExpression)) return newExpression
|
|
const insertIndex = getInsertIndex(sketchNodePaths, planeNodePath, 'end')
|
|
|
|
_ast.body.splice(insertIndex, 0, newExpression)
|
|
const { updatedEntryNodePath, updatedSketchNodePaths } =
|
|
updateSketchNodePathsWithInsertIndex({
|
|
insertIndex,
|
|
insertType: 'end',
|
|
sketchNodePaths,
|
|
})
|
|
|
|
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
|
|
const didReParse = await this.kclManager.executeAstMock(_ast)
|
|
if (err(didReParse)) return didReParse
|
|
|
|
const { truncatedAst } = await this.setupSketch({
|
|
sketchEntryNodePath: updatedEntryNodePath,
|
|
sketchNodePaths: updatedSketchNodePaths,
|
|
forward,
|
|
up,
|
|
position: sketchOrigin,
|
|
maybeModdedAst: _ast,
|
|
draftExpressionsIndices: { start: 0, end: 0 },
|
|
})
|
|
|
|
this.sceneInfra.setCallbacks({
|
|
onMove: async (args) => {
|
|
const firstProfileIndex = Number(updatedSketchNodePaths[0][1][0])
|
|
const nodePathWithCorrectedIndexForTruncatedAst =
|
|
structuredClone(updatedEntryNodePath)
|
|
|
|
nodePathWithCorrectedIndexForTruncatedAst[1][0] =
|
|
Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) -
|
|
firstProfileIndex
|
|
const _node = getNodeFromPath<VariableDeclaration>(
|
|
truncatedAst,
|
|
nodePathWithCorrectedIndexForTruncatedAst,
|
|
'VariableDeclaration'
|
|
)
|
|
let modded = structuredClone(truncatedAst)
|
|
if (trap(_node)) return
|
|
const sketchInit = _node.node.declaration.init
|
|
|
|
if (sketchInit.type === 'CallExpressionKw') {
|
|
const moddedResult = changeSketchArguments(
|
|
modded,
|
|
this.kclManager.variables,
|
|
{
|
|
type: 'path',
|
|
pathToNode: nodePathWithCorrectedIndexForTruncatedAst,
|
|
},
|
|
{
|
|
type: 'circle-three-point-segment',
|
|
p1: [point1[0], point1[1]],
|
|
p2: [point2[0], point2[1]],
|
|
p3: [
|
|
args.intersectionPoint.twoD.x,
|
|
args.intersectionPoint.twoD.y,
|
|
],
|
|
}
|
|
)
|
|
if (err(moddedResult)) return
|
|
modded = moddedResult.modifiedAst
|
|
}
|
|
|
|
const { execState } = await executeAstMock({
|
|
ast: modded,
|
|
rustContext: this.rustContext,
|
|
})
|
|
const sketch = sketchFromKclValue(execState.variables[varName], varName)
|
|
if (err(sketch)) return
|
|
const sgPaths = sketch.paths
|
|
const orthoFactor = orthoScale(this.sceneInfra.camControls.camera)
|
|
|
|
const varDecIndex = Number(updatedEntryNodePath[1][0])
|
|
|
|
this.updateSegment(
|
|
sketch.start,
|
|
0,
|
|
varDecIndex,
|
|
_ast,
|
|
orthoFactor,
|
|
sketch
|
|
)
|
|
sgPaths.forEach((seg, index) =>
|
|
this.updateSegment(seg, index, varDecIndex, _ast, orthoFactor, sketch)
|
|
)
|
|
},
|
|
onClick: async (args) => {
|
|
// If there is a valid camera interaction that matches, do that instead
|
|
const interaction = this.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 _node = getNodeFromPath<VariableDeclaration>(
|
|
_ast,
|
|
updatedEntryNodePath || [],
|
|
'VariableDeclaration'
|
|
)
|
|
if (trap(_node)) return
|
|
const sketchInit = _node.node?.declaration.init
|
|
|
|
let modded = structuredClone(_ast)
|
|
if (sketchInit.type === 'CallExpressionKw') {
|
|
const moddedResult = changeSketchArguments(
|
|
modded,
|
|
this.kclManager.variables,
|
|
{
|
|
type: 'path',
|
|
pathToNode: updatedEntryNodePath,
|
|
},
|
|
{
|
|
type: 'circle-three-point-segment',
|
|
p1: [point1[0], point1[1]],
|
|
p2: [point2[0], point2[1]],
|
|
p3: [cornerPoint.x || 0, cornerPoint.y || 0],
|
|
}
|
|
)
|
|
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 updateModelingState(_ast, EXECUTION_TYPE_MOCK, {
|
|
kclManager: this.kclManager,
|
|
editorManager: this.editorManager,
|
|
codeManager: this.codeManager,
|
|
})
|
|
this.sceneInfra.modelingSend({ type: 'Finish circle three point' })
|
|
}
|
|
},
|
|
})
|
|
return {
|
|
updatedEntryNodePath,
|
|
updatedSketchNodePaths,
|
|
expressionIndexToDelete: insertIndex,
|
|
}
|
|
}
|
|
setupDraftArc = async (
|
|
sketchEntryNodePath: PathToNode,
|
|
sketchNodePaths: PathToNode[],
|
|
planeNodePath: PathToNode,
|
|
forward: [number, number, number],
|
|
up: [number, number, number],
|
|
sketchOrigin: [number, number, number],
|
|
center: [x: number, y: number]
|
|
): Promise<SketchDetailsUpdate | Error> => {
|
|
let _ast = structuredClone(this.kclManager.ast)
|
|
|
|
const _node1 = getNodeFromPath<VariableDeclaration>(
|
|
_ast,
|
|
sketchEntryNodePath || [],
|
|
'VariableDeclaration'
|
|
)
|
|
if (trap(_node1)) return Promise.reject(_node1)
|
|
const variableDeclarationName = _node1.node?.declaration.id?.name || ''
|
|
|
|
const sg = sketchFromKclValue(
|
|
this.kclManager.variables[variableDeclarationName],
|
|
variableDeclarationName
|
|
)
|
|
if (err(sg)) return Promise.reject(sg)
|
|
const lastSeg = sg?.paths?.slice(-1)[0] || sg.start
|
|
|
|
// Calculate a default center point and radius based on the last segment's endpoint
|
|
const from: [number, number] = [lastSeg.to[0], lastSeg.to[1]]
|
|
const radius = Math.sqrt(
|
|
(center[0] - from[0]) ** 2 + (center[1] - from[1]) ** 2
|
|
)
|
|
const startAngle = Math.atan2(from[1] - center[1], from[0] - center[0])
|
|
const endAngle = startAngle + Math.PI / 180 // arbitrary 1 degree arc as starting default
|
|
const to: [number, number] = [
|
|
center[0] + radius * Math.cos(endAngle),
|
|
center[1] + radius * Math.sin(endAngle),
|
|
]
|
|
|
|
// Use addNewSketchLn to append an arc to the existing sketch
|
|
const mod = addNewSketchLn({
|
|
node: _ast,
|
|
variables: this.kclManager.variables,
|
|
input: {
|
|
type: 'arc-segment',
|
|
from,
|
|
to,
|
|
center,
|
|
radius,
|
|
ccw: true,
|
|
},
|
|
fnName: 'arc' as ToolTip,
|
|
pathToNode: sketchEntryNodePath,
|
|
})
|
|
|
|
if (trap(mod)) return Promise.reject(mod)
|
|
const pResult = parse(recast(mod.modifiedAst))
|
|
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
|
|
const didReParse = await this.kclManager.executeAstMock(_ast)
|
|
if (err(didReParse)) return didReParse
|
|
|
|
const index = sg.paths.length // because we've added a new segment that's not in the memory yet
|
|
const draftExpressionsIndices = { start: index, end: index }
|
|
|
|
this.sceneInfra.resetMouseListeners()
|
|
|
|
const { truncatedAst } = await this.setupSketch({
|
|
sketchEntryNodePath,
|
|
sketchNodePaths,
|
|
forward,
|
|
up,
|
|
position: sketchOrigin,
|
|
maybeModdedAst: _ast,
|
|
draftExpressionsIndices,
|
|
})
|
|
|
|
this.sceneInfra.setCallbacks({
|
|
onMove: async (args) => {
|
|
const firstProfileIndex = Number(sketchNodePaths[0][1][0])
|
|
const nodePathWithCorrectedIndexForTruncatedAst = structuredClone(
|
|
mod.pathToNode
|
|
)
|
|
|
|
nodePathWithCorrectedIndexForTruncatedAst[1][0] =
|
|
Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) -
|
|
firstProfileIndex
|
|
const _node = getNodeFromPath<VariableDeclaration>(
|
|
truncatedAst,
|
|
nodePathWithCorrectedIndexForTruncatedAst,
|
|
'VariableDeclaration'
|
|
)
|
|
let modded = structuredClone(truncatedAst)
|
|
if (trap(_node)) return
|
|
const sketchInit = _node.node.declaration.init
|
|
|
|
if (sketchInit.type === 'PipeExpression') {
|
|
// Calculate end angle based on mouse position
|
|
const endAngle = Math.atan2(
|
|
args.intersectionPoint.twoD.y - center[1],
|
|
args.intersectionPoint.twoD.x - center[0]
|
|
)
|
|
|
|
// Calculate the new 'to' point using the existing radius and the new end angle
|
|
const newTo: [number, number] = [
|
|
center[0] + radius * Math.cos(endAngle),
|
|
center[1] + radius * Math.sin(endAngle),
|
|
]
|
|
|
|
const moddedResult = changeSketchArguments(
|
|
modded,
|
|
this.kclManager.variables,
|
|
{
|
|
type: 'path',
|
|
pathToNode: nodePathWithCorrectedIndexForTruncatedAst,
|
|
},
|
|
{
|
|
type: 'arc-segment',
|
|
from: lastSeg.to,
|
|
to: newTo,
|
|
center: center,
|
|
radius: radius,
|
|
ccw: true,
|
|
}
|
|
)
|
|
if (err(moddedResult)) return
|
|
modded = moddedResult.modifiedAst
|
|
}
|
|
const { execState } = await executeAstMock({
|
|
ast: modded,
|
|
rustContext: this.rustContext,
|
|
})
|
|
const sketch = sketchFromKclValue(
|
|
execState.variables[variableDeclarationName],
|
|
variableDeclarationName
|
|
)
|
|
if (err(sketch)) return
|
|
const sgPaths = sketch.paths
|
|
const orthoFactor = orthoScale(this.sceneInfra.camControls.camera)
|
|
|
|
const varDecIndex = Number(sketchEntryNodePath[1][0])
|
|
|
|
this.updateSegment(
|
|
sketch.start,
|
|
0,
|
|
varDecIndex,
|
|
_ast,
|
|
orthoFactor,
|
|
sketch
|
|
)
|
|
sgPaths.forEach((seg, index) =>
|
|
this.updateSegment(seg, index, varDecIndex, _ast, orthoFactor, sketch)
|
|
)
|
|
},
|
|
onClick: async (args) => {
|
|
const firstProfileIndex = Number(sketchNodePaths[0][1][0])
|
|
const nodePathWithCorrectedIndexForTruncatedAst = structuredClone(
|
|
mod.pathToNode
|
|
)
|
|
|
|
nodePathWithCorrectedIndexForTruncatedAst[1][0] =
|
|
Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) -
|
|
firstProfileIndex
|
|
// If there is a valid camera interaction that matches, do that instead
|
|
const interaction = this.sceneInfra.camControls.getInteractionType(
|
|
args.mouseEvent
|
|
)
|
|
if (interaction !== 'none') return
|
|
// Commit the arc to the full AST/code and return to sketch.idle
|
|
const mousePoint = args.intersectionPoint?.twoD
|
|
if (!mousePoint || args.mouseEvent.button !== 0) return
|
|
|
|
const _node = getNodeFromPath<VariableDeclaration>(
|
|
_ast,
|
|
sketchEntryNodePath || [],
|
|
'VariableDeclaration'
|
|
)
|
|
if (trap(_node)) return
|
|
const sketchInit = _node.node?.declaration.init
|
|
|
|
let modded = structuredClone(_ast)
|
|
if (sketchInit.type === 'PipeExpression') {
|
|
// Calculate end angle based on final mouse position
|
|
const endAngle = Math.atan2(
|
|
mousePoint.y - center[1],
|
|
mousePoint.x - center[0]
|
|
)
|
|
|
|
// Calculate the final 'to' point using the existing radius and the final end angle
|
|
const finalTo: [number, number] = [
|
|
center[0] + radius * Math.cos(endAngle),
|
|
center[1] + radius * Math.sin(endAngle),
|
|
]
|
|
|
|
const moddedResult = changeSketchArguments(
|
|
modded,
|
|
this.kclManager.variables,
|
|
{
|
|
type: 'path',
|
|
pathToNode: mod.pathToNode,
|
|
},
|
|
{
|
|
type: 'arc-segment',
|
|
from: lastSeg.to,
|
|
to: finalTo,
|
|
center: center,
|
|
radius: radius,
|
|
ccw: true,
|
|
}
|
|
)
|
|
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 arc tool
|
|
await updateModelingState(_ast, EXECUTION_TYPE_MOCK, {
|
|
kclManager: this.kclManager,
|
|
editorManager: this.editorManager,
|
|
codeManager: this.codeManager,
|
|
})
|
|
this.sceneInfra.modelingSend({ type: 'Finish arc' })
|
|
}
|
|
},
|
|
})
|
|
return {
|
|
updatedEntryNodePath: sketchEntryNodePath,
|
|
updatedSketchNodePaths: sketchNodePaths,
|
|
expressionIndexToDelete: -1, // No need to delete any expression
|
|
}
|
|
}
|
|
setupDraftArcThreePoint = async (
|
|
sketchEntryNodePath: PathToNode,
|
|
sketchNodePaths: PathToNode[],
|
|
planeNodePath: PathToNode,
|
|
forward: [number, number, number],
|
|
up: [number, number, number],
|
|
sketchOrigin: [number, number, number],
|
|
p2: [x: number, y: number]
|
|
): Promise<SketchDetailsUpdate | Error> => {
|
|
let _ast = structuredClone(this.kclManager.ast)
|
|
|
|
const _node1 = getNodeFromPath<VariableDeclaration>(
|
|
_ast,
|
|
sketchEntryNodePath || [],
|
|
'VariableDeclaration'
|
|
)
|
|
if (trap(_node1)) return Promise.reject(_node1)
|
|
const variableDeclarationName = _node1.node?.declaration.id?.name || ''
|
|
|
|
const sg = sketchFromKclValue(
|
|
this.kclManager.variables[variableDeclarationName],
|
|
variableDeclarationName
|
|
)
|
|
if (err(sg)) return Promise.reject(sg)
|
|
const lastSeg = sg?.paths?.slice(-1)[0] || sg.start
|
|
|
|
// Calculate a default center point and radius based on the last segment's endpoint
|
|
const p1: [number, number] = [lastSeg.to[0], lastSeg.to[1]]
|
|
const p3: [number, number] = [p2[0] + 0.1, p2[1] + 0.1]
|
|
|
|
// Use addNewSketchLn to append an arc to the existing sketch
|
|
const mod = addNewSketchLn({
|
|
node: _ast,
|
|
variables: this.kclManager.variables,
|
|
input: {
|
|
type: 'circle-three-point-segment',
|
|
p1,
|
|
p2,
|
|
p3,
|
|
},
|
|
fnName: 'arcTo',
|
|
pathToNode: sketchEntryNodePath,
|
|
})
|
|
|
|
if (trap(mod)) return Promise.reject(mod)
|
|
const pResult = parse(recast(mod.modifiedAst))
|
|
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
|
|
const didReParse = await this.kclManager.executeAstMock(_ast)
|
|
if (err(didReParse)) return didReParse
|
|
|
|
const index = sg.paths.length // because we've added a new segment that's not in the memory yet
|
|
const draftExpressionsIndices = { start: index, end: index }
|
|
|
|
// Get the insertion index from the modified path
|
|
const insertIndex = Number(mod.pathToNode[1][0])
|
|
|
|
this.sceneInfra.resetMouseListeners()
|
|
|
|
const { truncatedAst } = await this.setupSketch({
|
|
sketchEntryNodePath,
|
|
sketchNodePaths,
|
|
forward,
|
|
up,
|
|
position: sketchOrigin,
|
|
maybeModdedAst: _ast,
|
|
draftExpressionsIndices,
|
|
})
|
|
|
|
const doNotSnapAsThreePointArcIsTheOnlySegment = sg.paths.length === 0
|
|
|
|
this.sceneInfra.setCallbacks({
|
|
onMove: async (args) => {
|
|
const firstProfileIndex = Number(sketchNodePaths[0][1][0])
|
|
const nodePathWithCorrectedIndexForTruncatedAst = structuredClone(
|
|
mod.pathToNode
|
|
)
|
|
|
|
nodePathWithCorrectedIndexForTruncatedAst[1][0] =
|
|
Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) -
|
|
firstProfileIndex
|
|
const _node = getNodeFromPath<VariableDeclaration>(
|
|
truncatedAst,
|
|
nodePathWithCorrectedIndexForTruncatedAst,
|
|
'VariableDeclaration'
|
|
)
|
|
let modded = structuredClone(truncatedAst)
|
|
if (trap(_node)) return
|
|
const sketchInit = _node.node.declaration.init
|
|
|
|
const maybeSnapToAxis = this.getSnappedDragPoint(
|
|
args.intersectionPoint.twoD,
|
|
args.intersects,
|
|
args.mouseEvent
|
|
).snappedPoint
|
|
|
|
const maybeSnapToProfileStart = doNotSnapAsThreePointArcIsTheOnlySegment
|
|
? new Vector2(...maybeSnapToAxis)
|
|
: this.maybeSnapProfileStartIntersect2d({
|
|
sketchEntryNodePath,
|
|
intersects: args.intersects,
|
|
intersection2d: new Vector2(...maybeSnapToAxis),
|
|
})
|
|
|
|
if (sketchInit.type === 'PipeExpression') {
|
|
const moddedResult = changeSketchArguments(
|
|
modded,
|
|
this.kclManager.variables,
|
|
{
|
|
type: 'path',
|
|
pathToNode: nodePathWithCorrectedIndexForTruncatedAst,
|
|
},
|
|
{
|
|
type: 'circle-three-point-segment',
|
|
p1,
|
|
p2,
|
|
p3: [maybeSnapToProfileStart.x, maybeSnapToProfileStart.y],
|
|
}
|
|
)
|
|
if (err(moddedResult)) return
|
|
modded = moddedResult.modifiedAst
|
|
}
|
|
const { execState } = await executeAstMock({
|
|
ast: modded,
|
|
rustContext: this.rustContext,
|
|
})
|
|
const sketch = sketchFromKclValue(
|
|
execState.variables[variableDeclarationName],
|
|
variableDeclarationName
|
|
)
|
|
if (err(sketch)) return
|
|
const sgPaths = sketch.paths
|
|
const orthoFactor = orthoScale(this.sceneInfra.camControls.camera)
|
|
|
|
const varDecIndex = Number(sketchEntryNodePath[1][0])
|
|
|
|
this.updateSegment(
|
|
sketch.start,
|
|
0,
|
|
varDecIndex,
|
|
_ast,
|
|
orthoFactor,
|
|
sketch
|
|
)
|
|
sgPaths.forEach((seg, index) =>
|
|
this.updateSegment(seg, index, varDecIndex, _ast, orthoFactor, sketch)
|
|
)
|
|
},
|
|
onClick: async (args) => {
|
|
const firstProfileIndex = Number(sketchNodePaths[0][1][0])
|
|
const nodePathWithCorrectedIndexForTruncatedAst = structuredClone(
|
|
mod.pathToNode
|
|
)
|
|
|
|
nodePathWithCorrectedIndexForTruncatedAst[1][0] =
|
|
Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) -
|
|
firstProfileIndex
|
|
// If there is a valid camera interaction that matches, do that instead
|
|
const interaction = this.sceneInfra.camControls.getInteractionType(
|
|
args.mouseEvent
|
|
)
|
|
if (interaction !== 'none') return
|
|
// Commit the arc to the full AST/code and return to sketch.idle
|
|
const mousePoint = args.intersectionPoint?.twoD
|
|
if (!mousePoint || args.mouseEvent.button !== 0) return
|
|
|
|
const _node = getNodeFromPath<VariableDeclaration>(
|
|
_ast,
|
|
sketchEntryNodePath || [],
|
|
'VariableDeclaration'
|
|
)
|
|
if (trap(_node)) return
|
|
const sketchInit = _node.node?.declaration.init
|
|
|
|
let modded = structuredClone(_ast)
|
|
|
|
const intersectsProfileStart =
|
|
!doNotSnapAsThreePointArcIsTheOnlySegment &&
|
|
this.didIntersectProfileStart(args, sketchEntryNodePath)
|
|
if (sketchInit.type === 'PipeExpression' && args.intersectionPoint) {
|
|
// Calculate end angle based on final mouse position
|
|
|
|
const moddedResult = changeSketchArguments(
|
|
modded,
|
|
this.kclManager.variables,
|
|
{
|
|
type: 'path',
|
|
pathToNode: mod.pathToNode,
|
|
},
|
|
{
|
|
type: 'circle-three-point-segment',
|
|
p1,
|
|
p2,
|
|
p3: this.getSnappedDragPoint(
|
|
args.intersectionPoint.twoD,
|
|
args.intersects,
|
|
args.mouseEvent
|
|
).snappedPoint,
|
|
}
|
|
)
|
|
if (err(moddedResult)) return
|
|
modded = moddedResult.modifiedAst
|
|
if (intersectsProfileStart) {
|
|
const originCoords = createArrayExpression([
|
|
createCallExpressionStdLibKw(
|
|
'profileStartX',
|
|
createPipeSubstitution(),
|
|
[]
|
|
),
|
|
createCallExpressionStdLibKw(
|
|
'profileStartY',
|
|
createPipeSubstitution(),
|
|
[]
|
|
),
|
|
])
|
|
|
|
const arcToCallExp = getNodeFromPath<CallExpression>(
|
|
modded,
|
|
mod.pathToNode,
|
|
'CallExpression'
|
|
)
|
|
if (err(arcToCallExp)) return
|
|
const firstArg = arcToCallExp.node.arguments[0]
|
|
if (firstArg.type !== 'ObjectExpression') return
|
|
for (const prop of firstArg.properties) {
|
|
if (prop.key.type === 'Identifier' && prop.key.name === 'end') {
|
|
prop.value = originCoords
|
|
}
|
|
}
|
|
|
|
const moddedResult = addCloseToPipe({
|
|
node: modded,
|
|
variables: this.kclManager.variables,
|
|
pathToNode: sketchEntryNodePath,
|
|
})
|
|
if (err(moddedResult)) return
|
|
modded = moddedResult
|
|
}
|
|
|
|
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 arc tool
|
|
await updateModelingState(_ast, EXECUTION_TYPE_MOCK, {
|
|
kclManager: this.kclManager,
|
|
editorManager: this.editorManager,
|
|
codeManager: this.codeManager,
|
|
})
|
|
if (intersectsProfileStart) {
|
|
this.sceneInfra.modelingSend({ type: 'Close sketch' })
|
|
} else {
|
|
this.sceneInfra.modelingSend({ type: 'Finish arc' })
|
|
}
|
|
}
|
|
},
|
|
})
|
|
return {
|
|
updatedEntryNodePath: mod.pathToNode,
|
|
updatedSketchNodePaths: sketchNodePaths,
|
|
expressionIndexToDelete: insertIndex, // Return the insertion index so it can be deleted if needed
|
|
}
|
|
}
|
|
setupDraftCircle = async (
|
|
sketchEntryNodePath: PathToNode,
|
|
sketchNodePaths: PathToNode[],
|
|
planeNodePath: PathToNode,
|
|
forward: [number, number, number],
|
|
up: [number, number, number],
|
|
sketchOrigin: [number, number, number],
|
|
circleCenter: [x: number, y: number]
|
|
): Promise<SketchDetailsUpdate | Error> => {
|
|
let _ast = structuredClone(this.kclManager.ast)
|
|
|
|
const varDec = getNodeFromPath<VariableDeclarator>(
|
|
_ast,
|
|
planeNodePath,
|
|
'VariableDeclarator'
|
|
)
|
|
|
|
if (err(varDec)) return varDec
|
|
if (varDec.node.type !== 'VariableDeclarator') return new Error('not a var')
|
|
|
|
const varName = findUniqueName(_ast, 'profile')
|
|
const newExpression = createVariableDeclaration(
|
|
varName,
|
|
createCallExpressionStdLibKw(
|
|
'circle',
|
|
createLocalName(varDec.node.id.name),
|
|
[
|
|
createLabeledArg(
|
|
'center',
|
|
createArrayExpression([
|
|
createLiteral(roundOff(circleCenter[0])),
|
|
createLiteral(roundOff(circleCenter[1])),
|
|
])
|
|
),
|
|
createLabeledArg('radius', createLiteral(1)),
|
|
]
|
|
)
|
|
)
|
|
|
|
const insertIndex = getInsertIndex(sketchNodePaths, planeNodePath, 'end')
|
|
|
|
_ast.body.splice(insertIndex, 0, newExpression)
|
|
const { updatedEntryNodePath, updatedSketchNodePaths } =
|
|
updateSketchNodePathsWithInsertIndex({
|
|
insertIndex,
|
|
insertType: 'end',
|
|
sketchNodePaths,
|
|
})
|
|
|
|
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
|
|
const didReParse = await this.kclManager.executeAstMock(_ast)
|
|
if (err(didReParse)) return didReParse
|
|
|
|
const { truncatedAst } = await this.setupSketch({
|
|
sketchEntryNodePath: updatedEntryNodePath,
|
|
sketchNodePaths: updatedSketchNodePaths,
|
|
forward,
|
|
up,
|
|
position: sketchOrigin,
|
|
maybeModdedAst: _ast,
|
|
draftExpressionsIndices: { start: 0, end: 0 },
|
|
})
|
|
|
|
this.sceneInfra.setCallbacks({
|
|
onMove: async (args) => {
|
|
const nodePathWithCorrectedIndexForTruncatedAst =
|
|
structuredClone(updatedEntryNodePath)
|
|
nodePathWithCorrectedIndexForTruncatedAst[1][0] =
|
|
Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) -
|
|
Number(planeNodePath[1][0]) -
|
|
1
|
|
const _node = getNodeFromPath<VariableDeclaration>(
|
|
truncatedAst,
|
|
nodePathWithCorrectedIndexForTruncatedAst,
|
|
'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 === 'CallExpressionKw') {
|
|
const moddedResult = changeSketchArguments(
|
|
modded,
|
|
this.kclManager.variables,
|
|
{
|
|
type: 'path',
|
|
pathToNode: nodePathWithCorrectedIndexForTruncatedAst,
|
|
},
|
|
{
|
|
type: 'arc-segment',
|
|
center: circleCenter,
|
|
radius: Math.sqrt(x ** 2 + y ** 2),
|
|
from: circleCenter,
|
|
to: circleCenter, // Same as from for a full circle
|
|
ccw: true,
|
|
}
|
|
)
|
|
if (err(moddedResult)) {
|
|
return
|
|
}
|
|
modded = moddedResult.modifiedAst
|
|
}
|
|
|
|
const { execState } = await executeAstMock({
|
|
ast: modded,
|
|
rustContext: this.rustContext,
|
|
})
|
|
const sketch = sketchFromKclValue(execState.variables[varName], varName)
|
|
if (err(sketch)) return
|
|
const sgPaths = sketch.paths
|
|
const orthoFactor = orthoScale(this.sceneInfra.camControls.camera)
|
|
|
|
const varDecIndex = Number(updatedEntryNodePath[1][0])
|
|
|
|
this.updateSegment(
|
|
sketch.start,
|
|
0,
|
|
varDecIndex,
|
|
_ast,
|
|
orthoFactor,
|
|
sketch
|
|
)
|
|
sgPaths.forEach((seg, index) =>
|
|
this.updateSegment(seg, index, varDecIndex, _ast, orthoFactor, sketch)
|
|
)
|
|
},
|
|
onClick: async (args) => {
|
|
// If there is a valid camera interaction that matches, do that instead
|
|
const interaction = this.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,
|
|
updatedEntryNodePath || [],
|
|
'VariableDeclaration'
|
|
)
|
|
if (trap(_node)) return
|
|
const sketchInit = _node.node?.declaration.init
|
|
|
|
let modded = structuredClone(_ast)
|
|
if (sketchInit.type === 'CallExpressionKw') {
|
|
const moddedResult = changeSketchArguments(
|
|
modded,
|
|
this.kclManager.variables,
|
|
{
|
|
type: 'path',
|
|
pathToNode: updatedEntryNodePath,
|
|
},
|
|
{
|
|
type: 'arc-segment',
|
|
center: circleCenter,
|
|
radius: Math.sqrt(x ** 2 + y ** 2),
|
|
from: circleCenter,
|
|
to: circleCenter, // Same as from for a full circle
|
|
ccw: true,
|
|
}
|
|
)
|
|
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 updateModelingState(_ast, EXECUTION_TYPE_MOCK, {
|
|
kclManager: this.kclManager,
|
|
editorManager: this.editorManager,
|
|
codeManager: this.codeManager,
|
|
})
|
|
this.sceneInfra.modelingSend({ type: 'Finish circle' })
|
|
}
|
|
},
|
|
})
|
|
return {
|
|
updatedEntryNodePath,
|
|
updatedSketchNodePaths,
|
|
expressionIndexToDelete: insertIndex,
|
|
}
|
|
}
|
|
setupSketchIdleCallbacks = ({
|
|
sketchEntryNodePath,
|
|
sketchNodePaths,
|
|
planeNodePath,
|
|
up,
|
|
forward,
|
|
position,
|
|
}: {
|
|
sketchEntryNodePath: PathToNode
|
|
sketchNodePaths: PathToNode[]
|
|
planeNodePath: PathToNode
|
|
forward: [number, number, number]
|
|
up: [number, number, number]
|
|
position?: [number, number, number]
|
|
}) => {
|
|
let addingNewSegmentStatus: 'nothing' | 'pending' | 'added' = 'nothing'
|
|
this.sceneInfra.setCallbacks({
|
|
onDragEnd: async () => {
|
|
if (addingNewSegmentStatus !== 'nothing') {
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
this.setupSketch({
|
|
sketchEntryNodePath,
|
|
sketchNodePaths,
|
|
maybeModdedAst: this.kclManager.ast,
|
|
up,
|
|
forward,
|
|
position,
|
|
})
|
|
// setting up the callbacks again resets value in closures
|
|
this.setupSketchIdleCallbacks({
|
|
sketchEntryNodePath,
|
|
sketchNodePaths,
|
|
planeNodePath,
|
|
up,
|
|
forward,
|
|
position,
|
|
})
|
|
await this.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: this.kclManager.ast,
|
|
variables: this.kclManager.variables,
|
|
kclManager: this.kclManager,
|
|
})
|
|
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: this.kclManager.ast,
|
|
variables: this.kclManager.variables,
|
|
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
|
|
|
|
const didReParse = await this.kclManager.executeAstMock(
|
|
mod.modifiedAst
|
|
)
|
|
if (err(didReParse)) return
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
this.setupSketch({
|
|
sketchEntryNodePath: pathToNode,
|
|
sketchNodePaths,
|
|
maybeModdedAst: this.kclManager.ast,
|
|
up,
|
|
forward,
|
|
position,
|
|
})
|
|
addingNewSegmentStatus = 'added'
|
|
} else if (addingNewSegmentStatus === 'added') {
|
|
const pathToNodeForNewSegment = pathToNode.slice(0, pathToNodeIndex)
|
|
pathToNodeForNewSegment.push([pipeIndex - 2, 'index'])
|
|
this.onDragSegment({
|
|
sketchNodePaths,
|
|
sketchEntryNodePath: pathToNodeForNewSegment,
|
|
object: selected,
|
|
intersection2d: intersectionPoint.twoD,
|
|
intersects,
|
|
mouseEvent: mouseEvent,
|
|
})
|
|
}
|
|
return
|
|
}
|
|
|
|
this.onDragSegment({
|
|
object: selected,
|
|
intersection2d: intersectionPoint.twoD,
|
|
intersects,
|
|
sketchNodePaths,
|
|
sketchEntryNodePath,
|
|
mouseEvent: mouseEvent,
|
|
})
|
|
},
|
|
onMove: () => {},
|
|
onClick: (args) => {
|
|
// If there is a valid camera interaction that matches, do that instead
|
|
const interaction = this.sceneInfra.camControls.getInteractionType(
|
|
args.mouseEvent
|
|
)
|
|
if (interaction !== 'none') return
|
|
if (args?.mouseEvent.which !== 1) return
|
|
if (!args || !args.selected) {
|
|
this.sceneInfra.modelingSend({
|
|
type: 'Set selection',
|
|
data: {
|
|
selectionType: 'singleCodeCursor',
|
|
},
|
|
})
|
|
return
|
|
}
|
|
const { selected } = args
|
|
const event = getEventForSegmentSelection(selected)
|
|
if (!event) return
|
|
this.sceneInfra.modelingSend(event)
|
|
},
|
|
...this.mouseEnterLeaveCallbacks(),
|
|
})
|
|
}
|
|
prepareTruncatedAst = (
|
|
sketchNodePaths: PathToNode[],
|
|
ast?: Node<Program>,
|
|
draftSegment?: DraftSegment
|
|
) =>
|
|
prepareTruncatedAst(
|
|
sketchNodePaths,
|
|
ast || this.kclManager.ast,
|
|
this.kclManager.lastSuccessfulVariables,
|
|
draftSegment
|
|
)
|
|
|
|
getSnappedDragPoint(
|
|
pos: Vector2,
|
|
intersects: Intersection<Object3D<Object3DEventMap>>[],
|
|
mouseEvent: MouseEvent,
|
|
// During draft segment mouse move:
|
|
// - the three.js object currently being dragged: the new draft segment or existing segment (may not be the last in activeSegments)
|
|
// When placing the draft segment::
|
|
// - the last segment in activeSegments
|
|
currentObject?: Object3D | Group
|
|
) {
|
|
let snappedPoint: Coords2d = [pos.x, pos.y]
|
|
|
|
const intersectsYAxis = intersects.find(
|
|
(sceneObject) => sceneObject.object.name === Y_AXIS
|
|
)
|
|
const intersectsXAxis = intersects.find(
|
|
(sceneObject) => sceneObject.object.name === X_AXIS
|
|
)
|
|
|
|
// Snap to previous segment's tangent direction when drawing a straight segment
|
|
let snappedToTangent = false
|
|
let negativeTangentDirection = false
|
|
|
|
const disableTangentSnapping = mouseEvent.ctrlKey || mouseEvent.altKey
|
|
const forceDirectionSnapping = mouseEvent.shiftKey
|
|
if (!disableTangentSnapping) {
|
|
const segments: SafeArray<Group> = Object.values(this.activeSegments) // Using the order in the object feels wrong
|
|
const currentIndex =
|
|
currentObject instanceof Group ? segments.indexOf(currentObject) : -1
|
|
const current = segments[currentIndex]
|
|
if (
|
|
current?.userData.type === STRAIGHT_SEGMENT &&
|
|
// This draft check is not strictly necessary currently, but we only want
|
|
// to snap when drawing a new segment, this makes that more robust.
|
|
current?.userData.draft
|
|
) {
|
|
const prev = segments[currentIndex - 1]
|
|
if (prev && ARC_SEGMENT_TYPES.includes(prev.userData.type)) {
|
|
const snapDirection = findTangentDirection(prev)
|
|
if (snapDirection) {
|
|
const SNAP_TOLERANCE_PIXELS = 8 * window.devicePixelRatio
|
|
const SNAP_MIN_DISTANCE_PIXELS = 10 * window.devicePixelRatio
|
|
const orthoFactor = orthoScale(this.sceneInfra.camControls.camera)
|
|
|
|
// See if snapDirection intersects with any of the axes
|
|
if (intersectsXAxis || intersectsYAxis) {
|
|
let intersectionPoint: Coords2d | undefined
|
|
if (intersectsXAxis && intersectsYAxis) {
|
|
// Current mouse position intersects with both axes (origin) -> that has precedence over tangent so we snap to the origin.
|
|
intersectionPoint = [0, 0]
|
|
} else {
|
|
// Intersects only one axis
|
|
const axisLine: [Coords2d, Coords2d] = intersectsXAxis
|
|
? [
|
|
[0, 0],
|
|
[1, 0],
|
|
]
|
|
: [
|
|
[0, 0],
|
|
[0, 1],
|
|
]
|
|
// See if that axis line intersects with the tangent direction
|
|
// Note: this includes both positive and negative tangent directions as it just checks 2 lines.
|
|
intersectionPoint = calculateIntersectionOfTwoLines({
|
|
line1: axisLine,
|
|
line2Angle: getAngle([0, 0], snapDirection),
|
|
line2Point: current.userData.from,
|
|
})
|
|
}
|
|
|
|
// If yes, see if that intersection point is within tolerance and if yes snap to it.
|
|
if (
|
|
intersectionPoint &&
|
|
getLength(intersectionPoint, snappedPoint) / orthoFactor <
|
|
SNAP_TOLERANCE_PIXELS
|
|
) {
|
|
snappedPoint = intersectionPoint
|
|
snappedToTangent = true
|
|
}
|
|
}
|
|
if (!snappedToTangent) {
|
|
// Otherwise, try to snap to the tangent direction, in both positive and negative directions
|
|
const { closestPoint, t } = closestPointOnRay(
|
|
prev.userData.to,
|
|
snapDirection,
|
|
snappedPoint,
|
|
true
|
|
)
|
|
if (
|
|
forceDirectionSnapping ||
|
|
(this.sceneInfra.screenSpaceDistance(
|
|
closestPoint,
|
|
snappedPoint
|
|
) < SNAP_TOLERANCE_PIXELS &&
|
|
// We only want to snap to the tangent direction if the mouse has moved enough to avoid quick jumps
|
|
// at the beginning of the drag
|
|
this.sceneInfra.screenSpaceDistance(
|
|
current.userData.from,
|
|
current.userData.to
|
|
) > SNAP_MIN_DISTANCE_PIXELS)
|
|
) {
|
|
snappedPoint = closestPoint
|
|
snappedToTangent = true
|
|
negativeTangentDirection = t < 0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Snap to the main axes if there was no snapping to tangent direction
|
|
if (!snappedToTangent) {
|
|
snappedPoint = [
|
|
intersectsYAxis ? 0 : snappedPoint[0],
|
|
intersectsXAxis ? 0 : snappedPoint[1],
|
|
]
|
|
}
|
|
|
|
return {
|
|
isSnapped: !!(intersectsYAxis || intersectsXAxis || snappedToTangent),
|
|
snappedToTangent,
|
|
negativeTangentDirection,
|
|
snappedPoint,
|
|
intersectsXAxis,
|
|
intersectsYAxis,
|
|
}
|
|
}
|
|
|
|
positionDraftPoint({
|
|
origin,
|
|
yAxis,
|
|
zAxis,
|
|
snappedPoint,
|
|
}: {
|
|
origin: SketchDetails['origin']
|
|
yAxis: SketchDetails['yAxis']
|
|
zAxis: SketchDetails['zAxis']
|
|
snappedPoint: Vector2
|
|
}) {
|
|
const draftPoint = this.getDraftPoint()
|
|
if (!draftPoint) {
|
|
this.createDraftPoint({
|
|
point: snappedPoint,
|
|
origin,
|
|
yAxis,
|
|
zAxis,
|
|
})
|
|
} 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)
|
|
}
|
|
}
|
|
|
|
maybeSnapProfileStartIntersect2d({
|
|
sketchEntryNodePath,
|
|
intersects,
|
|
intersection2d: _intersection2d,
|
|
}: {
|
|
sketchEntryNodePath: PathToNode
|
|
intersects: Intersection<Object3D<Object3DEventMap>>[]
|
|
intersection2d: Vector2
|
|
}) {
|
|
const intersectsProfileStart = intersects
|
|
.map(({ object }) => getParentGroup(object, [PROFILE_START]))
|
|
.find(isGroupStartProfileForCurrentProfile(sketchEntryNodePath))
|
|
const intersection2d = intersectsProfileStart
|
|
? new Vector2(
|
|
intersectsProfileStart.position.x,
|
|
intersectsProfileStart.position.y
|
|
)
|
|
: _intersection2d
|
|
return intersection2d
|
|
}
|
|
|
|
onDragSegment({
|
|
object,
|
|
intersection2d: _intersection2d,
|
|
sketchEntryNodePath,
|
|
sketchNodePaths,
|
|
draftInfo,
|
|
intersects,
|
|
mouseEvent,
|
|
}: {
|
|
object: Object3D<Object3DEventMap>
|
|
intersection2d: Vector2
|
|
sketchEntryNodePath: PathToNode
|
|
sketchNodePaths: PathToNode[]
|
|
intersects: Intersection<Object3D<Object3DEventMap>>[]
|
|
draftInfo?: {
|
|
truncatedAst: Node<Program>
|
|
variableDeclarationName: string
|
|
}
|
|
mouseEvent: MouseEvent
|
|
}) {
|
|
const intersection2d = this.maybeSnapProfileStartIntersect2d({
|
|
sketchEntryNodePath,
|
|
intersects,
|
|
intersection2d: _intersection2d,
|
|
})
|
|
|
|
const group = getParentGroup(object, SEGMENT_BODIES_PLUS_PROFILE_START)
|
|
const subGroup = getParentGroup(object, [
|
|
ARROWHEAD,
|
|
CIRCLE_CENTER_HANDLE,
|
|
CIRCLE_THREE_POINT_HANDLE1,
|
|
CIRCLE_THREE_POINT_HANDLE2,
|
|
CIRCLE_THREE_POINT_HANDLE3,
|
|
THREE_POINT_ARC_HANDLE2,
|
|
THREE_POINT_ARC_HANDLE3,
|
|
ARC_ANGLE_END,
|
|
])
|
|
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
|
|
}
|
|
|
|
const from: [number, number] = [
|
|
group.userData?.from?.[0],
|
|
group.userData?.from?.[1],
|
|
]
|
|
const { snappedPoint: dragTo, snappedToTangent } = this.getSnappedDragPoint(
|
|
intersection2d,
|
|
intersects,
|
|
mouseEvent,
|
|
object
|
|
)
|
|
let modifiedAst = draftInfo
|
|
? draftInfo.truncatedAst
|
|
: { ...this.kclManager.ast }
|
|
|
|
const nodePathWithCorrectedIndexForTruncatedAst =
|
|
structuredClone(pathToNode)
|
|
nodePathWithCorrectedIndexForTruncatedAst[1][0] =
|
|
Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) -
|
|
Number(sketchNodePaths[0][1][0])
|
|
|
|
const _node = getNodeFromPath<Node<CallExpression | CallExpressionKw>>(
|
|
modifiedAst,
|
|
draftInfo ? nodePathWithCorrectedIndexForTruncatedAst : pathToNode,
|
|
['CallExpression', 'CallExpressionKw']
|
|
)
|
|
if (trap(_node)) return
|
|
const node = _node.node
|
|
|
|
if (node.type !== 'CallExpression' && node.type !== 'CallExpressionKw')
|
|
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,
|
|
to: from, // Same as from for a full circle
|
|
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
|
|
),
|
|
ccw: true,
|
|
}
|
|
if (
|
|
group.name === CIRCLE_SEGMENT &&
|
|
subGroup?.name === CIRCLE_CENTER_HANDLE
|
|
)
|
|
return {
|
|
type: 'arc-segment',
|
|
from,
|
|
to: from, // Same as from for a full circle
|
|
center: dragTo,
|
|
radius: group.userData.radius,
|
|
ccw: true,
|
|
}
|
|
|
|
// Handle ARC_SEGMENT with center handle
|
|
if (
|
|
group.name === ARC_SEGMENT &&
|
|
subGroup?.name === CIRCLE_CENTER_HANDLE
|
|
) {
|
|
// the user is dragging the circle's center, but the values they updating the arc's radius and start angle
|
|
// we need to calculate what the radius should be and a new to point that respects the endAngle
|
|
const newCenter = dragTo
|
|
const radius = Math.sqrt(
|
|
(newCenter[0] - group.userData.from[0]) ** 2 +
|
|
(newCenter[1] - group.userData.from[1]) ** 2
|
|
)
|
|
const endAngle = Math.atan2(
|
|
group.userData.to[1] - group.userData.center[1],
|
|
group.userData.to[0] - group.userData.center[0]
|
|
)
|
|
const newTo: [number, number] = [
|
|
newCenter[0] + radius * Math.cos(endAngle),
|
|
newCenter[1] + radius * Math.sin(endAngle),
|
|
]
|
|
return {
|
|
type: 'arc-segment',
|
|
from: group.userData.from,
|
|
to: newTo,
|
|
center: newCenter,
|
|
radius,
|
|
ccw: group.userData.ccw,
|
|
}
|
|
}
|
|
// Handle ARC_SEGMENT with end angle handle
|
|
if (group.name === ARC_SEGMENT && subGroup?.name === ARC_ANGLE_END) {
|
|
// Calculate the angle from center to drag point
|
|
const center = group.userData.center
|
|
const endAngle = Math.atan2(
|
|
dragTo[1] - center[1],
|
|
dragTo[0] - center[0]
|
|
)
|
|
|
|
// Calculate the point on the arc at the given angle and radius
|
|
const radius = group.userData.radius
|
|
const toPoint: [number, number] = [
|
|
center[0] + radius * Math.cos(endAngle),
|
|
center[1] + radius * Math.sin(endAngle),
|
|
]
|
|
|
|
return {
|
|
type: 'arc-segment',
|
|
from: group.userData.from,
|
|
to: toPoint,
|
|
center: center,
|
|
radius: radius,
|
|
ccw: group.userData.ccw,
|
|
}
|
|
}
|
|
if (
|
|
subGroup?.name &&
|
|
[
|
|
CIRCLE_THREE_POINT_HANDLE1,
|
|
CIRCLE_THREE_POINT_HANDLE2,
|
|
CIRCLE_THREE_POINT_HANDLE3,
|
|
].includes(subGroup?.name)
|
|
) {
|
|
const input: SegmentInputs = {
|
|
type: 'circle-three-point-segment',
|
|
p1: group.userData.p1,
|
|
p2: group.userData.p2,
|
|
p3: group.userData.p3,
|
|
}
|
|
if (subGroup?.name === CIRCLE_THREE_POINT_HANDLE1) {
|
|
input.p1 = dragTo
|
|
} else if (subGroup?.name === CIRCLE_THREE_POINT_HANDLE2) {
|
|
input.p2 = dragTo
|
|
} else if (subGroup?.name === CIRCLE_THREE_POINT_HANDLE3) {
|
|
input.p3 = dragTo
|
|
}
|
|
return input
|
|
}
|
|
if (
|
|
subGroup?.name &&
|
|
[THREE_POINT_ARC_HANDLE2, THREE_POINT_ARC_HANDLE3].includes(
|
|
subGroup?.name
|
|
)
|
|
) {
|
|
const input: SegmentInputs = {
|
|
type: 'circle-three-point-segment',
|
|
p1: group.userData.p1,
|
|
p2: group.userData.p2,
|
|
p3: group.userData.p3,
|
|
}
|
|
if (subGroup?.name === THREE_POINT_ARC_HANDLE2) {
|
|
input.p2 = dragTo
|
|
} else if (subGroup?.name === THREE_POINT_ARC_HANDLE3) {
|
|
input.p3 = dragTo
|
|
}
|
|
return input
|
|
}
|
|
|
|
// 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,
|
|
},
|
|
variables: this.kclManager.variables,
|
|
})
|
|
} else {
|
|
modded = changeSketchArguments(
|
|
modifiedAst,
|
|
this.kclManager.variables,
|
|
{
|
|
type: 'sourceRange',
|
|
sourceRange: topLevelRange(node.start, node.end),
|
|
},
|
|
getChangeSketchInput()
|
|
)
|
|
}
|
|
if (trap(modded)) return
|
|
|
|
modifiedAst = modded.modifiedAst
|
|
const info = draftInfo
|
|
? draftInfo
|
|
: this.prepareTruncatedAst(sketchNodePaths || [], modifiedAst)
|
|
if (trap(info, { suppress: true })) return
|
|
const { truncatedAst } = 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
|
|
this.codeManager.updateCodeEditor(code)
|
|
const { execState } = await executeAstMock({
|
|
ast: truncatedAst,
|
|
rustContext: this.rustContext,
|
|
})
|
|
const variables = execState.variables
|
|
const sketchesInfo = getSketchesInfo({
|
|
sketchNodePaths,
|
|
ast: truncatedAst,
|
|
variables,
|
|
kclManager: this.kclManager,
|
|
})
|
|
const callBacks: (() => SegmentOverlayPayload | null)[] = []
|
|
for (const sketchInfo of sketchesInfo) {
|
|
const { sketch, pathToNode: _pathToNode } = sketchInfo
|
|
const varDecIndex = Number(_pathToNode[1][0])
|
|
|
|
if (!sketch) return
|
|
|
|
const sgPaths = sketch.paths
|
|
const orthoFactor = orthoScale(this.sceneInfra.camControls.camera)
|
|
|
|
this.updateSegment(
|
|
sketch.start,
|
|
0,
|
|
varDecIndex,
|
|
modifiedAst,
|
|
orthoFactor,
|
|
sketch,
|
|
snappedToTangent
|
|
)
|
|
|
|
callBacks.push(
|
|
...sgPaths.map((group, index) =>
|
|
this.updateSegment(
|
|
group,
|
|
index,
|
|
varDecIndex,
|
|
modifiedAst,
|
|
orthoFactor,
|
|
sketch,
|
|
snappedToTangent
|
|
)
|
|
)
|
|
)
|
|
}
|
|
this.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
|
|
* @param snappedToTangent if currently drawn draft segment is snapping to previous arc tangent
|
|
*/
|
|
updateSegment = (
|
|
segment: Path | Sketch['start'],
|
|
index: number,
|
|
varDecIndex: number,
|
|
modifiedAst: Program,
|
|
orthoFactor: number,
|
|
sketch: Sketch,
|
|
snappedToTangent: boolean = false
|
|
): (() => 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 type = group?.userData?.type
|
|
const factor =
|
|
(this.sceneInfra.camControls.camera instanceof OrthographicCamera
|
|
? orthoFactor
|
|
: perspScale(this.sceneInfra.camControls.camera, group)) /
|
|
this.sceneInfra._baseUnitMultiplier
|
|
let input: SegmentInputs = {
|
|
type: 'straight-segment',
|
|
from: segment.from,
|
|
to: segment.to,
|
|
snap: snappedToTangent,
|
|
}
|
|
let update: SegmentUtils['update'] | null = null
|
|
if (type === TANGENTIAL_ARC_TO_SEGMENT) {
|
|
update = segmentUtils.tangentialArc.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,
|
|
to: segment.from, // Use from as to for full circles
|
|
center: segment.center,
|
|
radius: segment.radius,
|
|
ccw: true,
|
|
}
|
|
} else if (
|
|
type === CIRCLE_THREE_POINT_SEGMENT &&
|
|
'type' in segment &&
|
|
segment.type === 'CircleThreePoint'
|
|
) {
|
|
update = segmentUtils.circleThreePoint.update
|
|
input = {
|
|
type: 'circle-three-point-segment',
|
|
p1: segment.p1,
|
|
p2: segment.p2,
|
|
p3: segment.p3,
|
|
}
|
|
} else if (
|
|
type === ARC_SEGMENT &&
|
|
'type' in segment &&
|
|
segment.type === 'Arc'
|
|
) {
|
|
update = segmentUtils.arc.update
|
|
input = {
|
|
type: 'arc-segment',
|
|
from: segment.from,
|
|
to: segment.to,
|
|
center: segment.center,
|
|
radius: segment.radius,
|
|
ccw: segment.ccw,
|
|
}
|
|
} else if (
|
|
type === THREE_POINT_ARC_SEGMENT &&
|
|
'type' in segment &&
|
|
segment.type === 'ArcThreePoint'
|
|
) {
|
|
update = segmentUtils.threePointArc.update
|
|
input = {
|
|
type: 'circle-three-point-segment',
|
|
p1: segment.p1,
|
|
p2: segment.p2,
|
|
p3: segment.p3,
|
|
}
|
|
}
|
|
const callBack =
|
|
update &&
|
|
!err(update) &&
|
|
update({
|
|
input,
|
|
group,
|
|
scale: factor,
|
|
prevSegment: sgPaths[index - 1],
|
|
sceneInfra: this.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.sceneInfra.scene.remove(this.axisGroup)
|
|
}
|
|
|
|
tearDownSketch({ removeAxis = true }: { removeAxis?: boolean }) {
|
|
// Remove all draft groups
|
|
this.draftPointGroups.forEach((draftPointGroup) => {
|
|
this.sceneInfra.scene.remove(draftPointGroup)
|
|
})
|
|
|
|
// Remove all sketch tools
|
|
|
|
if (this.axisGroup && removeAxis)
|
|
this.sceneInfra.scene.remove(this.axisGroup)
|
|
const sketchSegments = this.sceneInfra.scene.children.find(
|
|
({ userData }) => userData?.type === SKETCH_GROUP_SEGMENTS
|
|
)
|
|
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.sceneInfra.scene.remove(sketchSegments)
|
|
}
|
|
this.sceneInfra.camControls.enableRotate = true
|
|
this.activeSegments = {}
|
|
}
|
|
|
|
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(this.kclManager.ast))
|
|
if (trap(pResult) || !resultIsOk(pResult))
|
|
return Promise.reject(pResult)
|
|
const updatedAst = pResult.program
|
|
const _node = getNodeFromPath<
|
|
Node<CallExpression | CallExpressionKw>
|
|
>(updatedAst, parent.userData.pathToNode, [
|
|
'CallExpressionKw',
|
|
'CallExpression',
|
|
])
|
|
if (trap(_node, { suppress: true })) return
|
|
const node = _node.node
|
|
this.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(this.sceneInfra.camControls.camera)
|
|
|
|
let input: SegmentInputs = {
|
|
type: 'straight-segment',
|
|
from: parent.userData.from,
|
|
to: parent.userData.to,
|
|
}
|
|
const factor =
|
|
(this.sceneInfra.camControls.camera instanceof OrthographicCamera
|
|
? orthoFactor
|
|
: perspScale(this.sceneInfra.camControls.camera, parent)) /
|
|
this.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.tangentialArc.update
|
|
input = {
|
|
type: 'arc-segment',
|
|
from: parent.userData.from,
|
|
to: parent.userData.to,
|
|
radius: parent.userData.radius,
|
|
center: parent.userData.center,
|
|
ccw:
|
|
parent.userData.ccw !== undefined ? parent.userData.ccw : true,
|
|
}
|
|
} else if (parent.name === CIRCLE_SEGMENT) {
|
|
update = segmentUtils.circle.update
|
|
input = {
|
|
type: 'arc-segment',
|
|
from: parent.userData.from,
|
|
to: parent.userData.to,
|
|
radius: parent.userData.radius,
|
|
center: parent.userData.center,
|
|
ccw:
|
|
parent.userData.ccw !== undefined ? parent.userData.ccw : true,
|
|
}
|
|
} else if (parent.name === ARC_SEGMENT) {
|
|
update = segmentUtils.arc.update
|
|
input = {
|
|
type: 'arc-segment',
|
|
from: parent.userData.from,
|
|
to: parent.userData.to,
|
|
radius: parent.userData.radius,
|
|
center: parent.userData.center,
|
|
ccw:
|
|
parent.userData.ccw !== undefined ? parent.userData.ccw : true,
|
|
}
|
|
} else if (parent.name === THREE_POINT_ARC_SEGMENT) {
|
|
update = segmentUtils.threePointArc.update
|
|
input = {
|
|
type: 'circle-three-point-segment',
|
|
p1: parent.userData.p1,
|
|
p2: parent.userData.p2,
|
|
p3: parent.userData.p3,
|
|
}
|
|
}
|
|
|
|
update &&
|
|
update({
|
|
prevSegment: parent.userData.prevSegment,
|
|
input,
|
|
group: parent,
|
|
scale: factor,
|
|
sceneInfra: this.sceneInfra,
|
|
})
|
|
return
|
|
}
|
|
this.editorManager.setHighlightRange([defaultSourceRange()])
|
|
},
|
|
onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => {
|
|
this.editorManager.setHighlightRange([defaultSourceRange()])
|
|
const parent = getParentGroup(
|
|
selected,
|
|
SEGMENT_BODIES_PLUS_PROFILE_START
|
|
)
|
|
if (parent) {
|
|
const orthoFactor = orthoScale(this.sceneInfra.camControls.camera)
|
|
|
|
let input: SegmentInputs = {
|
|
type: 'straight-segment',
|
|
from: parent.userData.from,
|
|
to: parent.userData.to,
|
|
}
|
|
const factor =
|
|
(this.sceneInfra.camControls.camera instanceof OrthographicCamera
|
|
? orthoFactor
|
|
: perspScale(this.sceneInfra.camControls.camera, parent)) /
|
|
this.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.tangentialArc.update
|
|
input = {
|
|
type: 'arc-segment',
|
|
from: parent.userData.from,
|
|
to: parent.userData.to,
|
|
radius: parent.userData.radius,
|
|
center: parent.userData.center,
|
|
ccw:
|
|
parent.userData.ccw !== undefined ? parent.userData.ccw : true,
|
|
}
|
|
} else if (parent.name === CIRCLE_SEGMENT) {
|
|
update = segmentUtils.circle.update
|
|
input = {
|
|
type: 'arc-segment',
|
|
from: parent.userData.from,
|
|
to: parent.userData.to,
|
|
radius: parent.userData.radius,
|
|
center: parent.userData.center,
|
|
ccw:
|
|
parent.userData.ccw !== undefined ? parent.userData.ccw : true,
|
|
}
|
|
} else if (parent.name === ARC_SEGMENT) {
|
|
update = segmentUtils.arc.update
|
|
input = {
|
|
type: 'arc-segment',
|
|
from: parent.userData.from,
|
|
to: parent.userData.to,
|
|
radius: parent.userData.radius,
|
|
center: parent.userData.center,
|
|
ccw:
|
|
parent.userData.ccw !== undefined ? parent.userData.ccw : true,
|
|
}
|
|
} else if (parent.name === THREE_POINT_ARC_SEGMENT) {
|
|
update = segmentUtils.threePointArc.update
|
|
input = {
|
|
type: 'circle-three-point-segment',
|
|
p1: parent.userData.p1,
|
|
p2: parent.userData.p2,
|
|
p3: parent.userData.p3,
|
|
}
|
|
}
|
|
|
|
update &&
|
|
update({
|
|
prevSegment: parent.userData.prevSegment,
|
|
input,
|
|
group: parent,
|
|
scale: factor,
|
|
sceneInfra: this.sceneInfra,
|
|
})
|
|
}
|
|
const isSelected = parent?.userData?.isSelected
|
|
colorSegment(
|
|
selected,
|
|
isSelected
|
|
? 0x0000ff
|
|
: parent?.userData?.baseColor ||
|
|
getThemeColorForThreeJs(this.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() {
|
|
this.sceneInfra.modelingSend({
|
|
type: 'Set Segment Overlays',
|
|
data: {
|
|
type: 'clear',
|
|
},
|
|
})
|
|
}
|
|
|
|
async getSketchOrientationDetails(sketch: Sketch): Promise<{
|
|
quat: Quaternion
|
|
sketchDetails: Omit<
|
|
SketchDetails & { faceId?: string },
|
|
'sketchNodePaths' | 'sketchEntryNodePath' | 'planeNodePath'
|
|
>
|
|
}> {
|
|
if (sketch.on.type === 'plane') {
|
|
const zAxis = crossProduct(sketch?.on.xAxis, sketch?.on.yAxis)
|
|
return {
|
|
quat: getQuaternionFromZAxis(massageFormats(zAxis)),
|
|
sketchDetails: {
|
|
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,
|
|
},
|
|
}
|
|
}
|
|
const faceInfo = await this.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: {
|
|
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,
|
|
},
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 getFaceDetails(
|
|
entityId: string
|
|
): Promise<Models['GetSketchModePlane_type']> {
|
|
// TODO mode engine connection to allow batching returns and batch the following
|
|
await this.engineCommandManager.sendSceneCommand({
|
|
type: 'modeling_cmd_req',
|
|
cmd_id: uuidv4(),
|
|
cmd: {
|
|
type: 'enable_sketch_mode',
|
|
adjust_camera: false,
|
|
animated: false,
|
|
ortho: false,
|
|
entity_id: entityId,
|
|
},
|
|
})
|
|
let resp = await this.engineCommandManager.sendSceneCommand({
|
|
type: 'modeling_cmd_req',
|
|
cmd_id: uuidv4(),
|
|
cmd: { type: 'get_sketch_mode_plane' },
|
|
})
|
|
|
|
if (!resp) {
|
|
return Promise.reject('no response')
|
|
}
|
|
|
|
if (isArray(resp)) {
|
|
resp = resp[0]
|
|
}
|
|
|
|
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 this.engineCommandManager.sendSceneCommand({
|
|
type: 'modeling_cmd_req',
|
|
cmd_id: uuidv4(),
|
|
cmd: { type: 'sketch_mode_disable' },
|
|
})
|
|
return faceInfo
|
|
}
|
|
|
|
drawDashedLine({ from, to }: { from: Coords2d; to: Coords2d }) {
|
|
const baseColor = getThemeColorForThreeJs(this.sceneInfra._theme)
|
|
const color = baseColor
|
|
const meshType = STRAIGHT_SEGMENT_DASH
|
|
|
|
const segmentGroup = new Group()
|
|
const shape = new Shape()
|
|
shape.moveTo(0, -5) // The width of the line in px (2.4px in this case)
|
|
shape.lineTo(0, 5)
|
|
const line = new LineCurve3(
|
|
new Vector3(from[0], from[1], 0),
|
|
new Vector3(to[0], to[1], 0)
|
|
)
|
|
const geometry = new ExtrudeGeometry(shape, {
|
|
steps: 2,
|
|
bevelEnabled: false,
|
|
extrudePath: line,
|
|
})
|
|
const body = new MeshBasicMaterial({ color })
|
|
const mesh = new Mesh(geometry, body)
|
|
|
|
mesh.userData.type = meshType
|
|
mesh.name = meshType
|
|
segmentGroup.name = DRAFT_DASHED_LINE
|
|
segmentGroup.userData = {
|
|
type: DRAFT_DASHED_LINE,
|
|
from,
|
|
to,
|
|
}
|
|
|
|
segmentGroup.add(mesh)
|
|
segmentGroup.layers.set(SKETCH_LAYER)
|
|
segmentGroup.traverse((child) => {
|
|
child.layers.set(SKETCH_LAYER)
|
|
})
|
|
if (this.currentSketchQuaternion) {
|
|
segmentGroup.setRotationFromQuaternion(this.currentSketchQuaternion)
|
|
}
|
|
return {
|
|
group: segmentGroup,
|
|
updater: (group: Group, to: Coords2d, orthoFactor: number) => {
|
|
const scale =
|
|
(this.sceneInfra.camControls.camera instanceof OrthographicCamera
|
|
? orthoFactor
|
|
: perspScale(this.sceneInfra.camControls.camera, group)) /
|
|
this.sceneInfra._baseUnitMultiplier
|
|
const from = group.userData.from
|
|
|
|
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 straightSegmentBodyDashed = group.children.find(
|
|
(child) => child.userData.type === STRAIGHT_SEGMENT_DASH
|
|
) as Mesh
|
|
if (straightSegmentBodyDashed) {
|
|
straightSegmentBodyDashed.geometry = dashedStraight(
|
|
from,
|
|
to,
|
|
shape,
|
|
scale
|
|
)
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
// calculations/pure-functions/easy to test so no excuse not to
|
|
|
|
function prepareTruncatedAst(
|
|
sketchNodePaths: PathToNode[],
|
|
ast: Node<Program>,
|
|
variables: VariableMap,
|
|
draftSegment?: DraftSegment
|
|
):
|
|
| {
|
|
truncatedAst: Node<Program>
|
|
// can I remove the below?
|
|
variableDeclarationName: string
|
|
}
|
|
| Error {
|
|
const bodyStartIndex = Number(sketchNodePaths?.[0]?.[1]?.[0]) || 0
|
|
const bodyEndIndex =
|
|
Number(sketchNodePaths[sketchNodePaths.length - 1]?.[1]?.[0]) ||
|
|
ast.body.length
|
|
const _ast = structuredClone(ast)
|
|
|
|
const _node = getNodeFromPath<Node<VariableDeclaration>>(
|
|
_ast,
|
|
sketchNodePaths[0] || [],
|
|
'VariableDeclaration'
|
|
)
|
|
if (err(_node)) return _node
|
|
const variableDeclarationName = _node.node?.declaration?.id?.name || ''
|
|
const sg = sketchFromKclValue(
|
|
variables[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 = createCallExpressionStdLibKw('line', null, [
|
|
createLabeledArg(
|
|
ARG_END,
|
|
createArrayExpression([createLiteral(0), createLiteral(0)])
|
|
),
|
|
])
|
|
} else {
|
|
newSegment = createCallExpressionStdLibKw('tangentialArc', null, [
|
|
createLabeledArg(
|
|
ARG_END_ABSOLUTE,
|
|
createArrayExpression([
|
|
createLiteral(lastSeg.to[0]),
|
|
createLiteral(lastSeg.to[1]),
|
|
])
|
|
),
|
|
])
|
|
}
|
|
;(
|
|
(_ast.body[bodyStartIndex] 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[bodyStartIndex] as VariableDeclaration)
|
|
.declaration.init as PipeExpression
|
|
).body.slice(-1)[0]
|
|
;(
|
|
(_ast.body[bodyStartIndex] as VariableDeclaration).declaration
|
|
.init as PipeExpression
|
|
).body.slice(-1)[0].start = lastPipeItem.start
|
|
|
|
_ast.end = lastPipeItem.end
|
|
const varDec = _ast.body[bodyStartIndex] 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.slice(bodyStartIndex, bodyEndIndex + 1)),
|
|
}
|
|
|
|
return {
|
|
truncatedAst,
|
|
variableDeclarationName,
|
|
}
|
|
}
|
|
|
|
function sketchFromPathToNode({
|
|
pathToNode,
|
|
ast,
|
|
variables,
|
|
kclManager,
|
|
}: {
|
|
pathToNode: PathToNode
|
|
ast: Program
|
|
variables: VariableMap
|
|
kclManager: KclManager
|
|
}): Sketch | null | Error {
|
|
const _varDec = getNodeFromPath<VariableDeclarator>(
|
|
kclManager.ast,
|
|
pathToNode,
|
|
'VariableDeclarator'
|
|
)
|
|
if (err(_varDec)) return _varDec
|
|
const varDec = _varDec.node
|
|
const result = variables[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,
|
|
kclManager: KclManager
|
|
): Quaternion | Error {
|
|
const sketch = sketchFromPathToNode({
|
|
pathToNode: sketchPathToNode,
|
|
ast: kclManager.ast,
|
|
variables: kclManager.variables,
|
|
kclManager,
|
|
})
|
|
if (err(sketch)) return sketch
|
|
const zAxis =
|
|
sketch?.on.xAxis && sketch?.on.yAxis
|
|
? crossProduct(sketch?.on.xAxis, sketch?.on.yAxis)
|
|
: sketchNormalBackUp
|
|
if (!zAxis) return Error('Sketch zAxis not found')
|
|
|
|
return getQuaternionFromZAxis(massageFormats(zAxis))
|
|
}
|
|
|
|
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 | { x: number; y: number; z: number }
|
|
): Vector3 {
|
|
return isArray(a) ? new Vector3(a[0], a[1], a[2]) : new Vector3(a.x, a.y, a.z)
|
|
}
|
|
|
|
function getSketchesInfo({
|
|
sketchNodePaths,
|
|
ast,
|
|
variables,
|
|
kclManager,
|
|
}: {
|
|
sketchNodePaths: PathToNode[]
|
|
ast: Node<Program>
|
|
variables: VariableMap
|
|
kclManager: KclManager
|
|
}): {
|
|
sketch: Sketch
|
|
pathToNode: PathToNode
|
|
}[] {
|
|
const sketchesInfo: {
|
|
sketch: Sketch
|
|
pathToNode: PathToNode
|
|
}[] = []
|
|
for (const path of sketchNodePaths) {
|
|
const sketch = sketchFromPathToNode({
|
|
pathToNode: path,
|
|
ast,
|
|
variables,
|
|
kclManager,
|
|
})
|
|
if (err(sketch)) continue
|
|
if (!sketch) continue
|
|
sketchesInfo.push({
|
|
sketch,
|
|
pathToNode: path,
|
|
})
|
|
}
|
|
return sketchesInfo
|
|
}
|
|
|
|
/**
|
|
* 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>,
|
|
kclManager: KclManager
|
|
): Selections {
|
|
const artifactGraph = kclManager.artifactGraph
|
|
const artifact = getArtifactFromRange(sourceRange, artifactGraph) || undefined
|
|
const selection: Selections = {
|
|
graphSelections: [
|
|
{
|
|
artifact,
|
|
codeRef: codeRefFromRange(sourceRange, ast),
|
|
},
|
|
],
|
|
otherSelections: [],
|
|
}
|
|
return selection
|
|
}
|
|
|
|
function isGroupStartProfileForCurrentProfile(sketchEntryNodePath: PathToNode) {
|
|
return (group: Group<Object3DEventMap> | null) => {
|
|
if (group?.name !== PROFILE_START) return false
|
|
const groupExpressionIndex = Number(group.userData.pathToNode[1][0])
|
|
const isProfileStartOfCurrentExpr =
|
|
groupExpressionIndex === sketchEntryNodePath[1][0]
|
|
return isProfileStartOfCurrentExpr
|
|
}
|
|
}
|
|
|
|
// Returns the 2D tangent direction vector at the end of the segmentGroup if it's an arc.
|
|
function findTangentDirection(segmentGroup: Group) {
|
|
let tangentDirection: Coords2d | undefined
|
|
if (segmentGroup.userData.type === TANGENTIAL_ARC_TO_SEGMENT) {
|
|
const prevSegment = segmentGroup.userData.prevSegment
|
|
const arcInfo = getTangentialArcToInfo({
|
|
arcStartPoint: segmentGroup.userData.from,
|
|
arcEndPoint: segmentGroup.userData.to,
|
|
tanPreviousPoint: getTanPreviousPoint(prevSegment),
|
|
obtuse: true,
|
|
})
|
|
const tangentAngle =
|
|
arcInfo.endAngle + (Math.PI / 2) * (arcInfo.ccw ? 1 : -1)
|
|
tangentDirection = [Math.cos(tangentAngle), Math.sin(tangentAngle)]
|
|
} else if (
|
|
segmentGroup.userData.type === ARC_SEGMENT ||
|
|
segmentGroup.userData.type === THREE_POINT_ARC_SEGMENT
|
|
) {
|
|
const tangentAngle =
|
|
deg2Rad(
|
|
getAngle(segmentGroup.userData.center, segmentGroup.userData.to)
|
|
) +
|
|
(Math.PI / 2) * (segmentGroup.userData.ccw ? 1 : -1)
|
|
tangentDirection = [Math.cos(tangentAngle), Math.sin(tangentAngle)]
|
|
} else {
|
|
console.warn(
|
|
'Unsupported segment type for tangent direction calculation: ',
|
|
segmentGroup.userData.type
|
|
)
|
|
}
|
|
return tangentDirection
|
|
}
|