most of the fix for 3 point circle

This commit is contained in:
Kurt Hutten Irev-Dev
2025-01-31 21:26:19 +11:00
parent 33468c4c96
commit 5a5138a703
13 changed files with 1014 additions and 632 deletions

View File

@ -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,
})
}
}
}

View File

@ -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
}

View File

@ -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(

View File

@ -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

View File

@ -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({

View File

@ -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,

View File

@ -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)

View File

@ -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
}

View File

@ -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,

View File

@ -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(

View File

@ -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

View File

@ -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