Add 3-point circle tool (#4832)
* Add 3-point circle tool This adds a 1st pass for the 3-point circle tool. There is disabled code to drag around the 3 points and redraw the circle and a triangle created by those points. It will be enabled in a follow-up PR when we have circle3Point in the stdlib. For now, all it does is after the 3rd click, will insert circle center-radius KCL code for users to modify. * PR comments
This commit is contained in:
@ -3,6 +3,9 @@ import {
|
||||
DoubleSide,
|
||||
Group,
|
||||
Intersection,
|
||||
Line,
|
||||
LineDashedMaterial,
|
||||
BufferGeometry,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
Object3D,
|
||||
@ -13,6 +16,7 @@ import {
|
||||
Points,
|
||||
Quaternion,
|
||||
Scene,
|
||||
SphereGeometry,
|
||||
Vector2,
|
||||
Vector3,
|
||||
} from 'three'
|
||||
@ -31,6 +35,8 @@ 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 {
|
||||
@ -64,6 +70,7 @@ import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { executeAst, ToolTip } from 'lang/langHelpers'
|
||||
import {
|
||||
createProfileStartHandle,
|
||||
createArcGeometry,
|
||||
SegmentUtils,
|
||||
segmentUtils,
|
||||
} from './segments'
|
||||
@ -1219,6 +1226,228 @@ export class SceneEntities {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 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 = async (
|
||||
startSketchOnASTNodePath: PathToNode,
|
||||
forward: Vector3,
|
||||
up: Vector3,
|
||||
sketchOrigin: Vector3
|
||||
) => {
|
||||
// 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:
|
||||
// Do not reorient your surfaces to the intersection plane. Your points are
|
||||
// already in 3D space, not 2D. If you intersect say XZ, you want the points
|
||||
// to continue to live at the 3D intersection point, not be rotated to end
|
||||
// up elsewhere!
|
||||
// groupOfDrafts.setRotationFromQuaternion(orientation)
|
||||
this.scene.add(groupOfDrafts)
|
||||
|
||||
const DRAFT_POINT_RADIUS = 6
|
||||
|
||||
const createPoint = (center: Vector3): number => {
|
||||
const geometry = new SphereGeometry(DRAFT_POINT_RADIUS)
|
||||
const color = getThemeColorForThreeJs(sceneInfra._theme)
|
||||
const material = new MeshBasicMaterial({ color })
|
||||
|
||||
const mesh = new Mesh(geometry, material)
|
||||
mesh.userData = { type: CIRCLE_3_POINT_DRAFT_POINT }
|
||||
mesh.layers.set(SKETCH_LAYER)
|
||||
mesh.position.copy(center)
|
||||
mesh.scale.set(scale, scale, scale)
|
||||
mesh.renderOrder = 100
|
||||
|
||||
groupOfDrafts.add(mesh)
|
||||
|
||||
return mesh.id
|
||||
}
|
||||
|
||||
const circle3Point = (
|
||||
points: Vector2[]
|
||||
): undefined | { center: Vector3; radius: number } => {
|
||||
// A 3-point circle is undefined if it doesn't have 3 points :)
|
||||
if (points.length !== 3) return undefined
|
||||
|
||||
// y = (i/j)(x-h) + b
|
||||
// i and j variables for the slopes
|
||||
const i = [points[1].x - points[0].x, points[2].x - points[1].x]
|
||||
const j = [points[1].y - points[0].y, points[2].y - points[1].y]
|
||||
|
||||
// Our / threejs coordinate system affects this a lot. If you take this
|
||||
// code into a different code base, you may have to adjust a/b to being
|
||||
// -1/a/b, b/a, etc! In this case, a/-b did the trick.
|
||||
const m = [i[0] / -j[0], i[1] / -j[1]]
|
||||
|
||||
const h = [
|
||||
(points[0].x + points[1].x) / 2,
|
||||
(points[1].x + points[2].x) / 2,
|
||||
]
|
||||
const b = [
|
||||
(points[0].y + points[1].y) / 2,
|
||||
(points[1].y + points[2].y) / 2,
|
||||
]
|
||||
|
||||
// Algebraically derived
|
||||
const x = (-m[0] * h[0] + b[0] - b[1] + m[1] * h[1]) / (m[1] - m[0])
|
||||
const y = m[0] * (x - h[0]) + b[0]
|
||||
|
||||
const center = new Vector3(x, y, 0)
|
||||
const radius = Math.sqrt((points[1].x - x) ** 2 + (points[1].y - y) ** 2)
|
||||
|
||||
return {
|
||||
center,
|
||||
radius,
|
||||
}
|
||||
}
|
||||
|
||||
// TO BE SHORT LIVED: unused function to draw the circle and lines.
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line
|
||||
const createCircle3Point = (points: Vector2[]) => {
|
||||
const circleParams = circle3Point(points)
|
||||
|
||||
// A circle cannot be created for these points.
|
||||
if (!circleParams) return
|
||||
|
||||
const color = getThemeColorForThreeJs(sceneInfra._theme)
|
||||
const geometryCircle = createArcGeometry({
|
||||
center: [circleParams.center.x, circleParams.center.y],
|
||||
radius: circleParams.radius,
|
||||
startAngle: 0,
|
||||
endAngle: Math.PI * 2,
|
||||
ccw: true,
|
||||
isDashed: true,
|
||||
scale,
|
||||
})
|
||||
const materialCircle = new MeshBasicMaterial({ color })
|
||||
|
||||
if (groupCircle) groupOfDrafts.remove(groupCircle)
|
||||
groupCircle = new Group()
|
||||
groupCircle.renderOrder = 1
|
||||
|
||||
const meshCircle = new Mesh(geometryCircle, materialCircle)
|
||||
meshCircle.userData = { type: CIRCLE_3_POINT_DRAFT_CIRCLE }
|
||||
meshCircle.layers.set(SKETCH_LAYER)
|
||||
meshCircle.position.set(circleParams.center.x, circleParams.center.y, 0)
|
||||
meshCircle.scale.set(scale, scale, scale)
|
||||
groupCircle.add(meshCircle)
|
||||
|
||||
const geometryPolyLine = new BufferGeometry().setFromPoints([
|
||||
...points,
|
||||
points[0],
|
||||
])
|
||||
const materialPolyLine = new LineDashedMaterial({
|
||||
color,
|
||||
scale,
|
||||
dashSize: 6,
|
||||
gapSize: 6,
|
||||
})
|
||||
const meshPolyLine = new Line(geometryPolyLine, materialPolyLine)
|
||||
meshPolyLine.computeLineDistances()
|
||||
groupCircle.add(meshPolyLine)
|
||||
|
||||
groupOfDrafts.add(groupCircle)
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
this.scene.remove(groupOfDrafts)
|
||||
}
|
||||
|
||||
// The target of our dragging
|
||||
let target: Object3D | undefined = undefined
|
||||
|
||||
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(args.intersectionPoint.threeD)
|
||||
points.set(target.id, args.intersectionPoint.twoD)
|
||||
},
|
||||
async onDragEnd(_args) {
|
||||
target = undefined
|
||||
},
|
||||
async onClick(args) {
|
||||
if (points.size >= 3) return
|
||||
if (!args.intersectionPoint) return
|
||||
|
||||
const id = createPoint(args.intersectionPoint.threeD)
|
||||
points.set(id, args.intersectionPoint.twoD)
|
||||
|
||||
if (points.size < 2) return
|
||||
|
||||
// We've now got 3 points, let's create our circle!
|
||||
const astSnapshot = structuredClone(kclManager.ast)
|
||||
let nodeQueryResult
|
||||
nodeQueryResult = getNodeFromPath<VariableDeclaration>(
|
||||
astSnapshot,
|
||||
startSketchOnASTNodePath,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (err(nodeQueryResult)) return Promise.reject(nodeQueryResult)
|
||||
const startSketchOnASTNode = nodeQueryResult
|
||||
|
||||
const circleParams = circle3Point(Array.from(points.values()))
|
||||
|
||||
if (!circleParams) return
|
||||
|
||||
const kclCircle3Point = parse(`circle({
|
||||
center = [${circleParams.center.x}, ${circleParams.center.y}],
|
||||
radius = ${circleParams.radius},
|
||||
}, %)`)
|
||||
|
||||
if (err(kclCircle3Point) || kclCircle3Point.program === null) return
|
||||
if (kclCircle3Point.program.body[0].type !== 'ExpressionStatement')
|
||||
return
|
||||
|
||||
const clonedStartSketchOnASTNode = structuredClone(startSketchOnASTNode)
|
||||
startSketchOnASTNode.node.declaration.init = createPipeExpression([
|
||||
clonedStartSketchOnASTNode.node.declaration.init,
|
||||
kclCircle3Point.program.body[0].expression,
|
||||
])
|
||||
|
||||
await kclManager.executeAstMock(astSnapshot)
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(astSnapshot)
|
||||
|
||||
sceneInfra.modelingSend({ type: 'circle3PointsFinished', cleanup })
|
||||
},
|
||||
})
|
||||
}
|
||||
setupDraftCircle = async (
|
||||
sketchPathToNode: PathToNode,
|
||||
forward: [number, number, number],
|
||||
|
@ -62,6 +62,8 @@ export const ARROWHEAD = 'arrowhead'
|
||||
export const SEGMENT_LENGTH_LABEL = 'segment-length-label'
|
||||
export const SEGMENT_LENGTH_LABEL_TEXT = 'segment-length-label-text'
|
||||
export const SEGMENT_LENGTH_LABEL_OFFSET_PX = 30
|
||||
export const CIRCLE_3_POINT_DRAFT_POINT = 'circle-3-point-draft-point'
|
||||
export const CIRCLE_3_POINT_DRAFT_CIRCLE = 'circle-3-point-draft-circle'
|
||||
|
||||
export interface OnMouseEnterLeaveArgs {
|
||||
selected: Object3D<Object3DEventMap>
|
||||
|
@ -432,10 +432,19 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
},
|
||||
{
|
||||
id: 'circle-three-points',
|
||||
onClick: () =>
|
||||
console.error('Three-point circle not yet implemented'),
|
||||
onClick: ({ modelingState, modelingSend }) =>
|
||||
modelingSend({
|
||||
type: 'change tool',
|
||||
data: {
|
||||
tool: !modelingState.matches({
|
||||
Sketch: 'circle3PointToolSelect',
|
||||
})
|
||||
? 'circle3Points'
|
||||
: 'none',
|
||||
},
|
||||
}),
|
||||
icon: 'circle',
|
||||
status: 'unavailable',
|
||||
status: 'available',
|
||||
title: 'Three-point circle',
|
||||
showTitle: false,
|
||||
description: 'Draw a circle defined by three points',
|
||||
|
@ -215,6 +215,7 @@ export type SketchTool =
|
||||
| 'rectangle'
|
||||
| 'center rectangle'
|
||||
| 'circle'
|
||||
| 'circle3Points'
|
||||
| 'none'
|
||||
|
||||
export type ModelingMachineEvent =
|
||||
@ -238,7 +239,7 @@ export type ModelingMachineEvent =
|
||||
}
|
||||
| { type: 'Sketch no face' }
|
||||
| { type: 'Toggle gui mode' }
|
||||
| { type: 'Cancel' }
|
||||
| { type: 'Cancel'; cleanup?: () => void }
|
||||
| { type: 'CancelSketch' }
|
||||
| { type: 'Add start point' }
|
||||
| { type: 'Make segment horizontal' }
|
||||
@ -318,6 +319,7 @@ export type ModelingMachineEvent =
|
||||
| { type: 'Finish rectangle' }
|
||||
| { type: 'Finish center rectangle' }
|
||||
| { type: 'Finish circle' }
|
||||
| { type: 'circle3PointsFinished'; cleanup?: () => void }
|
||||
| { type: 'Artifact graph populated' }
|
||||
| { type: 'Artifact graph emptied' }
|
||||
|
||||
@ -566,6 +568,9 @@ export const modelingMachine = setup({
|
||||
canRectangleOrCircleTool({ sketchDetails }),
|
||||
'next is circle': ({ context: { sketchDetails, currentTool } }) =>
|
||||
currentTool === 'circle' && canRectangleOrCircleTool({ sketchDetails }),
|
||||
'next is circle 3 point': ({ context: { sketchDetails, currentTool } }) =>
|
||||
currentTool === 'circle3Points' &&
|
||||
canRectangleOrCircleTool({ sketchDetails }),
|
||||
'next is line': ({ context }) => context.currentTool === 'line',
|
||||
'next is none': ({ context }) => context.currentTool === 'none',
|
||||
},
|
||||
@ -974,6 +979,25 @@ export const modelingMachine = setup({
|
||||
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||||
})
|
||||
},
|
||||
entryDraftCircle3Point: ({ context: { sketchDetails }, event }) => {
|
||||
if (event.type !== 'change tool') return
|
||||
if (event.data?.tool !== 'circle3Points') return
|
||||
if (!sketchDetails) return
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
sceneEntitiesManager.entryDraftCircle3Point(
|
||||
sketchDetails.sketchPathToNode,
|
||||
new Vector3(...sketchDetails.zAxis),
|
||||
new Vector3(...sketchDetails.yAxis),
|
||||
new Vector3(...sketchDetails.origin)
|
||||
)
|
||||
},
|
||||
exitDraftCircle3Point: ({ event }) => {
|
||||
if (event.type !== 'circle3PointsFinished' && event.type !== 'Cancel')
|
||||
return
|
||||
if (!event.cleanup) return
|
||||
event.cleanup()
|
||||
},
|
||||
'set up draft line without teardown': ({ context: { sketchDetails } }) => {
|
||||
if (!sketchDetails) return
|
||||
|
||||
@ -2336,6 +2360,10 @@ export const modelingMachine = setup({
|
||||
target: 'Center Rectangle tool',
|
||||
guard: 'next is center rectangle',
|
||||
},
|
||||
{
|
||||
target: 'circle3PointToolSelect',
|
||||
guard: 'next is circle 3 point',
|
||||
},
|
||||
],
|
||||
|
||||
entry: ['assign tool in context', 'reset selections'],
|
||||
@ -2369,6 +2397,25 @@ export const modelingMachine = setup({
|
||||
initial: 'Awaiting origin',
|
||||
entry: 'listen for circle origin',
|
||||
},
|
||||
circle3PointToolSelect: {
|
||||
on: {
|
||||
'change tool': 'Change Tool',
|
||||
},
|
||||
|
||||
states: {
|
||||
circle3PointsAwaiting: {
|
||||
on: {
|
||||
circle3PointsFinished: {
|
||||
target: '#Modeling.Sketch.SketchIdle',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
initial: 'circle3PointsAwaiting',
|
||||
entry: 'entryDraftCircle3Point',
|
||||
exit: 'exitDraftCircle3Point',
|
||||
},
|
||||
},
|
||||
|
||||
initial: 'Init',
|
||||
|
Reference in New Issue
Block a user