This commit is contained in:
49lf
2025-01-24 15:58:54 -05:00
parent 32e8975799
commit 90086488b5
9 changed files with 651 additions and 418 deletions

View File

@ -0,0 +1,436 @@
import {
Group,
Mesh,
Vector3,
Vector2,
Object3D,
SphereGeometry,
MeshBasicMaterial,
Color,
BufferGeometry,
LineDashedMaterial,
Line,
} from 'three'
import { PathToNode, recast, parse, VariableDeclaration } from 'lang/wasm'
import { getNodeFromPath } from 'lang/queryAst'
import { LabeledArg } from 'wasm-lib/kcl/bindings/LabeledArg'
import { Literal } from 'wasm-lib/kcl/bindings/Literal'
import { calculate_circle_from_3_points } from 'wasm-lib/pkg/wasm_lib'
import { findUniqueName, createPipeExpression } from 'lang/modifyAst'
import { getThemeColorForThreeJs } from 'lib/theme'
import { err } from 'lib/trap'
import { codeManager } from 'lib/singletons'
import { SketchTool, NoOpTool } from './interfaceSketchTool'
import { createCircleGeometry } from './segments'
import { quaternionFromUpNForward } from './helpers'
import {
SKETCH_LAYER,
CIRCLE_3_POINT_DRAFT_POINT,
CIRCLE_3_POINT_DRAFT_CIRCLE,
} from './sceneInfra'
interface InitArgs {
scene: Scene
intersectionPlane: any
startSketchOnASTNodePath: PathToNode
maybeExistingNodePath: PathToNode
forward: Vector3
up: Vector3
sketchOrigin: Vector3
// I want the intention to be clear, but keep done() semantics general.
callDoneFnAfterBeingDefined?: true
done: () => void
}
// The reason why InitArgs are not part of the CircleThreePoint constructor is
// because we may want to re-initialize the same instance many times, w/ init()
export function CircleThreePoint(initArgs: InitArgs): SketchTool {
// lee: This is a bit long, but subsequent sketch tools don't need to detail
// all this. Use this as a template for other tools.
// The KCL to generate. Parses into an AST to be modified / manipulated.
const selfASTNode = parse(`profileVarNameToBeReplaced = circleThreePoint(
sketchVarNameToBeReplaced,
p1 = [0.0, 0.0],
p2 = [0.0, 0.0],
p3 = [0.0, 0.0],
)`)
// AST node to work with. It's either an existing one, or a new one.
let isNewSketch = true
const astSnapshot = structuredClone(kclManager.ast)
if (initArgs.maybeExistingNodePath.length === 0) {
// Travel 1 node up from the sketch plane AST node, and append or
// update the new profile AST node.
// Get the index of the sketch AST node.
// (It could be in a program or function body!)
// (It could be in the middle of anywhere in the program!)
// ['body', 'index']
// [8, 'index'] <- ...[1]?.[0] refers to 8.
const nextIndex = initArgs.startSketchOnASTNodePath[
// - 3 puts us at the body of a function or the overall program
initArgs.startSketchOnASTNodePath.length - 3
][0] + 1
const bodyASTNode = getNodeFromPath<VariableDeclaration>(
astSnapshot,
Array.from(initArgs.startSketchOnASTNodePath).splice(
0,
initArgs.startSketchOnASTNodePath.length - 3
),
'VariableDeclaration'
)
// In the event of an error, we return a no-op tool.
// Should maybe consider something else, like ExceptionTool or something.
// Logically there should never be an error.
if (err(bodyASTNode)) return new NoOpTool()
// Attach the node
bodyASTNode.node.splice(nextIndex, 0, selfASTNode.program.body[0])
} else {
selfASTNode = getNodeFromPath<VariableDeclaration>(
kclManager.ast,
initArgs.maybeExistingNodePath,
'VariableDeclaration'
)
if (err(selfASTNode)) return new NoOpTool()
isNewSketch = false
}
// Keep track of points in the scene with their ThreeJS ids.
const points: Map<number, Vector2> = new Map()
// Keep a reference so we can destroy and recreate as needed.
let groupCircle: Group | undefined
// Add our new group to the list of groups to render
const groupOfDrafts = new Group()
groupOfDrafts.name = 'circle-3-point-group'
groupOfDrafts.layers.set(SKETCH_LAYER)
groupOfDrafts.traverse((child) => {
child.layers.set(SKETCH_LAYER)
})
initArgs.scene.add(groupOfDrafts)
// lee: Not a fan we need to re-iterate this dummy object all over the place
// just to get the scale but okie dokie.
const dummy = new Mesh()
dummy.position.set(0, 0, 0)
const scale = sceneInfra.getClientSceneScaleFactor(dummy)
// How large the points on the circle will render as
const DRAFT_POINT_RADIUS = 10 // px
// The target of our dragging
let target: Object3D | undefined = undefined
this.destroy = async () => {
initArgs.scene.remove(groupOfDrafts)
}
const createPoint = (
center: Vector3,
// This is to draw dots with no interactions; purely visual.
opts?: { noInteraction?: boolean }
): Mesh => {
const geometry = new SphereGeometry(DRAFT_POINT_RADIUS)
const color = getThemeColorForThreeJs(sceneInfra._theme)
const material = new MeshBasicMaterial({
color: opts?.noInteraction
? sceneInfra._theme === 'light'
? new Color(color).multiplyScalar(0.15)
: new Color(0x010101).multiplyScalar(2000)
: color,
})
const mesh = new Mesh(geometry, material)
mesh.userData = {
type: opts?.noInteraction ? 'ghost' : CIRCLE_3_POINT_DRAFT_POINT,
}
mesh.renderOrder = 1000
mesh.layers.set(SKETCH_LAYER)
mesh.position.copy(center)
mesh.scale.set(scale, scale, scale)
mesh.renderOrder = 100
return mesh
}
const createCircleThreePointGraphic = async (
points: Vector2[],
center: Vector2,
radius: number
) => {
if (
Number.isNaN(radius) ||
Number.isNaN(center.x) ||
Number.isNaN(center.y)
)
return
const color = getThemeColorForThreeJs(sceneInfra._theme)
const lineCircle = createCircleGeometry({
center: [center.x, center.y],
radius,
color,
isDashed: false,
scale: 1,
})
lineCircle.userData = { type: CIRCLE_3_POINT_DRAFT_CIRCLE }
lineCircle.layers.set(SKETCH_LAYER)
if (groupCircle) groupOfDrafts.remove(groupCircle)
groupCircle = new Group()
groupCircle.renderOrder = 1
groupCircle.add(lineCircle)
const pointMesh = createPoint(new Vector3(center.x, center.y, 0), {
noInteraction: true,
})
groupCircle.add(pointMesh)
const geometryPolyLine = new BufferGeometry().setFromPoints([
...points.map((p) => new Vector3(p.x, p.y, 0)),
new Vector3(points[0].x, points[0].y, 0),
])
const materialPolyLine = new LineDashedMaterial({
color,
scale: 1 / scale,
dashSize: 6,
gapSize: 6,
})
const meshPolyLine = new Line(geometryPolyLine, materialPolyLine)
meshPolyLine.computeLineDistances()
groupCircle.add(meshPolyLine)
groupOfDrafts.add(groupCircle)
}
const insertCircleThreePointKclIntoASTSnapshot = (
points: Vector2[],
): Program => {
// Make TypeScript happy about selfASTNode property accesses.
if (err(selfASTNode) || selfASTNode.program === null)
return kclManager.ast
if (selfASTNode.program.body[0].type !== 'VariableDeclaration')
return kclManager.ast
if (
selfASTNode.program.body[0].declaration.init.type !==
'CallExpressionKw'
)
return kclManager.ast
// Make accessing the labeled arguments easier / less verbose
const arg = (x: LabeledArg): Literal[] | undefined => {
if (
'arg' in x &&
'elements' in x.arg &&
x.arg.type === 'ArrayExpression'
) {
if (x.arg.elements.every((x) => x.type === 'Literal')) {
return x.arg.elements
}
}
return undefined
}
// Set the `profileXXX =` variable name if not set
if (
selfASTNode.program.body[0].declaration.id.name ===
'profileVarNameToBeReplaced'
) {
const profileVarName = findUniqueName(astSnapshot, 'profile')
selfASTNode.program.body[0].declaration.id.name = profileVarName
}
// Used to get the sketch variable name
const startSketchOnASTNode = getNodeFromPath<VariableDeclaration>(
astSnapshot,
initArgs.startSketchOnASTNodePath,
'VariableDeclaration'
)
if (err(startSketchOnASTNode)) return astSnapshot
// Set the sketch variable name
if (/^sketch/.test(startSketchOnASTNode.node.declaration.id.name)) {
selfASTNode.program.body[0].declaration.init.unlabeled.name =
startSketchOnASTNode.node.declaration.id.name
}
// Set the points 1-3
const selfASTNodeArgs =
selfASTNode.program.body[0].declaration.init.arguments
const arg0 = arg(selfASTNodeArgs[0])
if (!arg0) return kclManager.ast
arg0[0].value = points[0].x
arg0[0].raw = points[0].x.toString()
arg0[1].value = points[0].y
arg0[1].raw = points[0].y.toString()
const arg1 = arg(selfASTNodeArgs[1])
if (!arg1) return kclManager.ast
arg1[0].value = points[1].x
arg1[0].raw = points[1].x.toString()
arg1[1].value = points[1].y
arg1[1].raw = points[1].y.toString()
const arg2 = arg(selfASTNodeArgs[2])
if (!arg2) return kclManager.ast
arg2[0].value = points[2].x
arg2[0].raw = points[2].x.toString()
arg2[1].value = points[2].y
arg2[1].raw = points[2].y.toString()
// Return the `Program`
return astSnapshot
}
this.init = () => {
groupOfDrafts.position.copy(initArgs.sketchOrigin)
const orientation = quaternionFromUpNForward(initArgs.up, initArgs.forward)
// Reminder: the intersection plane is the primary way to derive a XY
// position from a mouse click in ThreeJS.
// Here, we position and orient so it's facing the viewer.
initArgs.intersectionPlane!.setRotationFromQuaternion(orientation)
initArgs.intersectionPlane!.position.copy(initArgs.sketchOrigin)
// lee: I'm keeping this here as a developer gotchya:
// If you use 3D points, do not rotate anything.
// If you use 2D points (easier to deal with, generally do this!), then
// rotate the group just like this! Remember to rotate other groups too!
groupOfDrafts.setRotationFromQuaternion(orientation)
initArgs.scene.add(groupOfDrafts)
// We're not working with an existing circleThreePoint.
if (isNewSketch) return
// Otherwise, we are :)
// Use the points in the AST as starting points.
const maybeVariableDeclaration = getNodeFromPath<VariableDeclaration>(
astSnapshot,
selfASTNode,
'VariableDeclaration'
)
// This should never happen.
if (err(maybeVariableDeclaration))
return Promise.reject(maybeVariableDeclaration)
const maybeCallExpressionKw = maybeVariableDeclaration.node.declaration.init
if (
maybeCallExpressionKw.type === 'CallExpressionKw' &&
maybeCallExpressionKw.callee.name === 'circleThreePoint'
) {
maybeCallExpressionKw?.arguments
.map(
({ arg }: any) =>
new Vector2(arg.elements[0].value, arg.elements[1].value)
)
.forEach((point: Vector2) => {
const pointMesh = createPoint(new Vector3(point.x, point.y, 0))
groupOfDrafts.add(pointMesh)
points.set(pointMesh.id, point)
})
void this.update()
}
}
this.update = async () => {
const points_ = Array.from(points.values())
const circleParams = calculate_circle_from_3_points(
points_[0].x,
points_[0].y,
points_[1].x,
points_[1].y,
points_[2].x,
points_[2].y
)
if (Number.isNaN(circleParams.radius)) return
await createCircleThreePointGraphic(
points_,
new Vector2(circleParams.center_x, circleParams.center_y),
circleParams.radius
)
const astWithNewCode = insertCircleThreePointKclIntoASTSnapshot(points_)
const codeAsString = recast(astWithNewCode)
if (err(codeAsString)) return
codeManager.updateCodeStateEditor(codeAsString)
return astWithNewCode
}
this.onDrag = async (args) => {
const draftPointsIntersected = args.intersects.filter(
(intersected) =>
intersected.object.userData.type === CIRCLE_3_POINT_DRAFT_POINT
)
const firstPoint = draftPointsIntersected[0]
if (firstPoint && !target) {
target = firstPoint.object
}
// The user was off their mark! Missed the object to select.
if (!target) return
target.position.copy(
new Vector3(
args.intersectionPoint.twoD.x,
args.intersectionPoint.twoD.y,
0
)
)
points.set(target.id, args.intersectionPoint.twoD)
if (points.size <= 2) return
await this.update()
}
this.onDragEnd = async (_args) => {
target = undefined
}
this.onClick = async (args) => {
if (points.size >= 3) return
if (!args.intersectionPoint) return
const pointMesh = createPoint(
new Vector3(
args.intersectionPoint.twoD.x,
args.intersectionPoint.twoD.y,
0
)
)
groupOfDrafts.add(pointMesh)
points.set(pointMesh.id, args.intersectionPoint.twoD)
if (points.size <= 2) return
const astWithNewCode = await this.update()
if (initArgs.callDoneFnAfterBeingDefined) {
// We "fake" execute to update the overall program memory.
// setupSketch needs that memory to be updated.
// We only do it at the very last moment before passing off control
// because this sketch tool logic doesn't need that at all, and is
// needless (sometimes heavy, but not here) computation.
await kclManager.executeAstMock(astWithNewCode)
initArgs.done()
}
}
}

