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 {
|
import {
|
||||||
BoxGeometry,
|
BoxGeometry,
|
||||||
|
Color,
|
||||||
DoubleSide,
|
DoubleSide,
|
||||||
Group,
|
Group,
|
||||||
Intersection,
|
Intersection,
|
||||||
@ -59,6 +60,7 @@ import {
|
|||||||
resultIsOk,
|
resultIsOk,
|
||||||
SourceRange,
|
SourceRange,
|
||||||
} from 'lang/wasm'
|
} from 'lang/wasm'
|
||||||
|
import { calculate_circle_from_3_points } from '../wasm-lib/pkg/wasm_lib'
|
||||||
import {
|
import {
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
kclManager,
|
kclManager,
|
||||||
@ -70,7 +72,7 @@ import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
|||||||
import { executeAst, ToolTip } from 'lang/langHelpers'
|
import { executeAst, ToolTip } from 'lang/langHelpers'
|
||||||
import {
|
import {
|
||||||
createProfileStartHandle,
|
createProfileStartHandle,
|
||||||
createArcGeometry,
|
createCircleGeometry,
|
||||||
SegmentUtils,
|
SegmentUtils,
|
||||||
segmentUtils,
|
segmentUtils,
|
||||||
} from './segments'
|
} from './segments'
|
||||||
@ -109,6 +111,8 @@ import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
|
|||||||
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
|
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
|
||||||
import { SegmentInputs } from 'lang/std/stdTypes'
|
import { SegmentInputs } from 'lang/std/stdTypes'
|
||||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
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 { radToDeg } from 'three/src/math/MathUtils'
|
||||||
import { getArtifactFromRange, codeRefFromRange } from 'lang/std/artifactGraph'
|
import { getArtifactFromRange, codeRefFromRange } from 'lang/std/artifactGraph'
|
||||||
|
|
||||||
@ -1261,110 +1265,98 @@ export class SceneEntities {
|
|||||||
const groupOfDrafts = new Group()
|
const groupOfDrafts = new Group()
|
||||||
groupOfDrafts.name = 'circle-3-point-group'
|
groupOfDrafts.name = 'circle-3-point-group'
|
||||||
groupOfDrafts.position.copy(sketchOrigin)
|
groupOfDrafts.position.copy(sketchOrigin)
|
||||||
|
|
||||||
// lee: I'm keeping this here as a developer gotchya:
|
// lee: I'm keeping this here as a developer gotchya:
|
||||||
// Do not reorient your surfaces to the intersection plane. Your points are
|
// If you use 3D points, do not rotate anything.
|
||||||
// already in 3D space, not 2D. If you intersect say XZ, you want the points
|
// If you use 2D points (easier to deal with, generally do this!), then
|
||||||
// to continue to live at the 3D intersection point, not be rotated to end
|
// rotate the group just like this! Remember to rotate other groups too!
|
||||||
// up elsewhere!
|
groupOfDrafts.setRotationFromQuaternion(orientation)
|
||||||
// groupOfDrafts.setRotationFromQuaternion(orientation)
|
|
||||||
this.scene.add(groupOfDrafts)
|
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 geometry = new SphereGeometry(DRAFT_POINT_RADIUS)
|
||||||
const color = getThemeColorForThreeJs(sceneInfra._theme)
|
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)
|
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.layers.set(SKETCH_LAYER)
|
||||||
mesh.position.copy(center)
|
mesh.position.copy(center)
|
||||||
mesh.scale.set(scale, scale, scale)
|
mesh.scale.set(scale, scale, scale)
|
||||||
mesh.renderOrder = 100
|
mesh.renderOrder = 100
|
||||||
|
|
||||||
groupOfDrafts.add(mesh)
|
return mesh
|
||||||
|
|
||||||
return mesh.id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const circle3Point = (
|
const createCircle3PointGraphic = async (
|
||||||
points: Vector2[]
|
points: Vector2[],
|
||||||
): undefined | { center: Vector3; radius: number } => {
|
center: Vector2,
|
||||||
// A 3-point circle is undefined if it doesn't have 3 points :)
|
radius: number
|
||||||
if (points.length !== 3) return undefined
|
) => {
|
||||||
|
if (
|
||||||
// y = (i/j)(x-h) + b
|
Number.isNaN(radius) ||
|
||||||
// i and j variables for the slopes
|
Number.isNaN(center.x) ||
|
||||||
const i = [points[1].x - points[0].x, points[2].x - points[1].x]
|
Number.isNaN(center.y)
|
||||||
const j = [points[1].y - points[0].y, points[2].y - points[1].y]
|
)
|
||||||
|
return
|
||||||
// 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 color = getThemeColorForThreeJs(sceneInfra._theme)
|
||||||
const geometryCircle = createArcGeometry({
|
const lineCircle = createCircleGeometry({
|
||||||
center: [circleParams.center.x, circleParams.center.y],
|
center: [center.x, center.y],
|
||||||
radius: circleParams.radius,
|
radius,
|
||||||
startAngle: 0,
|
color,
|
||||||
endAngle: Math.PI * 2,
|
isDashed: false,
|
||||||
ccw: true,
|
scale: 1,
|
||||||
isDashed: true,
|
|
||||||
scale,
|
|
||||||
})
|
})
|
||||||
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)
|
if (groupCircle) groupOfDrafts.remove(groupCircle)
|
||||||
groupCircle = new Group()
|
groupCircle = new Group()
|
||||||
groupCircle.renderOrder = 1
|
groupCircle.renderOrder = 1
|
||||||
|
groupCircle.add(lineCircle)
|
||||||
|
|
||||||
const meshCircle = new Mesh(geometryCircle, materialCircle)
|
const pointMesh = createPoint(new Vector3(center.x, center.y, 0), {
|
||||||
meshCircle.userData = { type: CIRCLE_3_POINT_DRAFT_CIRCLE }
|
noInteraction: true,
|
||||||
meshCircle.layers.set(SKETCH_LAYER)
|
})
|
||||||
meshCircle.position.set(circleParams.center.x, circleParams.center.y, 0)
|
groupCircle.add(pointMesh)
|
||||||
meshCircle.scale.set(scale, scale, scale)
|
|
||||||
groupCircle.add(meshCircle)
|
|
||||||
|
|
||||||
const geometryPolyLine = new BufferGeometry().setFromPoints([
|
const geometryPolyLine = new BufferGeometry().setFromPoints([
|
||||||
...points,
|
...points.map((p) => new Vector3(p.x, p.y, 0)),
|
||||||
points[0],
|
new Vector3(points[0].x, points[0].y, 0),
|
||||||
])
|
])
|
||||||
const materialPolyLine = new LineDashedMaterial({
|
const materialPolyLine = new LineDashedMaterial({
|
||||||
color,
|
color,
|
||||||
scale,
|
scale: 1 / scale,
|
||||||
dashSize: 6,
|
dashSize: 6,
|
||||||
gapSize: 6,
|
gapSize: 6,
|
||||||
})
|
})
|
||||||
@ -1375,13 +1367,146 @@ export class SceneEntities {
|
|||||||
groupOfDrafts.add(groupCircle)
|
groupOfDrafts.add(groupCircle)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The target of our dragging
|
const insertCircle3PointKclIntoAstSnapshot = (
|
||||||
let target: Object3D | undefined = undefined
|
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 = () => {
|
const cleanupFn = () => {
|
||||||
this.scene.remove(groupOfDrafts)
|
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({
|
sceneInfra.setCallbacks({
|
||||||
async onDrag(args) {
|
async onDrag(args) {
|
||||||
const draftPointsIntersected = args.intersects.filter(
|
const draftPointsIntersected = args.intersects.filter(
|
||||||
@ -1397,8 +1522,18 @@ export class SceneEntities {
|
|||||||
// The user was off their mark! Missed the object to select.
|
// The user was off their mark! Missed the object to select.
|
||||||
if (!target) return
|
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)
|
points.set(target.id, args.intersectionPoint.twoD)
|
||||||
|
|
||||||
|
if (points.size <= 2) return
|
||||||
|
|
||||||
|
await updateCircle3Point()
|
||||||
},
|
},
|
||||||
async onDragEnd(_args) {
|
async onDragEnd(_args) {
|
||||||
target = undefined
|
target = undefined
|
||||||
@ -1407,45 +1542,19 @@ export class SceneEntities {
|
|||||||
if (points.size >= 3) return
|
if (points.size >= 3) return
|
||||||
if (!args.intersectionPoint) return
|
if (!args.intersectionPoint) return
|
||||||
|
|
||||||
const id = createPoint(args.intersectionPoint.threeD)
|
const pointMesh = createPoint(
|
||||||
points.set(id, args.intersectionPoint.twoD)
|
new Vector3(
|
||||||
|
args.intersectionPoint.twoD.x,
|
||||||
if (points.size < 2) return
|
args.intersectionPoint.twoD.y,
|
||||||
|
0
|
||||||
// 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)
|
groupOfDrafts.add(pointMesh)
|
||||||
const startSketchOnASTNode = nodeQueryResult
|
points.set(pointMesh.id, args.intersectionPoint.twoD)
|
||||||
|
|
||||||
const circleParams = circle3Point(Array.from(points.values()))
|
if (points.size <= 2) return
|
||||||
|
|
||||||
if (!circleParams) return
|
await updateCircle3Point()
|
||||||
|
|
||||||
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()
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -9,6 +9,9 @@ import {
|
|||||||
ExtrudeGeometry,
|
ExtrudeGeometry,
|
||||||
Group,
|
Group,
|
||||||
LineCurve3,
|
LineCurve3,
|
||||||
|
LineBasicMaterial,
|
||||||
|
LineDashedMaterial,
|
||||||
|
Line,
|
||||||
Mesh,
|
Mesh,
|
||||||
MeshBasicMaterial,
|
MeshBasicMaterial,
|
||||||
NormalBufferAttributes,
|
NormalBufferAttributes,
|
||||||
@ -1003,6 +1006,49 @@ export function createArcGeometry({
|
|||||||
return geo
|
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(
|
export function dashedStraight(
|
||||||
from: Coords2d,
|
from: Coords2d,
|
||||||
to: Coords2d,
|
to: Coords2d,
|
||||||
|
@ -460,18 +460,16 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
disabled: (state) =>
|
disabled: (state) =>
|
||||||
state.matches('Sketch no face') ||
|
state.matches('Sketch no face') ||
|
||||||
(!canRectangleOrCircleTool(state.context) &&
|
(!canRectangleOrCircleTool(state.context) &&
|
||||||
!state.matches({ Sketch: 'Circle tool' })),
|
!state.matches({ Sketch: 'Circle tool' }) &&
|
||||||
isActive: (state) => state.matches({ Sketch: 'Circle tool' }),
|
!state.matches({ Sketch: 'circle3PointToolSelect' })),
|
||||||
|
isActive: (state) =>
|
||||||
|
state.matches({ Sketch: 'Circle tool' }) ||
|
||||||
|
state.matches({ Sketch: 'circle3PointToolSelect' }),
|
||||||
hotkey: (state) =>
|
hotkey: (state) =>
|
||||||
state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C',
|
state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C',
|
||||||
showTitle: false,
|
showTitle: false,
|
||||||
description: 'Start drawing a circle from its center',
|
description: 'Start drawing a circle from its center',
|
||||||
links: [
|
links: [],
|
||||||
{
|
|
||||||
label: 'GitHub issue',
|
|
||||||
url: 'https://github.com/KittyCAD/modeling-app/issues/1501',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'circle-three-points',
|
id: 'circle-three-points',
|
||||||
@ -488,7 +486,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
}),
|
}),
|
||||||
icon: 'circle',
|
icon: 'circle',
|
||||||
status: 'available',
|
status: 'available',
|
||||||
title: 'Three-point circle',
|
title: '3-point circle',
|
||||||
showTitle: false,
|
showTitle: false,
|
||||||
description: 'Draw a circle defined by three points',
|
description: 'Draw a circle defined by three points',
|
||||||
links: [],
|
links: [],
|
||||||
|
@ -422,6 +422,8 @@ export const modelingMachine = setup({
|
|||||||
},
|
},
|
||||||
'is editing existing sketch': ({ context: { sketchDetails } }) =>
|
'is editing existing sketch': ({ context: { sketchDetails } }) =>
|
||||||
isEditingExistingSketch({ sketchDetails }),
|
isEditingExistingSketch({ sketchDetails }),
|
||||||
|
'is editing 3-point circle': ({ context: { sketchDetails } }) =>
|
||||||
|
isEditing3PointCircle({ sketchDetails }),
|
||||||
'Can make selection horizontal': ({ context: { selectionRanges } }) => {
|
'Can make selection horizontal': ({ context: { selectionRanges } }) => {
|
||||||
const info = horzVertInfo(selectionRanges, 'horizontal')
|
const info = horzVertInfo(selectionRanges, 'horizontal')
|
||||||
if (trap(info)) return false
|
if (trap(info)) return false
|
||||||
@ -2187,6 +2189,10 @@ export const modelingMachine = setup({
|
|||||||
target: 'SketchIdle',
|
target: 'SketchIdle',
|
||||||
guard: 'is editing existing sketch',
|
guard: 'is editing existing sketch',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
target: 'circle3PointToolSelect',
|
||||||
|
guard: 'is editing 3-point circle',
|
||||||
|
},
|
||||||
'Line tool',
|
'Line tool',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -2518,13 +2524,8 @@ export const modelingMachine = setup({
|
|||||||
circle3PointToolSelect: {
|
circle3PointToolSelect: {
|
||||||
invoke: {
|
invoke: {
|
||||||
id: 'actor-circle-3-point',
|
id: 'actor-circle-3-point',
|
||||||
input: function ({ context, event }) {
|
input: function ({ context }) {
|
||||||
// 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
|
|
||||||
if (!context.sketchDetails) return
|
if (!context.sketchDetails) return
|
||||||
|
|
||||||
return context.sketchDetails
|
return context.sketchDetails
|
||||||
},
|
},
|
||||||
src: 'actorCircle3Point',
|
src: 'actorCircle3Point',
|
||||||
@ -2782,6 +2783,34 @@ export function isEditingExistingSketch({
|
|||||||
)
|
)
|
||||||
return (hasStartProfileAt && pipeExpression.body.length > 2) || hasCircle
|
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({
|
export function pipeHasCircle({
|
||||||
sketchDetails,
|
sketchDetails,
|
||||||
}: {
|
}: {
|
||||||
@ -2802,6 +2831,27 @@ export function pipeHasCircle({
|
|||||||
)
|
)
|
||||||
return hasCircle
|
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({
|
export function canRectangleOrCircleTool({
|
||||||
sketchDetails,
|
sketchDetails,
|
||||||
|
@ -70,7 +70,7 @@ mod settings;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod simulation_tests;
|
mod simulation_tests;
|
||||||
mod source_range;
|
mod source_range;
|
||||||
mod std;
|
pub mod std;
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub mod test_server;
|
pub mod test_server;
|
||||||
mod thread;
|
mod thread;
|
||||||
@ -84,7 +84,7 @@ pub use engine::{EngineManager, ExecutionKind};
|
|||||||
pub use errors::{CompilationError, ConnectionError, ExecError, KclError, KclErrorWithOutputs};
|
pub use errors::{CompilationError, ConnectionError, ExecError, KclError, KclErrorWithOutputs};
|
||||||
pub use execution::{
|
pub use execution::{
|
||||||
cache::{CacheInformation, OldAstState},
|
cache::{CacheInformation, OldAstState},
|
||||||
ExecState, ExecutorContext, ExecutorSettings,
|
ExecState, ExecutorContext, ExecutorSettings, Point2d,
|
||||||
};
|
};
|
||||||
pub use lsp::{
|
pub use lsp::{
|
||||||
copilot::Backend as CopilotLspBackend,
|
copilot::Backend as CopilotLspBackend,
|
||||||
|
@ -55,6 +55,10 @@ impl KwArgs {
|
|||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.labeled.len() + if self.unlabeled.is_some() { 1 } else { 0 }
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -270,6 +270,19 @@ pub fn calculate_circle_center(p1: [f64; 2], p2: [f64; 2], p3: [f64; 2]) -> [f64
|
|||||||
[x, y]
|
[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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
// Here you can bring your functions into scope
|
// Here you can bring your functions into scope
|
||||||
|
@ -5,7 +5,7 @@ use std::sync::Arc;
|
|||||||
use futures::stream::TryStreamExt;
|
use futures::stream::TryStreamExt;
|
||||||
use gloo_utils::format::JsValueSerdeExt;
|
use gloo_utils::format::JsValueSerdeExt;
|
||||||
use kcl_lib::{
|
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 tokio::sync::RwLock;
|
||||||
use tower_lsp::{LspService, Server};
|
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"))
|
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