most of the fix for 3 point circle
This commit is contained in:
@ -1,475 +0,0 @@
|
||||
import {
|
||||
Group,
|
||||
Mesh,
|
||||
Vector3,
|
||||
Vector2,
|
||||
Object3D,
|
||||
SphereGeometry,
|
||||
MeshBasicMaterial,
|
||||
Color,
|
||||
BufferGeometry,
|
||||
LineDashedMaterial,
|
||||
Line,
|
||||
Plane,
|
||||
} 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: Plane
|
||||
startSketchOnASTNodePath: PathToNode
|
||||
maybeExistingNodePath: PathToNode
|
||||
sketchNodePaths: PathToNode[]
|
||||
metadata?: any
|
||||
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.
|
||||
let selfASTNode = parse(`profileVarNameToBeReplaced = circleThreePoint(
|
||||
sketchVarNameToBeReplaced,
|
||||
p1 = [0.0, 0.0],
|
||||
p2 = [0.0, 0.0],
|
||||
p3 = [0.0, 0.0],
|
||||
)`)
|
||||
|
||||
// May be updated to false in the following code
|
||||
let isNewSketch = true
|
||||
const astSnapshot = structuredClone(kclManager.ast)
|
||||
|
||||
let sketchEntryNodePath = initArgs.maybeExistingNodePath
|
||||
|
||||
// AST node to work with. It's either an existing one, or a new one.
|
||||
if (sketchEntryNodePath.length === 0) {
|
||||
// Travel 1 node up from the sketch plane AST node, and append or
|
||||
// update the new profile AST node.
|
||||
|
||||
const OFFSET_TO_PARENT_SCOPE = -3
|
||||
|
||||
// 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'] <- in this case we're at the top program body
|
||||
// [x, 'index'] <- x is what we're incrementing here -v
|
||||
const nextIndex = initArgs.startSketchOnASTNodePath[
|
||||
// OFFSET_TO_PARENT_SCOPE puts us at the body of a function or
|
||||
// the overall program
|
||||
initArgs.startSketchOnASTNodePath.length + OFFSET_TO_PARENT_SCOPE
|
||||
][0] + 1
|
||||
|
||||
const bodyASTNode = getNodeFromPath<VariableDeclaration>(
|
||||
astSnapshot,
|
||||
structuredClone(initArgs.startSketchOnASTNodePath).splice(
|
||||
0,
|
||||
initArgs.startSketchOnASTNodePath.length + OFFSET_TO_PARENT_SCOPE
|
||||
),
|
||||
'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])
|
||||
|
||||
// Manipulate a copy of the original path to match our new AST.
|
||||
sketchEntryNodePath = structuredClone(initArgs.startSketchOnASTNodePath)
|
||||
sketchEntryNodePath[sketchEntryNodePath.length + OFFSET_TO_PARENT_SCOPE][0] = nextIndex
|
||||
} else {
|
||||
const tmpNode = getNodeFromPath<VariableDeclaration>(
|
||||
kclManager.ast,
|
||||
sketchEntryNodePath,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (err(tmpNode)) return new NoOpTool()
|
||||
|
||||
// Create references to the existing circleThreePoint AST node
|
||||
Object.assign(selfASTNode.program.body[0], tmpNode.node)
|
||||
|
||||
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)
|
||||
})
|
||||
Object.assign(groupOfDrafts.userData, initArgs.metadata)
|
||||
|
||||
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!
|
||||
|
||||
// For some reason, when going into edit mode, we don't need to orient...
|
||||
if (isNewSketch) {
|
||||
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,
|
||||
sketchEntryNodePath,
|
||||
'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)
|
||||
|
||||
if (isNewSketch) {
|
||||
// Add it to our sketch's profiles
|
||||
initArgs.sketchNodePaths.push(sketchEntryNodePath)
|
||||
}
|
||||
|
||||
// Pass the updated sketchDetails information back to the state machine.
|
||||
// In most (all?) cases the sketch plane never changes.
|
||||
// In many cases, sketchEntryNodePath may be a new PathToNode
|
||||
// (usually simply the next index in a scope)
|
||||
// updatedSketchNodePaths same deal, many cases it's the same or updated,
|
||||
// depends if a new circle is made.
|
||||
initArgs.done({
|
||||
updatedPlaneNodePath: initArgs.startSketchOnASTNodePath,
|
||||
updatedEntryNodePath: sketchEntryNodePath,
|
||||
updatedSketchNodePaths: initArgs.sketchNodePaths,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
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
|
||||
}
|
@ -85,6 +85,7 @@ import {
|
||||
createCallExpressionStdLib,
|
||||
createIdentifier,
|
||||
createLiteral,
|
||||
createNodeFromExprSnippet,
|
||||
createObjectExpression,
|
||||
createPipeExpression,
|
||||
createPipeSubstitution,
|
||||
@ -134,6 +135,13 @@ export const TANGENTIAL_ARC_TO__SEGMENT_DASH =
|
||||
'tangential-arc-to-segment-body-dashed'
|
||||
export const TANGENTIAL_ARC_TO_SEGMENT = 'tangential-arc-to-segment'
|
||||
export const TANGENTIAL_ARC_TO_SEGMENT_BODY = 'tangential-arc-to-segment-body'
|
||||
export const CIRCLE_THREE_POINT_SEGMENT = 'circle-three-point-segment'
|
||||
export const CIRCLE_THREE_POINT_SEGMENT_BODY = 'circle-segment-body'
|
||||
export const CIRCLE_THREE_POINT_SEGMENT_DASH =
|
||||
'circle-three-point-segment-body-dashed'
|
||||
export const CIRCLE_THREE_POINT_HANDLE1 = 'circle-three-point-handle1'
|
||||
export const CIRCLE_THREE_POINT_HANDLE2 = 'circle-three-point-handle2'
|
||||
export const CIRCLE_THREE_POINT_HANDLE3 = 'circle-three-point-handle3'
|
||||
export const CIRCLE_SEGMENT = 'circle-segment'
|
||||
export const CIRCLE_SEGMENT_BODY = 'circle-segment-body'
|
||||
export const CIRCLE_SEGMENT_DASH = 'circle-segment-body-dashed'
|
||||
@ -145,6 +153,7 @@ export const SEGMENT_BODIES = [
|
||||
STRAIGHT_SEGMENT,
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
CIRCLE_SEGMENT,
|
||||
CIRCLE_THREE_POINT_SEGMENT,
|
||||
]
|
||||
export const SEGMENT_BODIES_PLUS_PROFILE_START = [
|
||||
...SEGMENT_BODIES,
|
||||
@ -161,7 +170,6 @@ 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[] = []
|
||||
@ -221,6 +229,20 @@ export class SceneEntities {
|
||||
radius: segment.userData.radius,
|
||||
}
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
const callBack = update?.({
|
||||
prevSegment: segment.userData.prevSegment,
|
||||
@ -584,8 +606,6 @@ export class SceneEntities {
|
||||
programMemory,
|
||||
})
|
||||
|
||||
this.sketchTools = []
|
||||
|
||||
this.sceneProgramMemory = programMemory
|
||||
const group = new Group()
|
||||
position && group.position.set(...position)
|
||||
@ -606,7 +626,10 @@ export class SceneEntities {
|
||||
maybeModdedAst,
|
||||
sourceRangeFromRust(sketch.start.__geoMeta.sourceRange)
|
||||
)
|
||||
if (['Circle', 'CircleThreePoint'].includes(sketch?.paths?.[0]?.type) === false) {
|
||||
if (
|
||||
['Circle', 'CircleThreePoint'].includes(sketch?.paths?.[0]?.type) ===
|
||||
false
|
||||
) {
|
||||
const _profileStart = createProfileStartHandle({
|
||||
from: sketch.start.from,
|
||||
id: sketch.start.__geoMeta.id,
|
||||
@ -670,28 +693,13 @@ export class SceneEntities {
|
||||
if (err(_node1)) return
|
||||
const callExpName = _node1.node?.callee?.name
|
||||
|
||||
if (segment.type === 'CircleThreePoint') {
|
||||
const circleThreePoint = new CircleThreePoint({
|
||||
scene: group,
|
||||
intersectionPlane: this.intersectionPlane,
|
||||
startSketchOnASTNodePath: segPathToNode,
|
||||
maybeExistingNodePath: _node1.deepPath,
|
||||
sketchNodePaths: sketch.paths,
|
||||
metadata: segment,
|
||||
forward: new Vector3(...forward),
|
||||
up: new Vector3(...up),
|
||||
sketchOrigin: new Vector3(...position),
|
||||
})
|
||||
circleThreePoint.init()
|
||||
this.sketchTools.push(circleThreePoint)
|
||||
return
|
||||
}
|
||||
|
||||
const initSegment =
|
||||
segment.type === 'TangentialArcTo'
|
||||
? segmentUtils.tangentialArcTo.init
|
||||
: segment.type === 'Circle'
|
||||
? segmentUtils.circle.init
|
||||
: segment.type === 'CircleThreePoint'
|
||||
? segmentUtils.circleThreePoint.init
|
||||
: segmentUtils.straight.init
|
||||
const input: SegmentInputs =
|
||||
segment.type === 'Circle'
|
||||
@ -701,6 +709,13 @@ export class SceneEntities {
|
||||
center: segment.center,
|
||||
radius: segment.radius,
|
||||
}
|
||||
: segment.type === 'CircleThreePoint'
|
||||
? {
|
||||
type: 'circle-three-point-segment',
|
||||
p1: segment.p1,
|
||||
p2: segment.p2,
|
||||
p3: segment.p3,
|
||||
}
|
||||
: {
|
||||
type: 'straight-segment',
|
||||
from: segment.from,
|
||||
@ -1399,6 +1414,185 @@ export class SceneEntities {
|
||||
})
|
||||
return { updatedEntryNodePath, updatedSketchNodePaths }
|
||||
}
|
||||
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(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 = `[${point2[0] + 0.1}, ${
|
||||
point2[1] + 0.1
|
||||
}]`
|
||||
const newExpression = createNodeFromExprSnippet`${varName} = circleThreePoint({
|
||||
p1 = [${point1[0]}, ${point1[1]}],
|
||||
p2 = [${point2[0]}, ${point2[1]}],
|
||||
p3 = ${thirdPointCloseToWhereUserLastClicked}},
|
||||
${varDec.node.id.name}
|
||||
)`
|
||||
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
|
||||
await kclManager.executeAstMock(_ast)
|
||||
|
||||
const { programMemoryOverride, truncatedAst } = await this.setupSketch({
|
||||
sketchEntryNodePath: updatedEntryNodePath,
|
||||
sketchNodePaths: updatedSketchNodePaths,
|
||||
forward,
|
||||
up,
|
||||
position: sketchOrigin,
|
||||
maybeModdedAst: _ast,
|
||||
draftExpressionsIndices: { start: 0, end: 0 },
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
if (sketchInit.type === 'CallExpression') {
|
||||
const moddedResult = changeSketchArguments(
|
||||
modded,
|
||||
kclManager.programMemory,
|
||||
{
|
||||
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 executeAst({
|
||||
ast: modded,
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
// We make sure to send an empty program memory to denote we mean mock mode.
|
||||
programMemoryOverride,
|
||||
})
|
||||
const programMemory = execState.memory
|
||||
this.sceneProgramMemory = programMemory
|
||||
const sketch = sketchFromKclValue(programMemory.get(varName), varName)
|
||||
if (err(sketch)) return
|
||||
const sgPaths = sketch.paths
|
||||
const orthoFactor = orthoScale(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 = 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 === 'CallExpression') {
|
||||
const moddedResult = changeSketchArguments(
|
||||
modded,
|
||||
kclManager.programMemory,
|
||||
{
|
||||
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 kclManager.executeAstMock(_ast)
|
||||
sceneInfra.modelingSend({ type: 'Finish circle three point' })
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(_ast)
|
||||
}
|
||||
},
|
||||
})
|
||||
return { updatedEntryNodePath, updatedSketchNodePaths }
|
||||
}
|
||||
setupDraftCircle = async (
|
||||
sketchEntryNodePath: PathToNode,
|
||||
sketchNodePaths: PathToNode[],
|
||||
@ -1790,7 +1984,13 @@ export class SceneEntities {
|
||||
)
|
||||
|
||||
const group = getParentGroup(object, SEGMENT_BODIES_PLUS_PROFILE_START)
|
||||
const subGroup = getParentGroup(object, [ARROWHEAD, CIRCLE_CENTER_HANDLE])
|
||||
const subGroup = getParentGroup(object, [
|
||||
ARROWHEAD,
|
||||
CIRCLE_CENTER_HANDLE,
|
||||
CIRCLE_THREE_POINT_HANDLE1,
|
||||
CIRCLE_THREE_POINT_HANDLE2,
|
||||
CIRCLE_THREE_POINT_HANDLE3,
|
||||
])
|
||||
if (!group) return
|
||||
const pathToNode: PathToNode = structuredClone(group.userData.pathToNode)
|
||||
const varDecIndex = pathToNode[1][0]
|
||||
@ -1802,8 +2002,8 @@ export class SceneEntities {
|
||||
}
|
||||
|
||||
const from: [number, number] = [
|
||||
group.userData.from[0],
|
||||
group.userData.from[1],
|
||||
group.userData?.from?.[0],
|
||||
group.userData?.from?.[1],
|
||||
]
|
||||
const dragTo: [number, number] = [snappedPoint.x, snappedPoint.y]
|
||||
let modifiedAst = draftInfo ? draftInfo.truncatedAst : { ...kclManager.ast }
|
||||
@ -1858,6 +2058,29 @@ export class SceneEntities {
|
||||
center: dragTo,
|
||||
radius: group.userData.radius,
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// straight segment is the default
|
||||
return {
|
||||
@ -2011,6 +2234,18 @@ export class SceneEntities {
|
||||
center: segment.center,
|
||||
radius: segment.radius,
|
||||
}
|
||||
} 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,
|
||||
}
|
||||
}
|
||||
const callBack =
|
||||
update &&
|
||||
@ -2060,9 +2295,6 @@ export class SceneEntities {
|
||||
})
|
||||
|
||||
// Remove all sketch tools
|
||||
for (let tool of this.sketchTools) {
|
||||
await tool.destroy()
|
||||
}
|
||||
|
||||
if (this.axisGroup && removeAxis) this.scene.remove(this.axisGroup)
|
||||
const sketchSegments = this.scene.children.find(
|
||||
|
@ -31,6 +31,12 @@ import {
|
||||
CIRCLE_SEGMENT,
|
||||
CIRCLE_SEGMENT_BODY,
|
||||
CIRCLE_SEGMENT_DASH,
|
||||
CIRCLE_THREE_POINT_HANDLE1,
|
||||
CIRCLE_THREE_POINT_HANDLE2,
|
||||
CIRCLE_THREE_POINT_HANDLE3,
|
||||
CIRCLE_THREE_POINT_SEGMENT,
|
||||
CIRCLE_THREE_POINT_SEGMENT_BODY,
|
||||
CIRCLE_THREE_POINT_SEGMENT_DASH,
|
||||
EXTRA_SEGMENT_HANDLE,
|
||||
EXTRA_SEGMENT_OFFSET_PX,
|
||||
HIDE_HOVER_SEGMENT_LENGTH,
|
||||
@ -48,11 +54,13 @@ import {
|
||||
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
|
||||
import {
|
||||
ARROWHEAD,
|
||||
CIRCLE_3_POINT_DRAFT_CIRCLE,
|
||||
DRAFT_POINT,
|
||||
SceneInfra,
|
||||
SEGMENT_LENGTH_LABEL,
|
||||
SEGMENT_LENGTH_LABEL_OFFSET_PX,
|
||||
SEGMENT_LENGTH_LABEL_TEXT,
|
||||
SKETCH_LAYER,
|
||||
} from './sceneInfra'
|
||||
import { Themes, getThemeColorForThreeJs } from 'lib/theme'
|
||||
import { normaliseAngle, roundOff } from 'lib/utils'
|
||||
@ -61,6 +69,7 @@ import { SegmentInputs } from 'lang/std/stdTypes'
|
||||
import { err } from 'lib/trap'
|
||||
import { editorManager, sceneInfra } from 'lib/singletons'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { calculate_circle_from_3_points } from 'wasm-lib/pkg/wasm_lib'
|
||||
|
||||
interface CreateSegmentArgs {
|
||||
input: SegmentInputs
|
||||
@ -693,6 +702,194 @@ class CircleSegment implements SegmentUtils {
|
||||
}
|
||||
}
|
||||
|
||||
class CircleThreePointSegment implements SegmentUtils {
|
||||
init: SegmentUtils['init'] = ({
|
||||
input,
|
||||
id,
|
||||
pathToNode,
|
||||
isDraftSegment,
|
||||
scale = 1,
|
||||
theme,
|
||||
isSelected = false,
|
||||
sceneInfra,
|
||||
prevSegment,
|
||||
}) => {
|
||||
if (input.type !== 'circle-three-point-segment') {
|
||||
return new Error('Invalid segment type')
|
||||
}
|
||||
const { p1, p2, p3 } = input
|
||||
const { center_x, center_y, radius } = calculate_circle_from_3_points(
|
||||
p1[0],
|
||||
p1[1],
|
||||
p2[0],
|
||||
p2[1],
|
||||
p3[0],
|
||||
p3[1]
|
||||
)
|
||||
const center: [number, number] = [center_x, center_y]
|
||||
const baseColor = getThemeColorForThreeJs(theme)
|
||||
const color = isSelected ? 0x0000ff : baseColor
|
||||
|
||||
const group = new Group()
|
||||
const geometry = createArcGeometry({
|
||||
center,
|
||||
radius,
|
||||
startAngle: 0,
|
||||
endAngle: Math.PI * 2,
|
||||
ccw: true,
|
||||
isDashed: isDraftSegment,
|
||||
scale,
|
||||
})
|
||||
const mat = new MeshBasicMaterial({ color })
|
||||
const arcMesh = new Mesh(geometry, mat)
|
||||
const meshType = isDraftSegment
|
||||
? CIRCLE_THREE_POINT_SEGMENT_DASH
|
||||
: CIRCLE_THREE_POINT_SEGMENT_BODY
|
||||
const handle1 = createCircleThreePointHandle(
|
||||
scale,
|
||||
theme,
|
||||
CIRCLE_THREE_POINT_HANDLE1,
|
||||
color
|
||||
)
|
||||
const handle2 = createCircleThreePointHandle(
|
||||
scale,
|
||||
theme,
|
||||
CIRCLE_THREE_POINT_HANDLE2,
|
||||
color
|
||||
)
|
||||
const handle3 = createCircleThreePointHandle(
|
||||
scale,
|
||||
theme,
|
||||
CIRCLE_THREE_POINT_HANDLE3,
|
||||
color
|
||||
)
|
||||
|
||||
arcMesh.userData.type = meshType
|
||||
arcMesh.name = meshType
|
||||
group.userData = {
|
||||
type: CIRCLE_THREE_POINT_SEGMENT,
|
||||
draft: isDraftSegment,
|
||||
id,
|
||||
p1,
|
||||
p2,
|
||||
p3,
|
||||
ccw: true,
|
||||
prevSegment,
|
||||
pathToNode,
|
||||
isSelected,
|
||||
baseColor,
|
||||
}
|
||||
group.name = CIRCLE_THREE_POINT_SEGMENT
|
||||
|
||||
group.add(arcMesh, handle1, handle2, handle3)
|
||||
const updateOverlaysCallback = this.update({
|
||||
prevSegment,
|
||||
input,
|
||||
group,
|
||||
scale,
|
||||
sceneInfra,
|
||||
})
|
||||
if (err(updateOverlaysCallback)) return updateOverlaysCallback
|
||||
|
||||
return {
|
||||
group,
|
||||
updateOverlaysCallback,
|
||||
}
|
||||
}
|
||||
update: SegmentUtils['update'] = ({
|
||||
input,
|
||||
group,
|
||||
scale = 1,
|
||||
sceneInfra,
|
||||
}) => {
|
||||
if (input.type !== 'circle-three-point-segment') {
|
||||
return new Error('Invalid segment type')
|
||||
}
|
||||
const { p1, p2, p3 } = input
|
||||
group.userData.p1 = p1
|
||||
group.userData.p2 = p2
|
||||
group.userData.p3 = p3
|
||||
const { center_x, center_y, radius } = calculate_circle_from_3_points(
|
||||
p1[0],
|
||||
p1[1],
|
||||
p2[0],
|
||||
p2[1],
|
||||
p3[0],
|
||||
p3[1]
|
||||
)
|
||||
const center: [number, number] = [center_x, center_y]
|
||||
const points = [p1, p2, p3]
|
||||
const handles = [
|
||||
CIRCLE_THREE_POINT_HANDLE1,
|
||||
CIRCLE_THREE_POINT_HANDLE2,
|
||||
CIRCLE_THREE_POINT_HANDLE3,
|
||||
].map((handle) => group.getObjectByName(handle) as Group)
|
||||
handles.forEach((handle, i) => {
|
||||
const point = points[i]
|
||||
console.log('point', point, handle)
|
||||
if (handle && point) {
|
||||
handle.position.set(point[0], point[1], 0)
|
||||
handle.scale.set(scale, scale, scale)
|
||||
handle.visible = true
|
||||
}
|
||||
})
|
||||
|
||||
const pxLength = (2 * radius * Math.PI) / scale
|
||||
const shouldHideIdle = pxLength < HIDE_SEGMENT_LENGTH
|
||||
const shouldHideHover = pxLength < HIDE_HOVER_SEGMENT_LENGTH
|
||||
|
||||
const hoveredParent =
|
||||
sceneInfra.hoveredObject &&
|
||||
getParentGroup(sceneInfra.hoveredObject, [CIRCLE_SEGMENT])
|
||||
let isHandlesVisible = !shouldHideIdle
|
||||
if (hoveredParent && hoveredParent?.uuid === group?.uuid) {
|
||||
isHandlesVisible = !shouldHideHover
|
||||
}
|
||||
|
||||
const circleSegmentBody = group.children.find(
|
||||
(child) => child.userData.type === CIRCLE_THREE_POINT_SEGMENT_BODY
|
||||
) as Mesh
|
||||
|
||||
if (circleSegmentBody) {
|
||||
const newGeo = createArcGeometry({
|
||||
radius,
|
||||
center,
|
||||
startAngle: 0,
|
||||
endAngle: Math.PI * 2,
|
||||
ccw: true,
|
||||
scale,
|
||||
})
|
||||
circleSegmentBody.geometry = newGeo
|
||||
}
|
||||
const circleSegmentBodyDashed = group.getObjectByName(
|
||||
CIRCLE_THREE_POINT_SEGMENT_DASH
|
||||
)
|
||||
if (circleSegmentBodyDashed instanceof Mesh) {
|
||||
// consider throttling the whole updateTangentialArcToSegment
|
||||
// if there are more perf considerations going forward
|
||||
circleSegmentBodyDashed.geometry = createArcGeometry({
|
||||
center,
|
||||
radius,
|
||||
ccw: true,
|
||||
// make the start end where the handle is
|
||||
startAngle: Math.PI * 0.25,
|
||||
endAngle: Math.PI * 2.25,
|
||||
isDashed: true,
|
||||
scale,
|
||||
})
|
||||
}
|
||||
return () =>
|
||||
sceneInfra.updateOverlayDetails({
|
||||
arrowGroup: {} as any,
|
||||
group,
|
||||
isHandlesVisible,
|
||||
from: [0, 0],
|
||||
to: [center[0], center[1]],
|
||||
angle: Math.PI / 4,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function createProfileStartHandle({
|
||||
from,
|
||||
isDraft = false,
|
||||
@ -775,6 +972,32 @@ function createCircleCenterHandle(
|
||||
circleCenterGroup.scale.set(scale, scale, scale)
|
||||
return circleCenterGroup
|
||||
}
|
||||
function createCircleThreePointHandle(
|
||||
scale = 1,
|
||||
theme: Themes,
|
||||
name:
|
||||
| 'circle-three-point-handle1'
|
||||
| 'circle-three-point-handle2'
|
||||
| 'circle-three-point-handle3',
|
||||
color?: number
|
||||
): Group {
|
||||
const circleCenterGroup = new Group()
|
||||
|
||||
const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later
|
||||
const baseColor = getThemeColorForThreeJs(theme)
|
||||
const body = new MeshBasicMaterial({ color })
|
||||
const mesh = new Mesh(geometry, body)
|
||||
|
||||
circleCenterGroup.add(mesh)
|
||||
|
||||
circleCenterGroup.userData = {
|
||||
type: name,
|
||||
baseColor,
|
||||
}
|
||||
circleCenterGroup.name = name
|
||||
circleCenterGroup.scale.set(scale, scale, scale)
|
||||
return circleCenterGroup
|
||||
}
|
||||
|
||||
function createExtraSegmentHandle(
|
||||
scale: number,
|
||||
@ -1101,4 +1324,5 @@ export const segmentUtils = {
|
||||
straight: new StraightSegment(),
|
||||
tangentialArcTo: new TangentialArcToSegment(),
|
||||
circle: new CircleSegment(),
|
||||
circleThreePoint: new CircleThreePointSegment(),
|
||||
} as const
|
||||
|
@ -300,7 +300,9 @@ export const FileMachineProvider = ({
|
||||
async (data) => {
|
||||
if (data.method === 'overwrite') {
|
||||
codeManager.updateCodeStateEditor(data.code)
|
||||
await kclManager.executeCode(true)
|
||||
await kclManager.executeCode({
|
||||
zoomToFit: true,
|
||||
})
|
||||
await codeManager.writeToFile()
|
||||
} else if (data.method === 'newFile' && isDesktop()) {
|
||||
send({
|
||||
|
@ -1306,6 +1306,29 @@ export const ModelingMachineProvider = ({
|
||||
return result
|
||||
}
|
||||
),
|
||||
'set-up-draft-circle-three-point': fromPromise(
|
||||
async ({ input: { sketchDetails, data } }) => {
|
||||
if (!sketchDetails || !data)
|
||||
return reject('No sketch details or data')
|
||||
await sceneEntitiesManager.tearDownSketch({ removeAxis: false })
|
||||
|
||||
const result =
|
||||
await sceneEntitiesManager.setupDraftCircleThreePoint(
|
||||
sketchDetails.sketchEntryNodePath,
|
||||
sketchDetails.sketchNodePaths,
|
||||
sketchDetails.planeNodePath,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
sketchDetails.origin,
|
||||
data.p1,
|
||||
data.p2
|
||||
)
|
||||
if (err(result)) return reject(result)
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||||
|
||||
return result
|
||||
}
|
||||
),
|
||||
'set-up-draft-rectangle': fromPromise(
|
||||
async ({ input: { sketchDetails, data } }) => {
|
||||
if (!sketchDetails || !data)
|
||||
@ -1355,7 +1378,7 @@ export const ModelingMachineProvider = ({
|
||||
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
|
||||
}
|
||||
sceneInfra.resetMouseListeners()
|
||||
const { sketchTools } = await sceneEntitiesManager.setupSketch({
|
||||
await sceneEntitiesManager.setupSketch({
|
||||
sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [],
|
||||
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||||
forward: sketchDetails.zAxis,
|
||||
|
@ -496,7 +496,7 @@ export class KclManager {
|
||||
return
|
||||
}
|
||||
|
||||
zoomToFit = this.tryToZoomToFitOnCodeUpdate(ast, opts?.zoomToFit)
|
||||
// zoomToFit = this.tryToZoomToFitOnCodeUpdate(ast, opts?.zoomToFit)
|
||||
|
||||
this.ast = { ...ast }
|
||||
return this.executeAst(opts)
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
SourceRange,
|
||||
sketchFromKclValue,
|
||||
isPathToNodeNumber,
|
||||
parse,
|
||||
} from './wasm'
|
||||
import {
|
||||
isNodeSafeToReplacePath,
|
||||
@ -49,6 +50,7 @@ import { ExtrudeFacePlane } from 'machines/modelingMachine'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { KclExpressionWithVariable } from 'lib/commandTypes'
|
||||
import { Artifact, getPathsFromArtifact } from './std/artifactGraph'
|
||||
import { BodyItem } from 'wasm-lib/kcl/bindings/BodyItem'
|
||||
|
||||
export function startSketchOnDefault(
|
||||
node: Node<Program>,
|
||||
@ -316,7 +318,6 @@ export function extrudeSketch(
|
||||
const lastSketchNodePath =
|
||||
orderedSketchNodePaths[orderedSketchNodePaths.length - 1]
|
||||
|
||||
console.log('lastSketchNodePath', lastSketchNodePath, orderedSketchNodePaths)
|
||||
const sketchIndexInBody = Number(lastSketchNodePath[1][0])
|
||||
_node.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)
|
||||
|
||||
@ -1508,3 +1509,19 @@ export function splitPipedProfile(
|
||||
pathToPlane,
|
||||
}
|
||||
}
|
||||
|
||||
export function createNodeFromExprSnippet(
|
||||
strings: TemplateStringsArray,
|
||||
...expressions: any[]
|
||||
): Node<BodyItem> | Error {
|
||||
const code = strings.reduce(
|
||||
(acc, str, i) => acc + str + (expressions[i] || ''),
|
||||
''
|
||||
)
|
||||
let program = parse(code)
|
||||
if (err(program)) return program
|
||||
console.log('code', code, program)
|
||||
const node = program.program?.body[0]
|
||||
if (!node) return new Error('No node found')
|
||||
return node
|
||||
}
|
||||
|
@ -2091,7 +2091,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
ast: Node<Program>,
|
||||
artifactCommands: ArtifactCommand[],
|
||||
execStateArtifacts: ExecState['artifacts'],
|
||||
isPartialExecution?: true
|
||||
isPartialExecution?: boolean
|
||||
) {
|
||||
const newGraphArtifacts = createArtifactGraph({
|
||||
artifactCommands,
|
||||
|
@ -61,6 +61,9 @@ const STRAIGHT_SEGMENT_ERR = new Error(
|
||||
'Invalid input, expected "straight-segment"'
|
||||
)
|
||||
const ARC_SEGMENT_ERR = new Error('Invalid input, expected "arc-segment"')
|
||||
const CIRCLE_THREE_POINT_SEGMENT_ERR = new Error(
|
||||
'Invalid input, expected "circle-three-point-segment"'
|
||||
)
|
||||
|
||||
export type Coords2d = [number, number]
|
||||
|
||||
@ -1130,6 +1133,287 @@ export const circle: SketchLineHelper = {
|
||||
]
|
||||
},
|
||||
}
|
||||
export const circleThreePoint: SketchLineHelper = {
|
||||
add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => {
|
||||
if (segmentInput.type !== 'circle-three-point-segment')
|
||||
return CIRCLE_THREE_POINT_SEGMENT_ERR
|
||||
|
||||
const { p1, p2, p3 } = segmentInput
|
||||
const _node = { ...node }
|
||||
const nodeMeta = getNodeFromPath<PipeExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
|
||||
const { node: pipe } = nodeMeta
|
||||
|
||||
const createRoundedLiteral = (val: number) =>
|
||||
createLiteral(roundOff(val, 2))
|
||||
if (replaceExistingCallback) {
|
||||
const result = replaceExistingCallback([
|
||||
{
|
||||
type: 'arrayInObject',
|
||||
index: 0,
|
||||
key: 'p1',
|
||||
argType: 'xAbsolute',
|
||||
expr: createRoundedLiteral(p1[0]),
|
||||
},
|
||||
{
|
||||
type: 'arrayInObject',
|
||||
index: 1,
|
||||
key: 'p1',
|
||||
argType: 'yAbsolute',
|
||||
expr: createRoundedLiteral(p1[1]),
|
||||
},
|
||||
{
|
||||
type: 'arrayInObject',
|
||||
index: 0,
|
||||
key: 'p2',
|
||||
argType: 'xAbsolute',
|
||||
expr: createRoundedLiteral(p2[0]),
|
||||
},
|
||||
{
|
||||
type: 'arrayInObject',
|
||||
index: 1,
|
||||
key: 'p2',
|
||||
argType: 'yAbsolute',
|
||||
expr: createRoundedLiteral(p2[1]),
|
||||
},
|
||||
{
|
||||
type: 'arrayInObject',
|
||||
index: 0,
|
||||
key: 'p3',
|
||||
argType: 'xAbsolute',
|
||||
expr: createRoundedLiteral(p3[0]),
|
||||
},
|
||||
{
|
||||
type: 'arrayInObject',
|
||||
index: 1,
|
||||
key: 'p3',
|
||||
argType: 'yAbsolute',
|
||||
expr: createRoundedLiteral(p3[1]),
|
||||
},
|
||||
])
|
||||
if (err(result)) return result
|
||||
const { callExp, valueUsedInTransform } = result
|
||||
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
pipe.body[callIndex] = callExp
|
||||
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode,
|
||||
valueUsedInTransform,
|
||||
}
|
||||
}
|
||||
return new Error('not implemented')
|
||||
},
|
||||
updateArgs: ({ node, pathToNode, input }) => {
|
||||
if (input.type !== 'circle-three-point-segment')
|
||||
return CIRCLE_THREE_POINT_SEGMENT_ERR
|
||||
const { p1, p2, p3 } = input
|
||||
const _node = { ...node }
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
|
||||
const { node: callExpression, shallowPath } = nodeMeta
|
||||
const createRounded2DPointArr = (point: [number, number]) =>
|
||||
createArrayExpression([
|
||||
createLiteral(roundOff(point[0], 2)),
|
||||
createLiteral(roundOff(point[1], 2)),
|
||||
])
|
||||
|
||||
const firstArg = callExpression.arguments?.[0]
|
||||
const newP1 = createRounded2DPointArr(p1)
|
||||
const newP2 = createRounded2DPointArr(p2)
|
||||
const newP3 = createRounded2DPointArr(p3)
|
||||
mutateObjExpProp(firstArg, newP1, 'p1')
|
||||
mutateObjExpProp(firstArg, newP2, 'p2')
|
||||
mutateObjExpProp(firstArg, newP3, 'p3')
|
||||
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode: shallowPath,
|
||||
}
|
||||
},
|
||||
getTag: getTag(),
|
||||
addTag: addTag(),
|
||||
getConstraintInfo: (callExp: CallExpression, code, pathToNode) => {
|
||||
if (callExp.type !== 'CallExpression') return []
|
||||
const firstArg = callExp.arguments?.[0]
|
||||
if (firstArg.type !== 'ObjectExpression') return []
|
||||
const p1Details = getObjExprProperty(firstArg, 'p1')
|
||||
const p2Details = getObjExprProperty(firstArg, 'p2')
|
||||
const p3Details = getObjExprProperty(firstArg, 'p3')
|
||||
if (!p1Details || !p2Details || !p3Details) return []
|
||||
if (
|
||||
p1Details.expr.type !== 'ArrayExpression' ||
|
||||
p2Details.expr.type !== 'ArrayExpression' ||
|
||||
p3Details.expr.type !== 'ArrayExpression'
|
||||
)
|
||||
return []
|
||||
|
||||
const pathToP1ArrayExpression: PathToNode = [
|
||||
...pathToNode,
|
||||
['arguments', 'CallExpression'],
|
||||
[0, 'index'],
|
||||
['properties', 'ObjectExpression'],
|
||||
[p1Details.index, 'index'],
|
||||
['value', 'Property'],
|
||||
['elements', 'ArrayExpression'],
|
||||
]
|
||||
const pathToP2ArrayExpression: PathToNode = [
|
||||
...pathToNode,
|
||||
['arguments', 'CallExpression'],
|
||||
[0, 'index'],
|
||||
['properties', 'ObjectExpression'],
|
||||
[p2Details.index, 'index'],
|
||||
['value', 'Property'],
|
||||
['elements', 'ArrayExpression'],
|
||||
]
|
||||
const pathToP3ArrayExpression: PathToNode = [
|
||||
...pathToNode,
|
||||
['arguments', 'CallExpression'],
|
||||
[0, 'index'],
|
||||
['properties', 'ObjectExpression'],
|
||||
[p3Details.index, 'index'],
|
||||
['value', 'Property'],
|
||||
['elements', 'ArrayExpression'],
|
||||
]
|
||||
|
||||
const pathToP1XArg: PathToNode = [...pathToP1ArrayExpression, [0, 'index']]
|
||||
const pathToP1YArg: PathToNode = [...pathToP1ArrayExpression, [1, 'index']]
|
||||
const pathToP2XArg: PathToNode = [...pathToP2ArrayExpression, [0, 'index']]
|
||||
const pathToP2YArg: PathToNode = [...pathToP2ArrayExpression, [1, 'index']]
|
||||
const pathToP3XArg: PathToNode = [...pathToP3ArrayExpression, [0, 'index']]
|
||||
const pathToP3YArg: PathToNode = [...pathToP3ArrayExpression, [1, 'index']]
|
||||
|
||||
return [
|
||||
{
|
||||
stdLibFnName: 'circle',
|
||||
type: 'xAbsolute',
|
||||
isConstrained: isNotLiteralArrayOrStatic(p1Details.expr.elements[0]),
|
||||
sourceRange: [
|
||||
p1Details.expr.elements[0].start,
|
||||
p1Details.expr.elements[0].end,
|
||||
true,
|
||||
],
|
||||
pathToNode: pathToP1XArg,
|
||||
value: code.slice(
|
||||
p1Details.expr.elements[0].start,
|
||||
p1Details.expr.elements[0].end
|
||||
),
|
||||
argPosition: {
|
||||
type: 'arrayInObject',
|
||||
index: 0,
|
||||
key: 'p1',
|
||||
},
|
||||
},
|
||||
{
|
||||
stdLibFnName: 'circle',
|
||||
type: 'yAbsolute',
|
||||
isConstrained: isNotLiteralArrayOrStatic(p1Details.expr.elements[1]),
|
||||
sourceRange: [
|
||||
p1Details.expr.elements[1].start,
|
||||
p1Details.expr.elements[1].end,
|
||||
true,
|
||||
],
|
||||
pathToNode: pathToP1YArg,
|
||||
value: code.slice(
|
||||
p1Details.expr.elements[1].start,
|
||||
p1Details.expr.elements[1].end
|
||||
),
|
||||
argPosition: {
|
||||
type: 'arrayInObject',
|
||||
index: 1,
|
||||
key: 'p1',
|
||||
},
|
||||
},
|
||||
{
|
||||
stdLibFnName: 'circle',
|
||||
type: 'xAbsolute',
|
||||
isConstrained: isNotLiteralArrayOrStatic(p2Details.expr.elements[0]),
|
||||
sourceRange: [
|
||||
p2Details.expr.elements[0].start,
|
||||
p2Details.expr.elements[0].end,
|
||||
true,
|
||||
],
|
||||
pathToNode: pathToP2XArg,
|
||||
value: code.slice(
|
||||
p2Details.expr.elements[0].start,
|
||||
p2Details.expr.elements[0].end
|
||||
),
|
||||
argPosition: {
|
||||
type: 'arrayInObject',
|
||||
index: 0,
|
||||
key: 'p2',
|
||||
},
|
||||
},
|
||||
{
|
||||
stdLibFnName: 'circle',
|
||||
type: 'yAbsolute',
|
||||
isConstrained: isNotLiteralArrayOrStatic(p2Details.expr.elements[1]),
|
||||
sourceRange: [
|
||||
p2Details.expr.elements[1].start,
|
||||
p2Details.expr.elements[1].end,
|
||||
true,
|
||||
],
|
||||
pathToNode: pathToP2YArg,
|
||||
value: code.slice(
|
||||
p2Details.expr.elements[1].start,
|
||||
p2Details.expr.elements[1].end
|
||||
),
|
||||
argPosition: {
|
||||
type: 'arrayInObject',
|
||||
index: 1,
|
||||
key: 'p2',
|
||||
},
|
||||
},
|
||||
{
|
||||
stdLibFnName: 'circle',
|
||||
type: 'xAbsolute',
|
||||
isConstrained: isNotLiteralArrayOrStatic(p3Details.expr.elements[0]),
|
||||
sourceRange: [
|
||||
p3Details.expr.elements[0].start,
|
||||
p3Details.expr.elements[0].end,
|
||||
true,
|
||||
],
|
||||
pathToNode: pathToP3XArg,
|
||||
value: code.slice(
|
||||
p3Details.expr.elements[0].start,
|
||||
p3Details.expr.elements[0].end
|
||||
),
|
||||
argPosition: {
|
||||
type: 'arrayInObject',
|
||||
index: 0,
|
||||
key: 'p3',
|
||||
},
|
||||
},
|
||||
{
|
||||
stdLibFnName: 'circle',
|
||||
type: 'yAbsolute',
|
||||
isConstrained: isNotLiteralArrayOrStatic(p3Details.expr.elements[1]),
|
||||
sourceRange: [
|
||||
p3Details.expr.elements[1].start,
|
||||
p3Details.expr.elements[1].end,
|
||||
true,
|
||||
],
|
||||
pathToNode: pathToP3YArg,
|
||||
value: code.slice(
|
||||
p3Details.expr.elements[1].start,
|
||||
p3Details.expr.elements[1].end
|
||||
),
|
||||
argPosition: {
|
||||
type: 'arrayInObject',
|
||||
index: 1,
|
||||
key: 'p3',
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
export const angledLine: SketchLineHelper = {
|
||||
add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => {
|
||||
if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR
|
||||
@ -1898,6 +2182,7 @@ export const sketchLineHelperMap: { [key: string]: SketchLineHelper } = {
|
||||
angledLineThatIntersects,
|
||||
tangentialArcTo,
|
||||
circle,
|
||||
circleThreePoint,
|
||||
} as const
|
||||
|
||||
export function changeSketchArguments(
|
||||
|
@ -44,6 +44,13 @@ interface ArcSegmentInput {
|
||||
center: [number, number]
|
||||
radius: number
|
||||
}
|
||||
/** Inputs for three point circle */
|
||||
interface CircleThreePointSegmentInput {
|
||||
type: 'circle-three-point-segment'
|
||||
p1: [number, number]
|
||||
p2: [number, number]
|
||||
p3: [number, number]
|
||||
}
|
||||
|
||||
/**
|
||||
* SegmentInputs is a union type that can be either a StraightSegmentInput or an ArcSegmentInput.
|
||||
@ -51,7 +58,10 @@ interface ArcSegmentInput {
|
||||
* - StraightSegmentInput: Represents a straight segment with a starting point (from) and an ending point (to).
|
||||
* - ArcSegmentInput: Represents an arc segment with a starting point (from), a center point, and a radius.
|
||||
*/
|
||||
export type SegmentInputs = StraightSegmentInput | ArcSegmentInput
|
||||
export type SegmentInputs =
|
||||
| StraightSegmentInput
|
||||
| ArcSegmentInput
|
||||
| CircleThreePointSegmentInput
|
||||
|
||||
/**
|
||||
* Interface for adding or replacing a sketch stblib call expression to a sketch.
|
||||
@ -84,6 +94,9 @@ export type InputArgKeys =
|
||||
| 'intersectTag'
|
||||
| 'radius'
|
||||
| 'center'
|
||||
| 'p1'
|
||||
| 'p2'
|
||||
| 'p3'
|
||||
export interface SingleValueInput<T> {
|
||||
type: 'singleValue'
|
||||
argType: LineInputsType
|
||||
|
@ -449,7 +449,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
disabled: (state) => state.matches('Sketch no face'),
|
||||
isActive: (state) =>
|
||||
state.matches({ Sketch: 'Circle tool' }) ||
|
||||
state.matches({ Sketch: 'circle3PointToolSelect' }),
|
||||
state.matches({ Sketch: 'Circle three point tool' }),
|
||||
hotkey: (state) =>
|
||||
state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C',
|
||||
showTitle: false,
|
||||
@ -463,9 +463,9 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
type: 'change tool',
|
||||
data: {
|
||||
tool: !modelingState.matches({
|
||||
Sketch: 'circleThreePointToolSelect',
|
||||
Sketch: 'Circle three point tool',
|
||||
})
|
||||
? 'circleThreePoint'
|
||||
? 'circleThreePointNeo'
|
||||
: 'none',
|
||||
},
|
||||
}),
|
||||
|
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user