View File

@ -0,0 +1,30 @@
import { SceneInfra } from './sceneInfra'
export interface SketchTool {
init: () => void
// Update could mean draw, refresh editor state, etc. It's up to the
// SketchTool implementer.
update: () => void
// Clean up the state (such as ThreeJS scene)
destroy: () => void
// To be hooked into sceneInfra.callbacks or other places as necessary.
// All the necessary types exist in SceneInfra. If it ever majorly changes
// we want this to break such that they are corrected too.
onDragStart?: (typeof SceneInfra)['onDragStartCallback']
onDragEnd?: (typeof SceneInfra)['onDragEndCallback']
onDrag?: (typeof SceneInfra)['onDragCallback']
onMove?: (typeof SceneInfra)['onMoveCallback']
onClick?: (typeof SceneInfra)['onClickCallback']
onMouseEnter?: (typeof SceneInfra)['onMouseEnterCallback']
onMouseLeave?: (typeof SceneInfra)['onMouseLeaveCallback']
}
export function NoOpTool(): SketchTool {
this.init = () => {}
this.update = () => {}
this.destroy = () => {}
return this
}

View File

@ -36,8 +36,6 @@ import {
SKETCH_LAYER,
X_AXIS,
Y_AXIS,
CIRCLE_3_POINT_DRAFT_POINT,
CIRCLE_3_POINT_DRAFT_CIRCLE,
} from './sceneInfra'
import { isQuaternionVertical, quaternionFromUpNForward } from './helpers'
import {
@ -58,7 +56,6 @@ import {
resultIsOk,
SourceRange,
} from 'lang/wasm'
import { calculate_circle_from_3_points } from '../wasm-lib/pkg/wasm_lib'
import {
engineCommandManager,
kclManager,
@ -74,6 +71,7 @@ import {
SegmentUtils,
segmentUtils,
} from './segments'
import { CircleThreePoint } from './circleThreePoint'
import {
addCallExpressionsToPipe,
addCloseToPipe,
@ -163,6 +161,7 @@ export class SceneEntities {
scene: Scene
sceneProgramMemory: ProgramMemory = ProgramMemory.empty()
activeSegments: { [key: string]: Group } = {}
sketchTools: SketchTool[]
intersectionPlane: Mesh | null = null
axisGroup: Group | null = null
draftPointGroups: Group[] = []
@ -585,6 +584,8 @@ export class SceneEntities {
programMemory,
})
this.sketchTools = []
this.sceneProgramMemory = programMemory
const group = new Group()
position && group.position.set(...position)
@ -669,6 +670,20 @@ export class SceneEntities {
if (err(_node1)) return
const callExpName = _node1.node?.callee?.name
if (segment.type === 'CircleThreePoint') {
const circleThreePoint = new CircleThreePoint({
scene: this.scene,
intersectionPlane: this.intersectionPlane,
startSketchOnASTNodePath: segPathToNode,
forward,
up,
sketchOrigin: position,
})
circleThreePoint.init()
this.sketchTools.push(circleThreePoint)
return
}
const initSegment =
segment.type === 'TangentialArcTo'
? segmentUtils.tangentialArcTo.init
@ -1381,349 +1396,6 @@ export class SceneEntities {
})
return { updatedEntryNodePath, updatedSketchNodePaths }
}
// lee: Well, it appears all our code in sceneEntities each act as their own
// kind of classes. In this case, I'll keep utility functions pertaining to
// circle3Point here. Feel free to extract as needed.
entryDraftCircle3Point = (
done: () => void,
startSketchOnASTNodePath: PathToNode,
forward: Vector3,
up: Vector3,
sketchOrigin: Vector3
): (() => void) => {
// lee: Not a fan we need to re-iterate this dummy object all over the place
// just to get the scale but okie dokie.
const dummy = new Mesh()
dummy.position.set(0, 0, 0)
const scale = sceneInfra.getClientSceneScaleFactor(dummy)
const orientation = quaternionFromUpNForward(up, forward)
// Reminder: the intersection plane is the primary way to derive a XY
// position from a mouse click in ThreeJS.
// Here, we position and orient so it's facing the viewer.
this.intersectionPlane!.setRotationFromQuaternion(orientation)
this.intersectionPlane!.position.copy(sketchOrigin)
// Keep track of points in the scene with their ThreeJS ids.
const points: Map<number, Vector2> = new Map()
// Keep a reference so we can destroy and recreate as needed.
let groupCircle: Group | undefined
// Add our new group to the list of groups to render
const groupOfDrafts = new Group()
groupOfDrafts.name = 'circle-3-point-group'
groupOfDrafts.position.copy(sketchOrigin)
// lee: I'm keeping this here as a developer gotchya:
// If you use 3D points, do not rotate anything.
// If you use 2D points (easier to deal with, generally do this!), then
// rotate the group just like this! Remember to rotate other groups too!
groupOfDrafts.setRotationFromQuaternion(orientation)
this.scene.add(groupOfDrafts)
// How large the points on the circle will render as
const DRAFT_POINT_RADIUS = 10 // px
// The target of our dragging
let target: Object3D | undefined = undefined
// The KCL this will generate.
const kclCircle3Point = parse(`profileVarNameToBeReplaced = circleThreePoint(
sketchVarNameToBeReplaced,
p1 = [0.0, 0.0],
p2 = [0.0, 0.0],
p3 = [0.0, 0.0],
)`)
const createPoint = (
center: Vector3,
opts?: { noInteraction?: boolean }
): Mesh => {
const geometry = new SphereGeometry(DRAFT_POINT_RADIUS)
const color = getThemeColorForThreeJs(sceneInfra._theme)
const material = new MeshBasicMaterial({
color: opts?.noInteraction
? sceneInfra._theme === 'light'
? new Color(color).multiplyScalar(0.15)
: new Color(0x010101).multiplyScalar(2000)
: color,
})
const mesh = new Mesh(geometry, material)
mesh.userData = {
type: opts?.noInteraction ? 'ghost' : CIRCLE_3_POINT_DRAFT_POINT,
}
mesh.renderOrder = 1000
mesh.layers.set(SKETCH_LAYER)
mesh.position.copy(center)
mesh.scale.set(scale, scale, scale)
mesh.renderOrder = 100
return mesh
}
const createCircle3PointGraphic = async (
points: Vector2[],
center: Vector2,
radius: number
) => {
if (
Number.isNaN(radius) ||
Number.isNaN(center.x) ||
Number.isNaN(center.y)
)
return
const color = getThemeColorForThreeJs(sceneInfra._theme)
const lineCircle = createCircleGeometry({
center: [center.x, center.y],
radius,
color,
isDashed: false,
scale: 1,
})
lineCircle.userData = { type: CIRCLE_3_POINT_DRAFT_CIRCLE }
lineCircle.layers.set(SKETCH_LAYER)
// devnote: it's a mistake to use these with EllipseCurve :)
// lineCircle.position.set(center.x, center.y, 0)
// lineCircle.scale.set(scale, scale, scale)
if (groupCircle) groupOfDrafts.remove(groupCircle)
groupCircle = new Group()
groupCircle.renderOrder = 1
groupCircle.add(lineCircle)
const pointMesh = createPoint(new Vector3(center.x, center.y, 0), {
noInteraction: true,
})
groupCircle.add(pointMesh)
const geometryPolyLine = new BufferGeometry().setFromPoints([
...points.map((p) => new Vector3(p.x, p.y, 0)),
new Vector3(points[0].x, points[0].y, 0),
])
const materialPolyLine = new LineDashedMaterial({
color,
scale: 1 / scale,
dashSize: 6,
gapSize: 6,
})
const meshPolyLine = new Line(geometryPolyLine, materialPolyLine)
meshPolyLine.computeLineDistances()
groupCircle.add(meshPolyLine)
groupOfDrafts.add(groupCircle)
}
const insertCircle3PointKclIntoAstSnapshot = (
points: Vector2[],
sketchVarName: string
): Program => {
if (err(kclCircle3Point) || kclCircle3Point.program === null)
return kclManager.ast
if (kclCircle3Point.program.body[0].type !== 'VariableDeclaration')
return kclManager.ast
if (
kclCircle3Point.program.body[0].declaration.init.type !== 'CallExpressionKw'
)
return kclManager.ast
// Make accessing the labeled arguments easier / less verbose
const arg = (x: LabeledArg): Literal[] | undefined => {
if (
'arg' in x &&
'elements' in x.arg &&
x.arg.type === 'ArrayExpression'
) {
if (x.arg.elements.every((x) => x.type === 'Literal')) {
return x.arg.elements
}
}
return undefined
}
// Set the `profileXXX =` variable name if not set
if (kclCircle3Point.program.body[0].declaration.id.name === 'profileVarNameToBeReplaced') {
const profileVarName = findUniqueName(_ast, 'profile')
kclCircle3Point.program.body[0].declaration.id.name = profileVarName
}
// Set the sketch variable name
kclCircle3Point.program.body[0].declaration.init.unlabeled.name = sketchVarName
// Set the points 1-3
const kclCircle3PointArgs =
kclCircle3Point.program.body[0].declaration.init.arguments
const arg0 = arg(kclCircle3PointArgs[0])
if (!arg0) return kclManager.ast
arg0[0].value = points[0].x
arg0[0].raw = points[0].x.toString()
arg0[1].value = points[0].y
arg0[1].raw = points[0].y.toString()
const arg1 = arg(kclCircle3PointArgs[1])
if (!arg1) return kclManager.ast
arg1[0].value = points[1].x
arg1[0].raw = points[1].x.toString()
arg1[1].value = points[1].y
arg1[1].raw = points[1].y.toString()
const arg2 = arg(kclCircle3PointArgs[2])
if (!arg2) return kclManager.ast
arg2[0].value = points[2].x
arg2[0].raw = points[2].x.toString()
arg2[1].value = points[2].y
arg2[1].raw = points[2].y.toString()
const astSnapshot = structuredClone(kclManager.ast)
const startSketchOnASTNode = getNodeFromPath<VariableDeclaration>(
astSnapshot,
startSketchOnASTNodePath,
'VariableDeclaration'
)
if (err(startSketchOnASTNode)) return astSnapshot
// It's possible we're not the first profile on this sketch!
// It's also possible we've already added this profile, so modify it.
if (
startSketchOnASTNode.node.declaration.init.type === 'PipeExpression' &&
startSketchOnASTNode.node.declaration.init.body[1].type ===
'CallExpressionKw' &&
startSketchOnASTNode.node.declaration.init.body.length >= 2
) {
startSketchOnASTNode.node.declaration.init.body[1].arguments =
kclCircle3Point.program.body[0].expression.arguments
} else {
// Clone a new node based on the old, and replace the old with the new.
const clonedStartSketchOnASTNode = structuredClone(startSketchOnASTNode)
startSketchOnASTNode.node.declaration.init = createPipeExpression([
clonedStartSketchOnASTNode.node.declaration.init,
kclCircle3Point.program.body[0].expression,
])
}
// Return the `Program`
return astSnapshot
}
const updateCircle3Point = async (opts?: { execute?: true }) => {
const points_ = Array.from(points.values())
const circleParams = calculate_circle_from_3_points(
points_[0].x,
points_[0].y,
points_[1].x,
points_[1].y,
points_[2].x,
points_[2].y
)
if (Number.isNaN(circleParams.radius)) return
await createCircle3PointGraphic(
points_,
new Vector2(circleParams.center_x, circleParams.center_y),
circleParams.radius
)
const astWithNewCode = insertCircle3PointKclIntoAstSnapshot(points_)
const codeAsString = recast(astWithNewCode)
if (err(codeAsString)) return
codeManager.updateCodeStateEditor(codeAsString)
}
const cleanupFn = () => {
this.scene.remove(groupOfDrafts)
}
// The AST node we extracted earlier may already have a circleThreePoint!
// Use the points in the AST as starting points.
const astSnapshot = structuredClone(kclManager.ast)
const maybeVariableDeclaration = getNodeFromPath<VariableDeclaration>(
astSnapshot,
startSketchOnASTNodePath,
'VariableDeclaration'
)
if (err(maybeVariableDeclaration))
return () => {
done()
}
const maybeCallExpressionKw = maybeVariableDeclaration.node.declaration.init
if (
maybeCallExpressionKw.type === 'VariableDeclaration' &&
maybeCallExpressionKw.body[1].type === 'CallExpressionKw' &&
maybeCallExpressionKw.body[1]?.callee.name === 'circleThreePoint'
) {
maybeCallExpressionKw?.body[1].arguments
.map(
({ arg }: any) =>
new Vector2(arg.elements[0].value, arg.elements[1].value)
)
.forEach((point: Vector2) => {
const pointMesh = createPoint(new Vector3(point.x, point.y, 0))
groupOfDrafts.add(pointMesh)
points.set(pointMesh.id, point)
})
void updateCircle3Point()
}
sceneInfra.setCallbacks({
async onDrag(args) {
const draftPointsIntersected = args.intersects.filter(
(intersected) =>
intersected.object.userData.type === CIRCLE_3_POINT_DRAFT_POINT
)
const firstPoint = draftPointsIntersected[0]
if (firstPoint && !target) {
target = firstPoint.object
}
// The user was off their mark! Missed the object to select.
if (!target) return
target.position.copy(
new Vector3(
args.intersectionPoint.twoD.x,
args.intersectionPoint.twoD.y,
0
)
)
points.set(target.id, args.intersectionPoint.twoD)
if (points.size <= 2) return
await updateCircle3Point()
},
async onDragEnd(_args) {
target = undefined
},
async onClick(args) {
if (points.size >= 3) return
if (!args.intersectionPoint) return
const pointMesh = createPoint(
new Vector3(
args.intersectionPoint.twoD.x,
args.intersectionPoint.twoD.y,
0
)
)
groupOfDrafts.add(pointMesh)
points.set(pointMesh.id, args.intersectionPoint.twoD)
if (points.size <= 2) return
await updateCircle3Point()
},
})
return cleanupFn
}
setupDraftCircle = async (
sketchEntryNodePath: PathToNode,
sketchNodePaths: PathToNode[],
@ -2577,7 +2249,7 @@ function prepareTruncatedMemoryAndAst(
'VariableDeclaration'
)
if (err(_node)) return _node
const variableDeclarationName = _node.node?.declaration.id?.name || ''
const variableDeclarationName = _node.node?.declaration?.id?.name || ''
const sg = sketchFromKclValue(
programMemory.get(variableDeclarationName),
variableDeclarationName

View File

@ -1355,7 +1355,7 @@ export const ModelingMachineProvider = ({
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
}
sceneInfra.resetMouseListeners()
await sceneEntitiesManager.setupSketch({
const { sketchTools } = await sceneEntitiesManager.setupSketch({
sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [],
sketchNodePaths: sketchDetails.sketchNodePaths,
forward: sketchDetails.zAxis,
@ -1373,7 +1373,12 @@ export const ModelingMachineProvider = ({
position: sketchDetails.origin,
sketchNodePaths: sketchDetails.sketchNodePaths,
planeNodePath: sketchDetails.planeNodePath,
// We will want to pass sketchTools here
// to add their interactions
})
// We will want to update the context with sketchTools.
// They'll be used for their .destroy() in tearDownSketch
return undefined
}
),

View File

@ -446,8 +446,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
icon: 'circle',
status: 'available',
title: 'Center circle',
disabled: (state) =>
state.matches('Sketch no face'),
disabled: (state) => state.matches('Sketch no face'),
isActive: (state) =>
state.matches({ Sketch: 'Circle tool' }) ||
state.matches({ Sketch: 'circle3PointToolSelect' }),
@ -464,9 +463,9 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
type: 'change tool',
data: {
tool: !modelingState.matches({
Sketch: 'circle3PointToolSelect',
Sketch: 'circleThreePointToolSelect',
})
? 'circle3Points'
? 'circleThreePoint'
: 'none',
},
}),

View File

@ -84,6 +84,7 @@ import { MachineManager } from 'components/MachineManagerProvider'
import { addShell } from 'lang/modifyAst/addShell'
import { KclCommandValue } from 'lib/commandTypes'
import { getPathsFromPlaneArtifact } from 'lang/std/artifactGraph'
import { CircleThreePoint } from '../clientSideScene/circleThreePoint'
export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY'
@ -228,7 +229,7 @@ export type SketchTool =
| 'rectangle'
| 'center rectangle'
| 'circle'
| 'circle3Points'
| 'circleThreePoints'
| 'none'
export type ModelingMachineEvent =
@ -432,8 +433,6 @@ export const modelingMachine = setup({
'has valid selection for deletion': () => false,
'is editing existing sketch': ({ context: { sketchDetails } }) =>
isEditingExistingSketch({ sketchDetails }),
'is editing 3-point circle': ({ context: { sketchDetails } }) =>
isEditing3PointCircle({ sketchDetails }),
'Can make selection horizontal': ({ context: { selectionRanges } }) => {
const info = horzVertInfo(selectionRanges, 'horizontal')
if (trap(info)) return false
@ -583,8 +582,8 @@ export const modelingMachine = setup({
currentTool === 'center rectangle',
'next is circle': ({ context: { currentTool } }) =>
currentTool === 'circle',
'next is circle 3 point': ({ context: { currentTool } }) =>
currentTool === 'circle3Points',
'next is circle three point': ({ context: { currentTool } }) =>
currentTool === 'circleThreePoint',
'next is line': ({ context }) => context.currentTool === 'line',
'next is none': ({ context }) => context.currentTool === 'none',
},
@ -977,6 +976,7 @@ export const modelingMachine = setup({
},
'update sketchDetails': assign(({ event, context }) => {
if (
event.type !== 'xstate.done.actor.actor-circle-three-point' &&
event.type !== 'xstate.done.actor.set-up-draft-circle' &&
event.type !== 'xstate.done.actor.set-up-draft-rectangle' &&
event.type !== 'xstate.done.actor.set-up-draft-center-rectangle' &&
@ -1863,7 +1863,7 @@ export const modelingMachine = setup({
// lee: I REALLY wanted to inline this at the location of the actor invocation
// but the type checker loses it's fricking mind because the `actors` prop
// this exists on now doesn't have the correct type if I do that. *agh*.
actorCircle3Point: fromCallback<
actorCircleThreePoint: fromCallback<
{ type: '' }, // Not used. We receive() no events in this actor.
SketchDetails | undefined,
// Doesn't type-check anything for some reason.
@ -1873,17 +1873,39 @@ export const modelingMachine = setup({
// destroying the actor and going back to idle state.
if (!sketchDetails) return
const cleanupFn = sceneEntitiesManager.entryDraftCircle3Point(
// I make it clear that the stop is coming from an internal call
() => sendBack({ type: 'stop-internal' }),
sketchDetails.planeNodePath,
new Vector3(...sketchDetails.zAxis),
new Vector3(...sketchDetails.yAxis),
new Vector3(...sketchDetails.origin)
)
let tool = new CircleThreePoint({
scene: sceneEntitiesManager.scene,
intersectionPlane: sceneEntitiesManager.intersectionPlane,
startSketchOnASTNodePath: sketchDetails.planeNodePath,
maybeExistingNodePath: sketchDetails.sketchEntryNodePath,
forward: new Vector3(...sketchDetails.zAxis),
up: new Vector3(...sketchDetails.yAxis),
sketchOrigin: new Vector3(...sketchDetails.origin),
// Needed because of our current architecture of initializing
// shapes and then immediately entering "generic" sketch editing mode.
callDoneFnAfterBeingDefined: true,
done(output) {
sendBack({
type: 'xstate.done.actor.actor-circle-three-point',
output: {
updatedPlaneNodePath,
updatedEntryNodePath,
updatedSketchNodePaths,
}
})
}
})
sceneInfra.setCallbacks({
// After the third click this actor will transition.
onClick: tool.onClick,
})
tool.init()
// When the state is exited (by anything, even itself), this is run!
return cleanupFn
return tool.destroy
}),
},
// end actors
@ -2271,10 +2293,6 @@ export const modelingMachine = setup({
target: 'SketchIdle',
guard: 'is editing existing sketch',
},
{
target: 'circle3PointToolSelect',
guard: 'is editing 3-point circle',
},
'Line tool',
],
},
@ -2650,9 +2668,9 @@ export const modelingMachine = setup({
guard: 'next is center rectangle',
},
{
target: 'circle3PointToolSelect',
target: 'circleThreePointToolSelect',
reenter: true,
guard: 'next is circle 3 point',
guard: 'next is circle three point',
},
],
},
@ -2756,14 +2774,14 @@ export const modelingMachine = setup({
initial: 'splitting sketch pipe',
entry: ['assign tool in context', 'reset selections'],
},
circle3PointToolSelect: {
circleThreePointToolSelect: {
invoke: {
id: 'actor-circle-3-point',
id: 'actor-circle-three-point',
input: function ({ context }) {
if (!context.sketchDetails) return
return context.sketchDetails
},
src: 'actorCircle3Point',
src: 'actorCircleThreePoint',
},
on: {
// We still need this action to trigger (legacy code support)
@ -2771,6 +2789,7 @@ export const modelingMachine = setup({
// On stop event, transition to our usual SketchIdle state
'stop-internal': {
target: '#Modeling.Sketch.SketchIdle',
actions: 'update sketchDetails',
},
},
},
@ -3018,44 +3037,28 @@ export function isEditingExistingSketch({
maybePipeExpression.callee.name === 'circle')
)
return true
if (
maybePipeExpression.type === 'CallExpressionKw' &&
(maybePipeExpression.callee.name === 'startProfileAt' ||
maybePipeExpression.callee.name === 'circleThreePoint')
)
return true
if (maybePipeExpression.type !== 'PipeExpression') return false
const hasStartProfileAt = maybePipeExpression.body.some(
(item) =>
item.type === 'CallExpression' && item.callee.name === 'startProfileAt'
)
const hasCircle = maybePipeExpression.body.some(
(item) => item.type === 'CallExpression' && item.callee.name === 'circle'
)
const hasCircle =
maybePipeExpression.body.some(
(item) => item.type === 'CallExpression' && item.callee.name === 'circle'
) ||
maybePipeExpression.body.some(
(item) =>
item.type === 'CallExpressionKw' &&
item.callee.name === 'circleThreePoint'
)
return (hasStartProfileAt && maybePipeExpression.body.length > 1) || hasCircle
}
export function isEditing3PointCircle({
sketchDetails,
}: {
sketchDetails: SketchDetails | null
}): boolean {
if (!sketchDetails?.sketchEntryNodePath) return false
const variableDeclaration = getNodeFromPath<VariableDeclarator>(
kclManager.ast,
sketchDetails.sketchEntryNodePath,
'VariableDeclarator'
)
if (err(variableDeclaration)) return false
if (variableDeclaration.node.type !== 'VariableDeclarator') return false
const pipeExpression = variableDeclaration.node.init
if (pipeExpression.type !== 'PipeExpression') return false
const hasStartProfileAt = pipeExpression.body.some(
(item) =>
item.type === 'CallExpression' && item.callee.name === 'startProfileAt'
)
const hasCircle3Point = pipeExpression.body.some(
(item) =>
item.type === 'CallExpressionKw' &&
item.callee.name === 'circleThreePoint'
)
return (
(hasStartProfileAt && pipeExpression.body.length > 2) || hasCircle3Point
)
}
export function pipeHasCircle({
sketchDetails,
}: {

View File

@ -1402,6 +1402,19 @@ pub enum Path {
/// This is used to compute the tangential angle.
ccw: bool,
},
CircleThreePoint {
#[serde(flatten)]
base: BasePath,
/// Point 1 of the circle
#[ts(type = "[number, number]")]
p1: [f64; 2],
/// Point 2 of the circle
#[ts(type = "[number, number]")]
p2: [f64; 2],
/// Point 3 of the circle
#[ts(type = "[number, number]")]
p3: [f64; 2],
},
/// A path that is horizontal.
Horizontal {
#[serde(flatten)]
@ -1444,6 +1457,7 @@ enum PathType {
TangentialArc,
TangentialArcTo,
Circle,
CircleThreePoint,
Horizontal,
AngledLineTo,
Arc,
@ -1456,6 +1470,7 @@ impl From<&Path> for PathType {
Path::TangentialArcTo { .. } => Self::TangentialArcTo,
Path::TangentialArc { .. } => Self::TangentialArc,
Path::Circle { .. } => Self::Circle,
Path::CircleThreePoint { .. } => Self::CircleThreePoint,
Path::Horizontal { .. } => Self::Horizontal,
Path::AngledLineTo { .. } => Self::AngledLineTo,
Path::Base { .. } => Self::Base,
@ -1474,6 +1489,7 @@ impl Path {
Path::TangentialArcTo { base, .. } => base.geo_meta.id,
Path::TangentialArc { base, .. } => base.geo_meta.id,
Path::Circle { base, .. } => base.geo_meta.id,
Path::CircleThreePoint { base, .. } => base.geo_meta.id,
Path::Arc { base, .. } => base.geo_meta.id,
}
}
@ -1487,6 +1503,7 @@ impl Path {
Path::TangentialArcTo { base, .. } => base.tag.clone(),
Path::TangentialArc { base, .. } => base.tag.clone(),
Path::Circle { base, .. } => base.tag.clone(),
Path::CircleThreePoint { base, .. } => base.tag.clone(),
Path::Arc { base, .. } => base.tag.clone(),
}
}
@ -1500,6 +1517,7 @@ impl Path {
Path::TangentialArcTo { base, .. } => base,
Path::TangentialArc { base, .. } => base,
Path::Circle { base, .. } => base,
Path::CircleThreePoint { base, .. } => base,
Path::Arc { base, .. } => base,
}
}
@ -1537,6 +1555,10 @@ impl Path {
linear_distance(self.get_from(), self.get_to())
}
Self::Circle { radius, .. } => 2.0 * std::f64::consts::PI * radius,
Self::CircleThreePoint { p1, p2, p3, .. } => {
let circle_params = crate::std::utils::calculate_circle_from_3_points([p1.into(), p2.into(), p3.into()]);
2.0 * std::f64::consts::PI * circle_params.radius
},
Self::Arc { .. } => {
// TODO: Call engine utils to figure this out.
linear_distance(self.get_from(), self.get_to())
@ -1553,6 +1575,7 @@ impl Path {
Path::TangentialArcTo { base, .. } => Some(base),
Path::TangentialArc { base, .. } => Some(base),
Path::Circle { base, .. } => Some(base),
Path::CircleThreePoint { base, .. } => Some(base),
Path::Arc { base, .. } => Some(base),
}
}
@ -1572,6 +1595,16 @@ impl Path {
ccw: *ccw,
radius: *radius,
},
Path::CircleThreePoint {
p1, p2, p3, ..
} => {
let circle_params = crate::std::utils::calculate_circle_from_3_points([p1.into(), p2.into(), p3.into()]);
GetTangentialInfoFromPathsResult::Circle {
center: circle_params.center.into(),
ccw: true,
radius: circle_params.radius,
}
},
Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Base { .. } => {
let base = self.get_base();
GetTangentialInfoFromPathsResult::PreviousPoint(base.from)

View File

@ -231,7 +231,7 @@ pub(crate) async fn do_post_extrude(
Path::Arc { .. }
| Path::TangentialArc { .. }
| Path::TangentialArcTo { .. }
| Path::Circle { .. } => {
| Path::Circle { .. } | Path::CircleThreePoint { .. } => {
let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
face_id: *actual_face_id,
tag: path.get_base().tag.clone(),

View File

@ -182,6 +182,9 @@ pub async fn circle_three_point(exec_state: &mut ExecState, args: Args) -> Resul
tag = "Identifier for the circle to reference elsewhere.",
}
}]
/// Similar to inner_circle, but needs to retain 3-point information in the
/// path so it can be used for other features, otherwise it's lost.
async fn inner_circle_three_point(
p1: [f64; 2],
p2: [f64; 2],
@ -192,20 +195,72 @@ async fn inner_circle_three_point(
args: Args,
) -> Result<Sketch, KclError> {
let center = calculate_circle_center(p1, p2, p3);
inner_circle(
CircleData {
center,
// It can be the distance to any of the 3 points - they all lay on the circumference.
radius: distance(center.into(), p2.into()),
},
sketch_surface_or_group,
tag,
// It can be the distance to any of the 3 points - they all lay on the circumference.
let radius = distance(center.into(), p2.into());
let sketch_surface = match sketch_surface_or_group {
SketchOrSurface::SketchSurface(surface) => surface,
SketchOrSurface::Sketch(group) => group.on,
};
let sketch = crate::std::sketch::inner_start_profile_at(
[center[0] + radius, center[1]],
sketch_surface,
None,
exec_state,
args,
args.clone(),
)
.await
.await?;
let from = [center[0] + radius, center[1]];
let angle_start = Angle::zero();
let angle_end = Angle::turn();
let id = exec_state.next_uuid();
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::ExtendPath {
path: sketch.id.into(),
segment: PathSegment::Arc {
start: angle_start,
end: angle_end,
center: KPoint2d::from(center).map(LengthUnit),
radius: radius.into(),
relative: false,
},
}),
)
.await?;
let current_path = Path::CircleThreePoint {
base: BasePath {
from,
to: from,
tag: tag.clone(),
geo_meta: GeoMeta {
id,
metadata: args.source_range.into(),
},
},
p1,
p2,
p3,
};
let mut new_sketch = sketch.clone();
if let Some(tag) = &tag {
new_sketch.add_tag(tag, &current_path);
}
new_sketch.paths.push(current_path);
args.batch_modeling_cmd(id, ModelingCmd::from(mcmd::ClosePath { path_id: new_sketch.id }))
.await?;
Ok(new_sketch)
}
/// Type of the polygon
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Default)]
#[ts(export)]