Compare commits

...

6 Commits

4 changed files with 224 additions and 20 deletions

View File

@ -98,6 +98,7 @@ import {
import { getThemeColorForThreeJs } from 'lib/theme' import { getThemeColorForThreeJs } from 'lib/theme'
import { err, trap } from 'lib/trap' import { err, trap } from 'lib/trap'
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer' import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
import { addCircleToSketchAst } from 'lib/circleTool'
type DraftSegment = 'line' | 'tangentialArcTo' type DraftSegment = 'line' | 'tangentialArcTo'
@ -714,11 +715,8 @@ export class SceneEntities {
}) })
} }
setupDraftRectangle = async ( setupDraftRectangle = async (
sketchPathToNode: PathToNode, rectangleOrigin: [number, number],
forward: [number, number, number], { sketchPathToNode, origin, zAxis, yAxis }: SketchDetails
up: [number, number, number],
sketchOrigin: [number, number, number],
rectangleOrigin: [x: number, y: number]
) => { ) => {
let _ast = structuredClone(kclManager.ast) let _ast = structuredClone(kclManager.ast)
@ -750,9 +748,9 @@ export class SceneEntities {
const { programMemoryOverride, truncatedAst } = await this.setupSketch({ const { programMemoryOverride, truncatedAst } = await this.setupSketch({
sketchPathToNode, sketchPathToNode,
forward, forward: yAxis,
up, up: zAxis,
position: sketchOrigin, position: origin,
maybeModdedAst: _ast, maybeModdedAst: _ast,
draftExpressionsIndices: { start: 0, end: 3 }, draftExpressionsIndices: { start: 0, end: 3 },
}) })
@ -862,6 +860,52 @@ export class SceneEntities {
}, },
}) })
} }
setupCircleOriginListener = () => {
sceneInfra.setCallbacks({
onClick: (args) => {
const twoD = args.intersectionPoint?.twoD
if (!twoD) {
console.warn(`This click didn't have a 2D intersection`, args)
return
}
sceneInfra.modelingSend({
type: 'Add circle origin',
data: [twoD.x, twoD.y],
})
},
})
}
setupDraftCircle = async (
circleOrigin: [number, number],
{ sketchPathToNode, origin, zAxis, yAxis }: SketchDetails
) => {
const astWithCircle = await addCircleToSketchAst({
sourceAst: kclManager.ast,
sketchPathToNode,
circleOrigin,
})
const { programMemoryOverride, truncatedAst } = await this.setupSketch({
sketchPathToNode,
forward: yAxis,
up: zAxis,
position: origin,
maybeModdedAst: astWithCircle,
draftExpressionsIndices: { start: 0, end: 3 },
})
// TODO: Move this into the onclick handler to get the real shit
// Update the primary AST and unequip the rectangle tool
await kclManager.executeAstMock(astWithCircle)
sceneInfra.modelingSend({ type: 'CancelSketch' })
const { programMemory } = await executeAst({
ast: astWithCircle,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager,
programMemoryOverride,
})
}
setupSketchIdleCallbacks = ({ setupSketchIdleCallbacks = ({
pathToNode, pathToNode,
up, up,
@ -1176,6 +1220,9 @@ export class SceneEntities {
? orthoFactor ? orthoFactor
: perspScale(sceneInfra.camControls.camera, group)) / : perspScale(sceneInfra.camControls.camera, group)) /
sceneInfra._baseUnitMultiplier sceneInfra._baseUnitMultiplier
console.log('segment type', type)
if (type === TANGENTIAL_ARC_TO_SEGMENT) { if (type === TANGENTIAL_ARC_TO_SEGMENT) {
return this.updateTangentialArcToSegment({ return this.updateTangentialArcToSegment({
prevSegment: sgPaths[index - 1], prevSegment: sgPaths[index - 1],

92
src/lib/circleTool.ts Normal file
View File

@ -0,0 +1,92 @@
import {
createArrayExpression,
createCallExpressionStdLib,
createLiteral,
createPipeExpression,
createPipeSubstitution,
createTagDeclarator,
findUniqueName,
} from 'lang/modifyAst'
import { roundOff } from './utils'
import {
PathToNode,
Program,
VariableDeclaration,
parse,
recast,
} from 'lang/wasm'
import { getNodeFromPath } from 'lang/queryAst'
import { trap } from './trap'
/**
* Hide away the working with the AST
*/
export async function addCircleToSketchAst({
sourceAst,
sketchPathToNode,
circleOrigin,
}: {
sourceAst: Program
sketchPathToNode?: PathToNode
circleOrigin?: [number, number]
}) {
let _ast = JSON.parse(JSON.stringify(sourceAst))
const _node1 = getNodeFromPath<VariableDeclaration>(
_ast,
sketchPathToNode || [],
'VariableDeclaration'
)
if (trap(_node1)) return Promise.reject(_node1)
const tag = findUniqueName(_ast, 'circle')
const _node2 = getNodeFromPath<VariableDeclaration>(
_ast,
sketchPathToNode || [],
'VariableDeclaration'
)
if (trap(_node2)) return Promise.reject(_node2)
const startSketchOn = _node2.node?.declarations
const startSketchOnInit = startSketchOn?.[0]?.init
startSketchOn[0].init = createPipeExpression([
startSketchOnInit,
...getCircleCallExpressions({
center: circleOrigin,
tag,
}),
])
const maybeModdedAst = parse(recast(_ast))
if (trap(maybeModdedAst)) return Promise.reject(maybeModdedAst)
return Promise.resolve(maybeModdedAst)
}
/**
* Returns AST expressions for this KCL code:
* const yo = startSketchOn('XY')
* |> startProfileAt([0, 0], %)
* |> circle([0, 0], 0, %) <- this line
*/
export function getCircleCallExpressions({
center = [0, 0],
radius = 10,
tag,
}: {
center?: [number, number]
radius?: number
tag: string
}) {
return [
createCallExpressionStdLib('circle', [
createArrayExpression([
createLiteral(roundOff(center[0])),
createLiteral(roundOff(center[1])),
]),
createLiteral(roundOff(radius)),
createPipeSubstitution(),
createTagDeclarator(tag),
]),
]
}

View File

@ -293,7 +293,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
status: 'available', status: 'available',
disabled: (state) => disabled: (state) =>
state.matches('Sketch no face') || state.matches('Sketch no face') ||
state.matches('Sketch.Rectangle tool.Awaiting second corner'), state.matches('Sketch.Rectangle tool.Awaiting second corner') ||
state.matches('Sketch.Circle tool.Awaiting perimeter click'),
title: 'Line', title: 'Line',
hotkey: (state) => hotkey: (state) =>
state.matches('Sketch.Line tool') ? ['Esc', 'L'] : 'L', state.matches('Sketch.Line tool') ? ['Esc', 'L'] : 'L',
@ -355,9 +356,20 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
[ [
{ {
id: 'circle-center', id: 'circle-center',
onClick: () => console.error('Center circle not yet implemented'), onClick: ({ modelingStateMatches, modelingSend }) =>
modelingSend({
type: 'change tool',
data: {
tool: !modelingStateMatches('Sketch.Circle tool')
? 'circle'
: 'none',
},
}),
icon: 'circle', icon: 'circle',
status: 'unavailable', status: 'available',
disabled: (state) =>
!canRectangleTool(state.context) &&
!state.matches('Sketch.Circle tool'),
title: 'Center circle', title: 'Center circle',
showTitle: false, showTitle: false,
description: 'Start drawing a circle from its center', description: 'Start drawing a circle from its center',
@ -367,6 +379,9 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
url: 'https://github.com/KittyCAD/modeling-app/issues/1501', url: 'https://github.com/KittyCAD/modeling-app/issues/1501',
}, },
], ],
hotkey: (state) =>
state.matches('Sketch.Circle tool') ? ['Esc', 'C'] : 'C',
isActive: (state) => state.matches('Sketch.Circle tool'),
}, },
{ {
id: 'circle-three-points', id: 'circle-three-points',

View File

@ -141,7 +141,12 @@ interface Store {
openPanes: SidebarType[] openPanes: SidebarType[]
} }
export type SketchTool = 'line' | 'tangentialArc' | 'rectangle' | 'none' export type SketchTool =
| 'line'
| 'tangentialArc'
| 'rectangle'
| 'circle'
| 'none'
export type ModelingMachineEvent = export type ModelingMachineEvent =
| { | {
@ -209,6 +214,10 @@ export type ModelingMachineEvent =
type: 'Add rectangle origin' type: 'Add rectangle origin'
data: [x: number, y: number] data: [x: number, y: number]
} }
| {
type: 'Add circle origin'
data: [x: number, y: number]
}
| { | {
type: 'done.invoke.animate-to-face' | 'done.invoke.animate-to-sketch' type: 'done.invoke.animate-to-face' | 'done.invoke.animate-to-sketch'
data: SketchDetails data: SketchDetails
@ -591,6 +600,8 @@ export const modelingMachine = createMachine(
Cancel: '#Modeling.Sketch.undo startSketchOn', Cancel: '#Modeling.Sketch.undo startSketchOn',
}, },
}, },
'new state 1': {},
}, },
initial: 'Init', initial: 'Init',
@ -796,8 +807,31 @@ export const modelingMachine = createMachine(
target: 'Tangential arc to', target: 'Tangential arc to',
cond: 'next is tangential arc', cond: 'next is tangential arc',
}, },
{
target: 'Circle tool',
cond: 'next is circle',
},
], ],
}, },
'Circle tool': {
entry: 'listen for circle origin',
states: {
'Awaiting origin': {
on: {
'Add circle origin': {
target: 'Awaiting perimeter click',
actions: 'set up draft circle',
},
},
},
'Awaiting perimeter click': {},
},
initial: 'Awaiting origin',
},
}, },
initial: 'Init', initial: 'Init',
@ -1041,6 +1075,14 @@ export const modelingMachine = createMachine(
if ((state?.event as any).data.tool !== 'rectangle') return false if ((state?.event as any).data.tool !== 'rectangle') return false
return canRectangleTool({ sketchDetails }) return canRectangleTool({ sketchDetails })
}, },
'next is circle': ({ sketchDetails }, _, { state }) => {
if ((state?.event as any).data.tool !== 'circle') return false
// TODO: Both this an rectangle can currently only be used
// if the sketch is empty. They share limited implementations,
// so I'm using the same cond until we have
// multi-profile sketch support in.
return canRectangleTool({ sketchDetails })
},
'next is line': (_, __, { state }) => 'next is line': (_, __, { state }) =>
(state?.event as any).data.tool === 'line', (state?.event as any).data.tool === 'line',
'next is none': (_, __, { state }) => 'next is none': (_, __, { state }) =>
@ -1281,13 +1323,18 @@ export const modelingMachine = createMachine(
}, },
'set up draft rectangle': ({ sketchDetails }, { data }) => { 'set up draft rectangle': ({ sketchDetails }, { data }) => {
if (!sketchDetails || !data) return if (!sketchDetails || !data) return
sceneEntitiesManager.setupDraftRectangle(
sketchDetails.sketchPathToNode, sceneEntitiesManager.setupDraftRectangle(data, sketchDetails)
sketchDetails.zAxis, },
sketchDetails.yAxis, 'listen for circle origin': ({ sketchDetails }) => {
sketchDetails.origin, if (!sketchDetails) return
data sceneEntitiesManager.setupCircleOriginListener()
) },
'set up draft circle': ({ sketchDetails }, { data }) => {
if (!sketchDetails || !data) return
console.log('setting up draft circle', data)
sceneEntitiesManager.setupDraftCircle(data, sketchDetails)
}, },
'set up draft line without teardown': ({ sketchDetails }) => { 'set up draft line without teardown': ({ sketchDetails }) => {
if (!sketchDetails) return if (!sketchDetails) return
@ -1658,7 +1705,10 @@ export function isEditingExistingSketch({
(item) => (item) =>
item.type === 'CallExpression' && item.callee.name === 'startProfileAt' item.type === 'CallExpression' && item.callee.name === 'startProfileAt'
) )
return hasStartProfileAt && pipeExpression.body.length > 2 const hasCircle = pipeExpression.body.some(
(item) => item.type === 'CallExpression' && item.callee.name === 'circle'
)
return (hasStartProfileAt && pipeExpression.body.length > 2) || hasCircle
} }
export function canRectangleTool({ export function canRectangleTool({