3-point circle interactive component (#4982)
* Add dragging behavior to 3 point circle Uses our talked about technique of calling Rust functions to calculate new geometry coordinates and parameters. It works very well! Need to have the modeling app show "edit sketch" still. * Cargo fmt * cargo fmt * Address Jon's comments * Fix clippy * Dont use isNaN * Make points easier to select (enlarge) * Fix circle button not being activated * Ensure efficiency of updating editor vs execution * Make cargo clippy happy
This commit is contained in:
@ -1,5 +1,6 @@
|
||||
import {
|
||||
BoxGeometry,
|
||||
Color,
|
||||
DoubleSide,
|
||||
Group,
|
||||
Intersection,
|
||||
@ -59,6 +60,7 @@ import {
|
||||
resultIsOk,
|
||||
SourceRange,
|
||||
} from 'lang/wasm'
|
||||
import { calculate_circle_from_3_points } from '../wasm-lib/pkg/wasm_lib'
|
||||
import {
|
||||
engineCommandManager,
|
||||
kclManager,
|
||||
@ -70,7 +72,7 @@ import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { executeAst, ToolTip } from 'lang/langHelpers'
|
||||
import {
|
||||
createProfileStartHandle,
|
||||
createArcGeometry,
|
||||
createCircleGeometry,
|
||||
SegmentUtils,
|
||||
segmentUtils,
|
||||
} from './segments'
|
||||
@ -109,6 +111,8 @@ import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
|
||||
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
|
||||
import { SegmentInputs } from 'lang/std/stdTypes'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { LabeledArg } from 'wasm-lib/kcl/bindings/LabeledArg'
|
||||
import { Literal } from 'wasm-lib/kcl/bindings/Literal'
|
||||
import { radToDeg } from 'three/src/math/MathUtils'
|
||||
import { getArtifactFromRange, codeRefFromRange } from 'lang/std/artifactGraph'
|
||||
|
||||
@ -1261,110 +1265,98 @@ export class SceneEntities {
|
||||
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)
|
||||
// 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)
|
||||
|
||||
const DRAFT_POINT_RADIUS = 6
|
||||
// How large the points on the circle will render as
|
||||
const DRAFT_POINT_RADIUS = 10 // px
|
||||
|
||||
const createPoint = (center: Vector3): number => {
|
||||
// The target of our dragging
|
||||
let target: Object3D | undefined = undefined
|
||||
|
||||
// The KCL this will generate.
|
||||
const kclCircle3Point = parse(`circleThreePoint(
|
||||
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 })
|
||||
|
||||
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: CIRCLE_3_POINT_DRAFT_POINT }
|
||||
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
|
||||
|
||||
groupOfDrafts.add(mesh)
|
||||
|
||||
return mesh.id
|
||||
return mesh
|
||||
}
|
||||
|
||||
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 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 geometryCircle = createArcGeometry({
|
||||
center: [circleParams.center.x, circleParams.center.y],
|
||||
radius: circleParams.radius,
|
||||
startAngle: 0,
|
||||
endAngle: Math.PI * 2,
|
||||
ccw: true,
|
||||
isDashed: true,
|
||||
scale,
|
||||
const lineCircle = createCircleGeometry({
|
||||
center: [center.x, center.y],
|
||||
radius,
|
||||
color,
|
||||
isDashed: false,
|
||||
scale: 1,
|
||||
})
|
||||
const materialCircle = new MeshBasicMaterial({ color })
|
||||
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 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 pointMesh = createPoint(new Vector3(center.x, center.y, 0), {
|
||||
noInteraction: true,
|
||||
})
|
||||
groupCircle.add(pointMesh)
|
||||
|
||||
const geometryPolyLine = new BufferGeometry().setFromPoints([
|
||||
...points,
|
||||
points[0],
|
||||
...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,
|
||||
scale: 1 / scale,
|
||||
dashSize: 6,
|
||||
gapSize: 6,
|
||||
})
|
||||
@ -1375,13 +1367,146 @@ export class SceneEntities {
|
||||
groupOfDrafts.add(groupCircle)
|
||||
}
|
||||
|
||||
// The target of our dragging
|
||||
let target: Object3D | undefined = undefined
|
||||
const insertCircle3PointKclIntoAstSnapshot = (
|
||||
points: Vector2[]
|
||||
): Program => {
|
||||
if (err(kclCircle3Point) || kclCircle3Point.program === null)
|
||||
return kclManager.ast
|
||||
if (kclCircle3Point.program.body[0].type !== 'ExpressionStatement')
|
||||
return kclManager.ast
|
||||
if (
|
||||
kclCircle3Point.program.body[0].expression.type !== 'CallExpressionKw'
|
||||
)
|
||||
return kclManager.ast
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const kclCircle3PointArgs =
|
||||
kclCircle3Point.program.body[0].expression.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 already dealing with a PipeExpression.
|
||||
// Modify the current one.
|
||||
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 === 'PipeExpression' &&
|
||||
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(
|
||||
@ -1397,8 +1522,18 @@ export class SceneEntities {
|
||||
// The user was off their mark! Missed the object to select.
|
||||
if (!target) return
|
||||
|
||||
target.position.copy(args.intersectionPoint.threeD)
|
||||
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
|
||||
@ -1407,45 +1542,19 @@ export class SceneEntities {
|
||||
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'
|
||||
const pointMesh = createPoint(
|
||||
new Vector3(
|
||||
args.intersectionPoint.twoD.x,
|
||||
args.intersectionPoint.twoD.y,
|
||||
0
|
||||
)
|
||||
if (err(nodeQueryResult)) return Promise.reject(nodeQueryResult)
|
||||
const startSketchOnASTNode = nodeQueryResult
|
||||
)
|
||||
groupOfDrafts.add(pointMesh)
|
||||
points.set(pointMesh.id, args.intersectionPoint.twoD)
|
||||
|
||||
const circleParams = circle3Point(Array.from(points.values()))
|
||||
if (points.size <= 2) return
|
||||
|
||||
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)
|
||||
|
||||
done()
|
||||
await updateCircle3Point()
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -9,6 +9,9 @@ import {
|
||||
ExtrudeGeometry,
|
||||
Group,
|
||||
LineCurve3,
|
||||
LineBasicMaterial,
|
||||
LineDashedMaterial,
|
||||
Line,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
NormalBufferAttributes,
|
||||
@ -1003,6 +1006,49 @@ export function createArcGeometry({
|
||||
return geo
|
||||
}
|
||||
|
||||
// (lee) The above is much more complex than necessary.
|
||||
// I've derived the new code from:
|
||||
// https://threejs.org/docs/#api/en/extras/curves/EllipseCurve
|
||||
// I'm not sure why it wasn't done like this in the first place?
|
||||
// I don't touch the code above because it may break something else.
|
||||
export function createCircleGeometry({
|
||||
center,
|
||||
radius,
|
||||
color,
|
||||
isDashed = false,
|
||||
scale = 1,
|
||||
}: {
|
||||
center: Coords2d
|
||||
radius: number
|
||||
color: number
|
||||
isDashed?: boolean
|
||||
scale?: number
|
||||
}): Line {
|
||||
const circle = new EllipseCurve(
|
||||
center[0],
|
||||
center[1],
|
||||
radius,
|
||||
radius,
|
||||
0,
|
||||
Math.PI * 2,
|
||||
true,
|
||||
scale
|
||||
)
|
||||
const points = circle.getPoints(75) // just enough points to not see edges.
|
||||
const geometry = new BufferGeometry().setFromPoints(points)
|
||||
const material = !isDashed
|
||||
? new LineBasicMaterial({ color })
|
||||
: new LineDashedMaterial({
|
||||
color,
|
||||
scale,
|
||||
dashSize: 6,
|
||||
gapSize: 6,
|
||||
})
|
||||
const line = new Line(geometry, material)
|
||||
line.computeLineDistances()
|
||||
return line
|
||||
}
|
||||
|
||||
export function dashedStraight(
|
||||
from: Coords2d,
|
||||
to: Coords2d,
|
||||
|
@ -460,18 +460,16 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
disabled: (state) =>
|
||||
state.matches('Sketch no face') ||
|
||||
(!canRectangleOrCircleTool(state.context) &&
|
||||
!state.matches({ Sketch: 'Circle tool' })),
|
||||
isActive: (state) => state.matches({ Sketch: 'Circle tool' }),
|
||||
!state.matches({ Sketch: 'Circle tool' }) &&
|
||||
!state.matches({ Sketch: 'circle3PointToolSelect' })),
|
||||
isActive: (state) =>
|
||||
state.matches({ Sketch: 'Circle tool' }) ||
|
||||
state.matches({ Sketch: 'circle3PointToolSelect' }),
|
||||
hotkey: (state) =>
|
||||
state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C',
|
||||
showTitle: false,
|
||||
description: 'Start drawing a circle from its center',
|
||||
links: [
|
||||
{
|
||||
label: 'GitHub issue',
|
||||
url: 'https://github.com/KittyCAD/modeling-app/issues/1501',
|
||||
},
|
||||
],
|
||||
links: [],
|
||||
},
|
||||
{
|
||||
id: 'circle-three-points',
|
||||
@ -488,7 +486,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
}),
|
||||
icon: 'circle',
|
||||
status: 'available',
|
||||
title: 'Three-point circle',
|
||||
title: '3-point circle',
|
||||
showTitle: false,
|
||||
description: 'Draw a circle defined by three points',
|
||||
links: [],
|
||||
|
@ -422,6 +422,8 @@ export const modelingMachine = setup({
|
||||
},
|
||||
'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
|
||||
@ -2187,6 +2189,10 @@ export const modelingMachine = setup({
|
||||
target: 'SketchIdle',
|
||||
guard: 'is editing existing sketch',
|
||||
},
|
||||
{
|
||||
target: 'circle3PointToolSelect',
|
||||
guard: 'is editing 3-point circle',
|
||||
},
|
||||
'Line tool',
|
||||
],
|
||||
},
|
||||
@ -2518,13 +2524,8 @@ export const modelingMachine = setup({
|
||||
circle3PointToolSelect: {
|
||||
invoke: {
|
||||
id: 'actor-circle-3-point',
|
||||
input: function ({ context, event }) {
|
||||
// These are not really necessary but I believe they are needed
|
||||
// to satisfy TypeScript type narrowing or undefined check.
|
||||
if (event.type !== 'change tool') return
|
||||
if (event.data?.tool !== 'circle3Points') return
|
||||
input: function ({ context }) {
|
||||
if (!context.sketchDetails) return
|
||||
|
||||
return context.sketchDetails
|
||||
},
|
||||
src: 'actorCircle3Point',
|
||||
@ -2782,6 +2783,34 @@ export function isEditingExistingSketch({
|
||||
)
|
||||
return (hasStartProfileAt && pipeExpression.body.length > 2) || hasCircle
|
||||
}
|
||||
export function isEditing3PointCircle({
|
||||
sketchDetails,
|
||||
}: {
|
||||
sketchDetails: SketchDetails | null
|
||||
}): boolean {
|
||||
if (!sketchDetails?.sketchPathToNode) return false
|
||||
const variableDeclaration = getNodeFromPath<VariableDeclarator>(
|
||||
kclManager.ast,
|
||||
sketchDetails.sketchPathToNode,
|
||||
'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,
|
||||
}: {
|
||||
@ -2802,6 +2831,27 @@ export function pipeHasCircle({
|
||||
)
|
||||
return hasCircle
|
||||
}
|
||||
export function pipeHasCircleThreePoint({
|
||||
sketchDetails,
|
||||
}: {
|
||||
sketchDetails: SketchDetails | null
|
||||
}): boolean {
|
||||
if (!sketchDetails?.sketchPathToNode) return false
|
||||
const variableDeclaration = getNodeFromPath<VariableDeclarator>(
|
||||
kclManager.ast,
|
||||
sketchDetails.sketchPathToNode,
|
||||
'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 hasCircle = pipeExpression.body.some(
|
||||
(item) =>
|
||||
item.type === 'CallExpression' && item.callee.name === 'circleThreePoint'
|
||||
)
|
||||
return hasCircle
|
||||
}
|
||||
|
||||
export function canRectangleOrCircleTool({
|
||||
sketchDetails,
|
||||
|
@ -70,7 +70,7 @@ mod settings;
|
||||
#[cfg(test)]
|
||||
mod simulation_tests;
|
||||
mod source_range;
|
||||
mod std;
|
||||
pub mod std;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod test_server;
|
||||
mod thread;
|
||||
@ -84,7 +84,7 @@ pub use engine::{EngineManager, ExecutionKind};
|
||||
pub use errors::{CompilationError, ConnectionError, ExecError, KclError, KclErrorWithOutputs};
|
||||
pub use execution::{
|
||||
cache::{CacheInformation, OldAstState},
|
||||
ExecState, ExecutorContext, ExecutorSettings,
|
||||
ExecState, ExecutorContext, ExecutorSettings, Point2d,
|
||||
};
|
||||
pub use lsp::{
|
||||
copilot::Backend as CopilotLspBackend,
|
||||
|
@ -55,6 +55,10 @@ impl KwArgs {
|
||||
pub fn len(&self) -> usize {
|
||||
self.labeled.len() + if self.unlabeled.is_some() { 1 } else { 0 }
|
||||
}
|
||||
/// Are there no arguments?
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.labeled.len() == 0 && self.unlabeled.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -270,6 +270,19 @@ pub fn calculate_circle_center(p1: [f64; 2], p2: [f64; 2], p3: [f64; 2]) -> [f64
|
||||
[x, y]
|
||||
}
|
||||
|
||||
pub struct CircleParams {
|
||||
pub center: Point2d,
|
||||
pub radius: f64,
|
||||
}
|
||||
|
||||
pub fn calculate_circle_from_3_points(points: [Point2d; 3]) -> CircleParams {
|
||||
let center: Point2d = calculate_circle_center(points[0].into(), points[1].into(), points[2].into()).into();
|
||||
CircleParams {
|
||||
center,
|
||||
radius: distance(center, points[1]),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// Here you can bring your functions into scope
|
||||
|
@ -5,7 +5,7 @@ use std::sync::Arc;
|
||||
use futures::stream::TryStreamExt;
|
||||
use gloo_utils::format::JsValueSerdeExt;
|
||||
use kcl_lib::{
|
||||
exec::IdGenerator, CacheInformation, CoreDump, EngineManager, ExecState, ModuleId, OldAstState, Program,
|
||||
exec::IdGenerator, CacheInformation, CoreDump, EngineManager, ExecState, ModuleId, OldAstState, Point2d, Program,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
use tower_lsp::{LspService, Server};
|
||||
@ -576,3 +576,26 @@ pub fn base64_decode(input: &str) -> Result<Vec<u8>, JsValue> {
|
||||
|
||||
Err(JsValue::from_str("Invalid base64 encoding"))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmCircleParams {
|
||||
pub center_x: f64,
|
||||
pub center_y: f64,
|
||||
pub radius: f64,
|
||||
}
|
||||
|
||||
/// Calculate a circle from 3 points.
|
||||
#[wasm_bindgen]
|
||||
pub fn calculate_circle_from_3_points(ax: f64, ay: f64, bx: f64, by: f64, cx: f64, cy: f64) -> WasmCircleParams {
|
||||
let result = kcl_lib::std::utils::calculate_circle_from_3_points([
|
||||
Point2d { x: ax, y: ay },
|
||||
Point2d { x: bx, y: by },
|
||||
Point2d { x: cx, y: cy },
|
||||
]);
|
||||
|
||||
WasmCircleParams {
|
||||
center_x: result.center.x,
|
||||
center_y: result.center.y,
|
||||
radius: result.radius,
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user