WIP
This commit is contained in:
436
src/clientSideScene/circleThreePoint.ts
Normal file
436
src/clientSideScene/circleThreePoint.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
30
src/clientSideScene/interfaceSketchTool.ts
Normal file
30
src/clientSideScene/interfaceSketchTool.ts
Normal 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
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
),
|
||||
|
@ -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',
|
||||
},
|
||||
}),
|
||||
|
@ -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,
|
||||
}: {
|
||||
|
@ -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)
|
||||
|
@ -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(),
|
||||
|
@ -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, ¤t_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)]
|
||||
|
Reference in New Issue
Block a user