Boolean create UI (#5906)
* first steps, add to cmd bar etc * cmdbar working well enough * mvp * lint * fix after rebase * intersect and union mvps * add test * some clean up * further fix up * Update src/lang/modifyAst/boolean.ts Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com> * Update src/lang/modifyAst/boolean.ts Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com> * pierre's comments * tsc * add comment --------- Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
This commit is contained in:
117
e2e/playwright/boolean.spec.ts
Normal file
117
e2e/playwright/boolean.spec.ts
Normal file
@ -0,0 +1,117 @@
|
||||
import { test, expect } from './zoo-test'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
test.describe('Point and click for boolean workflows', () => {
|
||||
// Boolean operations to test
|
||||
const booleanOperations = [
|
||||
{
|
||||
name: 'union',
|
||||
code: 'union([extrude001, extrude006])',
|
||||
},
|
||||
{
|
||||
name: 'subtract',
|
||||
code: 'subtract([extrude001], tools = [extrude006])',
|
||||
},
|
||||
{
|
||||
name: 'intersect',
|
||||
code: 'intersect([extrude001, extrude006])',
|
||||
},
|
||||
] as const
|
||||
for (let i = 0; i < booleanOperations.length; i++) {
|
||||
const operation = booleanOperations[i]
|
||||
const operationName = operation.name
|
||||
const commandName = `Boolean ${
|
||||
operationName.charAt(0).toUpperCase() + operationName.slice(1)
|
||||
}`
|
||||
test(`Create boolean operation -- ${operationName}`, async ({
|
||||
context,
|
||||
homePage,
|
||||
cmdBar,
|
||||
editor,
|
||||
toolbar,
|
||||
scene,
|
||||
page,
|
||||
}) => {
|
||||
const file = await fs.readFile(
|
||||
path.resolve(
|
||||
__dirname,
|
||||
'../../',
|
||||
'./rust/kcl-lib/e2e/executor/inputs/boolean-setup-with'
|
||||
),
|
||||
'utf-8'
|
||||
)
|
||||
await context.addInitScript((file) => {
|
||||
localStorage.setItem('persistCode', file)
|
||||
}, file)
|
||||
await homePage.goToModelingScene()
|
||||
await scene.waitForExecutionDone()
|
||||
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
// Test coordinates for selection - these might need adjustment based on actual scene layout
|
||||
const cylinderPoint = { x: 592, y: 174 }
|
||||
const secondObjectPoint = { x: 683, y: 273 }
|
||||
|
||||
// Create mouse helpers for selecting objects
|
||||
const [clickFirstObject] = scene.makeMouseHelpers(
|
||||
cylinderPoint.x,
|
||||
cylinderPoint.y,
|
||||
{ steps: 10 }
|
||||
)
|
||||
const [clickSecondObject] = scene.makeMouseHelpers(
|
||||
secondObjectPoint.x,
|
||||
secondObjectPoint.y,
|
||||
{ steps: 10 }
|
||||
)
|
||||
|
||||
await test.step(`Test ${operationName} operation`, async () => {
|
||||
// Click the boolean operation button in the toolbar
|
||||
await toolbar.selectBoolean(operationName)
|
||||
|
||||
// Verify command bar is showing the right command
|
||||
await expect(cmdBar.page.getByTestId('command-name')).toContainText(
|
||||
commandName
|
||||
)
|
||||
|
||||
// Select first object in the scene, expect there to be a pixel diff from the selection color change
|
||||
await clickFirstObject({ pixelDiff: 50 })
|
||||
|
||||
// For subtract, we need to proceed to the next step before selecting the second object
|
||||
if (operationName !== 'subtract') {
|
||||
// should down shift key to select multiple objects
|
||||
await page.keyboard.down('Shift')
|
||||
}
|
||||
|
||||
// Select second object
|
||||
await clickSecondObject({ pixelDiff: 50 })
|
||||
|
||||
// Confirm the operation in the command bar
|
||||
await cmdBar.progressCmdBar()
|
||||
|
||||
if (operationName === 'union' || operationName === 'intersect') {
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: {
|
||||
Solids: '2 paths',
|
||||
},
|
||||
commandName,
|
||||
})
|
||||
} else if (operationName === 'subtract') {
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: {
|
||||
Tool: '1 path',
|
||||
Target: '1 path',
|
||||
},
|
||||
commandName,
|
||||
})
|
||||
}
|
||||
|
||||
await cmdBar.submit()
|
||||
|
||||
await editor.expectEditor.toContain(operation.code)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
@ -181,6 +181,14 @@ export class ToolbarFixture {
|
||||
).toBeVisible()
|
||||
await this.page.getByTestId('dropdown-center-rectangle').click()
|
||||
}
|
||||
selectBoolean = async (operation: 'union' | 'subtract' | 'intersect') => {
|
||||
await this.page
|
||||
.getByRole('button', { name: 'caret down Union: open menu' })
|
||||
.click()
|
||||
const operationTestId = `dropdown-boolean-${operation}`
|
||||
await expect(this.page.getByTestId(operationTestId)).toBeVisible()
|
||||
await this.page.getByTestId(operationTestId).click()
|
||||
}
|
||||
|
||||
selectCircleThreePoint = async () => {
|
||||
await this.page
|
||||
|
77
rust/kcl-lib/e2e/executor/inputs/boolean-setup-with
Normal file
77
rust/kcl-lib/e2e/executor/inputs/boolean-setup-with
Normal file
@ -0,0 +1,77 @@
|
||||
@settings(defaultLengthUnit = mm)
|
||||
|
||||
sketch001 = startSketchOn(XZ)
|
||||
profile001 = circle(sketch001, center = [154.36, 113.92], radius = 41.09)
|
||||
extrude001 = extrude(profile001, length = 200)
|
||||
sketch002 = startSketchOn(XY)
|
||||
profile002 = startProfileAt([72.24, -52.05], sketch002)
|
||||
|> angledLine([0, 181.26], %, $rectangleSegmentA001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001) - 90,
|
||||
21.54
|
||||
], %)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001),
|
||||
-segLen(rectangleSegmentA001)
|
||||
], %, $mySeg)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude002 = extrude(profile002, length = 150)
|
||||
|> chamfer(
|
||||
%,
|
||||
length = 15,
|
||||
tags = [mySeg],
|
||||
tag = $seg02,
|
||||
)
|
||||
|
||||
sketch003 = startSketchOn(extrude002, mySeg)
|
||||
profile003 = startProfileAt([207.36, 126.19], sketch003)
|
||||
|> angledLine([0, 33.57], %, $rectangleSegmentA002)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA002) - 90,
|
||||
99.11
|
||||
], %)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA002),
|
||||
-segLen(rectangleSegmentA002)
|
||||
], %, $seg01)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude003 = extrude(profile003, length = -20)
|
||||
sketch004 = startSketchOn(extrude003, seg01)
|
||||
profile004 = startProfileAt([-235.38, 66.16], sketch004)
|
||||
|> angledLine([0, 24.21], %, $rectangleSegmentA003)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA003) - 90,
|
||||
3.72
|
||||
], %)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA003),
|
||||
-segLen(rectangleSegmentA003)
|
||||
], %)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude004 = extrude(profile004, length = 30)
|
||||
|
||||
sketch005 = startSketchOn(extrude002, seg02)
|
||||
profile005 = startProfileAt([-129.93, -59.19], sketch005)
|
||||
|> xLine(length = 48.79)
|
||||
|> line(end = [1.33, 11.03])
|
||||
|> xLine(length = -60.56, tag = $seg03)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude005 = extrude(profile005, length = -10)
|
||||
sketch006 = startSketchOn(extrude005, seg03)
|
||||
profile006 = startProfileAt([-95.86, 38.73], sketch006)
|
||||
|> angledLine([0, 3.48], %, $rectangleSegmentA004)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA004) - 90,
|
||||
3.36
|
||||
], %)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA004),
|
||||
-segLen(rectangleSegmentA004)
|
||||
], %)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude006 = extrude(profile006, length = 13)
|
@ -6,7 +6,7 @@ import {
|
||||
getSelectionCountByType,
|
||||
getSelectionTypeDisplayText,
|
||||
} from 'lib/selections'
|
||||
import { kclManager } from 'lib/singletons'
|
||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { toSync } from 'lib/utils'
|
||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||
@ -112,6 +112,23 @@ function CommandBarSelectionInput({
|
||||
onSubmit(selection)
|
||||
}
|
||||
|
||||
// Clear selection if needed
|
||||
useEffect(() => {
|
||||
arg.clearSelectionFirst &&
|
||||
engineCommandManager.modelingSend({
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
selectionType: 'singleCodeCursor',
|
||||
},
|
||||
})
|
||||
}, [arg.clearSelectionFirst])
|
||||
|
||||
// Set selection filter if needed, and reset it when the component unmounts
|
||||
useEffect(() => {
|
||||
arg.selectionFilter && kclManager.setSelectionFilter(arg.selectionFilter)
|
||||
return () => kclManager.defaultSelectionFilter(selection)
|
||||
}, [arg.selectionFilter])
|
||||
|
||||
return (
|
||||
<form id="arg-form" onSubmit={handleSubmit}>
|
||||
<label
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
} from 'lib/selections'
|
||||
import { useSelector } from '@xstate/react'
|
||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||
import { kclManager } from 'lib/singletons'
|
||||
|
||||
const selectionSelector = (snapshot: any) => snapshot?.context.selectionRanges
|
||||
|
||||
@ -56,6 +57,12 @@ export default function CommandBarSelectionMixedInput({
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Set selection filter if needed, and reset it when the component unmounts
|
||||
useEffect(() => {
|
||||
arg.selectionFilter && kclManager.setSelectionFilter(arg.selectionFilter)
|
||||
return () => kclManager.defaultSelectionFilter(selection)
|
||||
}, [arg.selectionFilter])
|
||||
|
||||
function handleChange() {
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
|
338
src/lang/modifyAst/boolean.ts
Normal file
338
src/lang/modifyAst/boolean.ts
Normal file
@ -0,0 +1,338 @@
|
||||
import { Node } from '@rust/kcl-lib/bindings/Node'
|
||||
import EditorManager from 'editor/manager'
|
||||
import CodeManager from 'lang/codeManager'
|
||||
import { KclManager } from 'lang/KclSingleton'
|
||||
import { updateModelingState } from 'lang/modelingWorkflows'
|
||||
import {
|
||||
createArrayExpression,
|
||||
createCallExpressionStdLibKw,
|
||||
createLabeledArg,
|
||||
createLocalName,
|
||||
createVariableDeclaration,
|
||||
findUniqueName,
|
||||
} from 'lang/modifyAst'
|
||||
import { getNodeFromPath } from 'lang/queryAst'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { getFaceCodeRef } from 'lang/std/artifactGraph'
|
||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||
import {
|
||||
Artifact,
|
||||
ArtifactGraph,
|
||||
Program,
|
||||
VariableDeclaration,
|
||||
} from 'lang/wasm'
|
||||
import { EXECUTION_TYPE_REAL } from 'lib/constants'
|
||||
import { Selection, Selections } from 'lib/selections'
|
||||
import { err } from 'lib/trap'
|
||||
import { isArray } from 'lib/utils'
|
||||
|
||||
export async function applySubtractFromTargetOperatorSelections(
|
||||
target: Selection,
|
||||
tool: Selection,
|
||||
dependencies: {
|
||||
kclManager: KclManager
|
||||
engineCommandManager: EngineCommandManager
|
||||
codeManager: CodeManager
|
||||
editorManager: EditorManager
|
||||
}
|
||||
): Promise<Error | void> {
|
||||
const ast = dependencies.kclManager.ast
|
||||
if (!target.artifact || !tool.artifact) {
|
||||
return new Error('No artifact found')
|
||||
}
|
||||
const orderedChildrenTarget = findAllChildrenAndOrderByPlaceInCode(
|
||||
target.artifact,
|
||||
dependencies.engineCommandManager.artifactGraph
|
||||
)
|
||||
const orderedChildrenTool = findAllChildrenAndOrderByPlaceInCode(
|
||||
tool.artifact,
|
||||
dependencies.engineCommandManager.artifactGraph
|
||||
)
|
||||
|
||||
const lastVarTarget = getLastVariable(orderedChildrenTarget, ast)
|
||||
const lastVarTool = getLastVariable(orderedChildrenTool, ast)
|
||||
|
||||
if (!lastVarTarget || !lastVarTool) {
|
||||
return new Error('No variable found')
|
||||
}
|
||||
const modifiedAst = booleanSubtractAstMod({
|
||||
ast,
|
||||
targets: [lastVarTarget?.variableDeclaration?.node],
|
||||
tools: [lastVarTool?.variableDeclaration.node],
|
||||
})
|
||||
|
||||
await updateModelingState(modifiedAst, EXECUTION_TYPE_REAL, dependencies)
|
||||
}
|
||||
|
||||
export async function applyUnionFromTargetOperatorSelections(
|
||||
solids: Selections,
|
||||
dependencies: {
|
||||
kclManager: KclManager
|
||||
engineCommandManager: EngineCommandManager
|
||||
codeManager: CodeManager
|
||||
editorManager: EditorManager
|
||||
}
|
||||
): Promise<Error | void> {
|
||||
const ast = dependencies.kclManager.ast
|
||||
|
||||
const artifacts: Artifact[] = []
|
||||
for (const selection of solids.graphSelections) {
|
||||
if (selection.artifact) {
|
||||
artifacts.push(selection.artifact)
|
||||
}
|
||||
}
|
||||
|
||||
if (artifacts.length < 2) {
|
||||
return new Error('Not enough artifacts selected')
|
||||
}
|
||||
|
||||
const orderedChildrenEach = artifacts.map((artifact) =>
|
||||
findAllChildrenAndOrderByPlaceInCode(
|
||||
artifact,
|
||||
dependencies.engineCommandManager.artifactGraph
|
||||
)
|
||||
)
|
||||
|
||||
const lastVars: VariableDeclaration[] = []
|
||||
for (const orderedArtifactLeafs of orderedChildrenEach) {
|
||||
const lastVar = getLastVariable(orderedArtifactLeafs, ast)
|
||||
if (!lastVar) continue
|
||||
lastVars.push(lastVar.variableDeclaration.node)
|
||||
}
|
||||
|
||||
if (lastVars.length < 2) {
|
||||
return new Error('Not enough variables found')
|
||||
}
|
||||
|
||||
const modifiedAst = booleanUnionAstMod({
|
||||
ast,
|
||||
solids: lastVars,
|
||||
})
|
||||
await updateModelingState(modifiedAst, EXECUTION_TYPE_REAL, dependencies)
|
||||
}
|
||||
|
||||
export async function applyIntersectFromTargetOperatorSelections(
|
||||
solids: Selections,
|
||||
dependencies: {
|
||||
kclManager: KclManager
|
||||
engineCommandManager: EngineCommandManager
|
||||
codeManager: CodeManager
|
||||
editorManager: EditorManager
|
||||
}
|
||||
): Promise<Error | void> {
|
||||
const ast = dependencies.kclManager.ast
|
||||
|
||||
const artifacts: Artifact[] = []
|
||||
for (const selection of solids.graphSelections) {
|
||||
if (selection.artifact) {
|
||||
artifacts.push(selection.artifact)
|
||||
}
|
||||
}
|
||||
|
||||
if (artifacts.length < 2) {
|
||||
return new Error('Not enough artifacts selected')
|
||||
}
|
||||
|
||||
const orderedChildrenEach = artifacts.map((artifact) =>
|
||||
findAllChildrenAndOrderByPlaceInCode(
|
||||
artifact,
|
||||
dependencies.engineCommandManager.artifactGraph
|
||||
)
|
||||
)
|
||||
|
||||
const lastVars: VariableDeclaration[] = []
|
||||
for (const orderedArtifactLeafs of orderedChildrenEach) {
|
||||
const lastVar = getLastVariable(orderedArtifactLeafs, ast)
|
||||
if (!lastVar) continue
|
||||
lastVars.push(lastVar.variableDeclaration.node)
|
||||
}
|
||||
|
||||
if (lastVars.length < 2) {
|
||||
return new Error('Not enough variables found')
|
||||
}
|
||||
|
||||
const modifiedAst = booleanIntersectAstMod({
|
||||
ast,
|
||||
solids: lastVars,
|
||||
})
|
||||
await updateModelingState(modifiedAst, EXECUTION_TYPE_REAL, dependencies)
|
||||
}
|
||||
|
||||
/** returns all children of a given artifact, and sorts them DESC by start sourceRange
|
||||
* The usecase is we want the last declare relevant child to use in the boolean operations
|
||||
* but might be useful else where.
|
||||
*/
|
||||
export function findAllChildrenAndOrderByPlaceInCode(
|
||||
artifact: Artifact,
|
||||
artifactGraph: ArtifactGraph
|
||||
): Artifact[] {
|
||||
const result: string[] = []
|
||||
const stack: string[] = [artifact.id]
|
||||
|
||||
const getArtifacts = (stringIds: string[]): Artifact[] => {
|
||||
const artifactsWithCodeRefs: Artifact[] = []
|
||||
for (const id of stringIds) {
|
||||
const artifact = artifactGraph.get(id)
|
||||
if (artifact) {
|
||||
const codeRef = getFaceCodeRef(artifact)
|
||||
if (codeRef && codeRef.range[1] > 0) {
|
||||
artifactsWithCodeRefs.push(artifact)
|
||||
}
|
||||
}
|
||||
}
|
||||
return artifactsWithCodeRefs
|
||||
}
|
||||
|
||||
const pushToSomething = (
|
||||
resultId: string,
|
||||
childrenIdOrIds: null | string | string[]
|
||||
) => {
|
||||
if (isArray(childrenIdOrIds)) {
|
||||
if (childrenIdOrIds.length) {
|
||||
stack.push(...childrenIdOrIds)
|
||||
result.push(resultId)
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
if (childrenIdOrIds) {
|
||||
stack.push(childrenIdOrIds)
|
||||
result.push(resultId)
|
||||
} else {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (stack.length > 0) {
|
||||
const currentId = stack.pop()!
|
||||
const current = artifactGraph.get(currentId)
|
||||
if (current?.type === 'path') {
|
||||
pushToSomething(currentId, current?.sweepId)
|
||||
pushToSomething(currentId, current?.segIds)
|
||||
} else if (current?.type === 'sweep') {
|
||||
pushToSomething(currentId, current?.surfaceIds)
|
||||
} else if (current?.type === 'wall' || current?.type === 'cap') {
|
||||
pushToSomething(currentId, current?.pathIds)
|
||||
} else if (current?.type === 'segment') {
|
||||
pushToSomething(currentId, current?.edgeCutId)
|
||||
pushToSomething(currentId, current?.surfaceId)
|
||||
} else if (current?.type === 'edgeCut') {
|
||||
pushToSomething(currentId, current?.surfaceId)
|
||||
} else if (current?.type === 'startSketchOnPlane') {
|
||||
pushToSomething(currentId, current?.planeId)
|
||||
} else if (current?.type === 'plane') {
|
||||
pushToSomething(currentId, current.pathIds)
|
||||
}
|
||||
}
|
||||
|
||||
const codeRefArtifacts = getArtifacts(result)
|
||||
const orderedByCodeRefDest = codeRefArtifacts.sort((a, b) => {
|
||||
const aCodeRef = getFaceCodeRef(a)
|
||||
const bCodeRef = getFaceCodeRef(b)
|
||||
if (!aCodeRef || !bCodeRef) {
|
||||
return 0
|
||||
}
|
||||
return bCodeRef.range[0] - aCodeRef.range[0]
|
||||
})
|
||||
|
||||
return orderedByCodeRefDest
|
||||
}
|
||||
|
||||
/** Returns the last declared in code, relevant child */
|
||||
export function getLastVariable(
|
||||
orderedDescArtifacts: Artifact[],
|
||||
ast: Node<Program>
|
||||
) {
|
||||
for (const artifact of orderedDescArtifacts) {
|
||||
const codeRef = getFaceCodeRef(artifact)
|
||||
if (codeRef) {
|
||||
const pathToNode = getNodePathFromSourceRange(ast, codeRef.range)
|
||||
const varDec = getNodeFromPath<VariableDeclaration>(
|
||||
ast,
|
||||
pathToNode,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (!err(varDec)) {
|
||||
return {
|
||||
variableDeclaration: varDec,
|
||||
pathToNode: pathToNode,
|
||||
artifact,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function booleanSubtractAstMod({
|
||||
ast,
|
||||
targets,
|
||||
tools,
|
||||
}: {
|
||||
ast: Node<Program>
|
||||
targets: VariableDeclaration[]
|
||||
tools: VariableDeclaration[]
|
||||
}): Node<Program> {
|
||||
const newAst = structuredClone(ast)
|
||||
const newVarName = findUniqueName(newAst, 'solid')
|
||||
const createArrExpr = (varDecs: VariableDeclaration[]) =>
|
||||
createArrayExpression(
|
||||
varDecs.map((varDec) => createLocalName(varDec.declaration.id.name))
|
||||
)
|
||||
const targetsArrayExpression = createArrExpr(targets)
|
||||
const toolsArrayExpression = createArrExpr(tools)
|
||||
|
||||
const newVarDec = createVariableDeclaration(
|
||||
newVarName,
|
||||
createCallExpressionStdLibKw('subtract', targetsArrayExpression, [
|
||||
createLabeledArg('tools', toolsArrayExpression),
|
||||
])
|
||||
)
|
||||
newAst.body.push(newVarDec)
|
||||
return newAst
|
||||
}
|
||||
|
||||
export function booleanUnionAstMod({
|
||||
ast,
|
||||
solids,
|
||||
}: {
|
||||
ast: Node<Program>
|
||||
solids: VariableDeclaration[]
|
||||
}): Node<Program> {
|
||||
const newAst = structuredClone(ast)
|
||||
const newVarName = findUniqueName(newAst, 'solid')
|
||||
const createArrExpr = (varDecs: VariableDeclaration[]) =>
|
||||
createArrayExpression(
|
||||
varDecs.map((varDec) => createLocalName(varDec.declaration.id.name))
|
||||
)
|
||||
const solidsArrayExpression = createArrExpr(solids)
|
||||
|
||||
const newVarDec = createVariableDeclaration(
|
||||
newVarName,
|
||||
createCallExpressionStdLibKw('union', solidsArrayExpression, [])
|
||||
)
|
||||
newAst.body.push(newVarDec)
|
||||
return newAst
|
||||
}
|
||||
|
||||
export function booleanIntersectAstMod({
|
||||
ast,
|
||||
solids,
|
||||
}: {
|
||||
ast: Node<Program>
|
||||
solids: VariableDeclaration[]
|
||||
}): Node<Program> {
|
||||
const newAst = structuredClone(ast)
|
||||
const newVarName = findUniqueName(newAst, 'solid')
|
||||
const createArrExpr = (varDecs: VariableDeclaration[]) =>
|
||||
createArrayExpression(
|
||||
varDecs.map((varDec) => createLocalName(varDec.declaration.id.name))
|
||||
)
|
||||
const solidsArrayExpression = createArrExpr(solids)
|
||||
|
||||
const newVarDec = createVariableDeclaration(
|
||||
newVarName,
|
||||
createCallExpressionStdLibKw('intersect', solidsArrayExpression, [])
|
||||
)
|
||||
newAst.body.push(newVarDec)
|
||||
return newAst
|
||||
}
|
@ -23,6 +23,8 @@ import {
|
||||
import { getVariableDeclaration } from 'lang/queryAst/getVariableDeclaration'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { getNodeFromPath } from 'lang/queryAst'
|
||||
import { IS_NIGHTLY_OR_DEBUG } from 'routes/Settings'
|
||||
import { DEV } from 'env'
|
||||
|
||||
type OutputFormat = Models['OutputFormat3d_type']
|
||||
type OutputTypeKey = OutputFormat['type']
|
||||
@ -153,6 +155,16 @@ export type ModelingCommandSchema = {
|
||||
nodeToEdit?: PathToNode
|
||||
color: string
|
||||
}
|
||||
'Boolean Subtract': {
|
||||
target: Selections
|
||||
tool: Selections
|
||||
}
|
||||
'Boolean Union': {
|
||||
solids: Selections
|
||||
}
|
||||
'Boolean Intersect': {
|
||||
solids: Selections
|
||||
}
|
||||
}
|
||||
|
||||
export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
@ -507,6 +519,67 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
},
|
||||
},
|
||||
},
|
||||
'Boolean Subtract': {
|
||||
hide: DEV || IS_NIGHTLY_OR_DEBUG ? undefined : 'both',
|
||||
description: 'Subtract one solid from another.',
|
||||
icon: 'booleanSubtract',
|
||||
needsReview: true,
|
||||
args: {
|
||||
target: {
|
||||
inputType: 'selection',
|
||||
selectionTypes: ['path'],
|
||||
selectionFilter: ['object'],
|
||||
multiple: false,
|
||||
required: true,
|
||||
skip: true,
|
||||
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
|
||||
},
|
||||
tool: {
|
||||
clearSelectionFirst: true,
|
||||
inputType: 'selection',
|
||||
selectionTypes: ['path'],
|
||||
selectionFilter: ['object'],
|
||||
multiple: false,
|
||||
required: true,
|
||||
skip: false,
|
||||
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
|
||||
},
|
||||
},
|
||||
},
|
||||
'Boolean Union': {
|
||||
hide: DEV || IS_NIGHTLY_OR_DEBUG ? undefined : 'both',
|
||||
description: 'Union multiple solids into a single solid.',
|
||||
icon: 'booleanUnion',
|
||||
needsReview: true,
|
||||
args: {
|
||||
solids: {
|
||||
inputType: 'selection',
|
||||
selectionTypes: ['path'],
|
||||
selectionFilter: ['object'],
|
||||
multiple: true,
|
||||
required: true,
|
||||
skip: false,
|
||||
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
|
||||
},
|
||||
},
|
||||
},
|
||||
'Boolean Intersect': {
|
||||
hide: DEV || IS_NIGHTLY_OR_DEBUG ? undefined : 'both',
|
||||
description: 'Subtract one solid from another.',
|
||||
icon: 'booleanIntersect',
|
||||
needsReview: true,
|
||||
args: {
|
||||
solids: {
|
||||
inputType: 'selectionMixed',
|
||||
selectionTypes: ['path'],
|
||||
selectionFilter: ['object'],
|
||||
multiple: true,
|
||||
required: true,
|
||||
skip: false,
|
||||
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
|
||||
},
|
||||
},
|
||||
},
|
||||
'Offset plane': {
|
||||
description: 'Offset a plane.',
|
||||
icon: 'plane',
|
||||
@ -940,3 +1013,5 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
modelingMachineCommandConfig
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { CustomIconName } from 'components/CustomIcon'
|
||||
import { AllMachines } from 'hooks/useStateMachineCommands'
|
||||
import { Actor, AnyStateMachine, ContextFrom, EventFrom } from 'xstate'
|
||||
import { Expr, VariableDeclaration } from 'lang/wasm'
|
||||
import { Expr, Name, VariableDeclaration } from 'lang/wasm'
|
||||
import { commandBarMachine } from 'machines/commandBarMachine'
|
||||
import { ReactNode } from 'react'
|
||||
import { MachineManager } from 'components/MachineManagerProvider'
|
||||
import { Node } from '@rust/kcl-lib/bindings/Node'
|
||||
import { Artifact } from 'lang/std/artifactGraph'
|
||||
import { CommandBarContext } from 'machines/commandBarMachine'
|
||||
import { Name } from '@rust/kcl-lib/bindings/Name'
|
||||
import { EntityType_type } from '@kittycad/lib/dist/types/src/models'
|
||||
|
||||
type Icon = CustomIconName
|
||||
const _PLATFORMS = ['both', 'web', 'desktop'] as const
|
||||
@ -160,6 +160,8 @@ export type CommandArgumentConfig<
|
||||
| {
|
||||
inputType: 'selection'
|
||||
selectionTypes: Artifact['type'][]
|
||||
clearSelectionFirst?: boolean
|
||||
selectionFilter?: EntityType_type[]
|
||||
multiple: boolean
|
||||
validation?: ({
|
||||
data,
|
||||
@ -172,6 +174,7 @@ export type CommandArgumentConfig<
|
||||
| {
|
||||
inputType: 'selectionMixed'
|
||||
selectionTypes: Artifact['type'][]
|
||||
selectionFilter?: EntityType_type[]
|
||||
multiple: boolean
|
||||
allowNoSelection?: boolean
|
||||
validation?: ({
|
||||
@ -281,6 +284,8 @@ export type CommandArgument<
|
||||
| {
|
||||
inputType: 'selection'
|
||||
selectionTypes: Artifact['type'][]
|
||||
clearSelectionFirst?: boolean
|
||||
selectionFilter?: EntityType_type[]
|
||||
multiple: boolean
|
||||
validation?: ({
|
||||
data,
|
||||
@ -293,6 +298,7 @@ export type CommandArgument<
|
||||
| {
|
||||
inputType: 'selectionMixed'
|
||||
selectionTypes: Artifact['type'][]
|
||||
selectionFilter?: EntityType_type[]
|
||||
multiple: boolean
|
||||
allowNoSelection?: boolean
|
||||
validation?: ({
|
||||
|
@ -188,6 +188,8 @@ export function buildCommandArgument<
|
||||
multiple: arg.multiple,
|
||||
selectionTypes: arg.selectionTypes,
|
||||
validation: arg.validation,
|
||||
clearSelectionFirst: arg.clearSelectionFirst,
|
||||
selectionFilter: arg.selectionFilter,
|
||||
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
|
||||
} else if (arg.inputType === 'selectionMixed') {
|
||||
return {
|
||||
@ -198,6 +200,7 @@ export function buildCommandArgument<
|
||||
validation: arg.validation,
|
||||
allowNoSelection: arg.allowNoSelection,
|
||||
selectionSource: arg.selectionSource,
|
||||
selectionFilter: arg.selectionFilter,
|
||||
} satisfies CommandArgument<O, T> & { inputType: 'selectionMixed' }
|
||||
} else if (arg.inputType === 'kcl') {
|
||||
return {
|
||||
|
@ -209,9 +209,13 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
[
|
||||
{
|
||||
id: 'boolean-union',
|
||||
onClick: () => console.error('Boolean union not yet implemented'),
|
||||
onClick: () =>
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: { name: 'Boolean Union', groupId: 'modeling' },
|
||||
}),
|
||||
icon: 'booleanUnion',
|
||||
status: 'unavailable',
|
||||
status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'unavailable',
|
||||
title: 'Union',
|
||||
hotkey: 'Shift + B U',
|
||||
description: 'Combine two or more solids into a single solid.',
|
||||
@ -224,9 +228,13 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
},
|
||||
{
|
||||
id: 'boolean-subtract',
|
||||
onClick: () => console.error('Boolean subtract not yet implemented'),
|
||||
onClick: () =>
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: { name: 'Boolean Subtract', groupId: 'modeling' },
|
||||
}),
|
||||
icon: 'booleanSubtract',
|
||||
status: 'unavailable',
|
||||
status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'unavailable',
|
||||
title: 'Subtract',
|
||||
hotkey: 'Shift + B S',
|
||||
description: 'Subtract one solid from another.',
|
||||
@ -239,9 +247,13 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
},
|
||||
{
|
||||
id: 'boolean-intersect',
|
||||
onClick: () => console.error('Boolean intersect not yet implemented'),
|
||||
onClick: () =>
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: { name: 'Boolean Intersect', groupId: 'modeling' },
|
||||
}),
|
||||
icon: 'booleanIntersect',
|
||||
status: 'unavailable',
|
||||
status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'unavailable',
|
||||
title: 'Intersect',
|
||||
hotkey: 'Shift + B I',
|
||||
description: 'Create a solid from the intersection of two solids.',
|
||||
|
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user