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()
|
).toBeVisible()
|
||||||
await this.page.getByTestId('dropdown-center-rectangle').click()
|
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 () => {
|
selectCircleThreePoint = async () => {
|
||||||
await this.page
|
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,
|
getSelectionCountByType,
|
||||||
getSelectionTypeDisplayText,
|
getSelectionTypeDisplayText,
|
||||||
} from 'lib/selections'
|
} from 'lib/selections'
|
||||||
import { kclManager } from 'lib/singletons'
|
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
import { toSync } from 'lib/utils'
|
import { toSync } from 'lib/utils'
|
||||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||||
@ -112,6 +112,23 @@ function CommandBarSelectionInput({
|
|||||||
onSubmit(selection)
|
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 (
|
return (
|
||||||
<form id="arg-form" onSubmit={handleSubmit}>
|
<form id="arg-form" onSubmit={handleSubmit}>
|
||||||
<label
|
<label
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
} from 'lib/selections'
|
} from 'lib/selections'
|
||||||
import { useSelector } from '@xstate/react'
|
import { useSelector } from '@xstate/react'
|
||||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||||
|
import { kclManager } from 'lib/singletons'
|
||||||
|
|
||||||
const selectionSelector = (snapshot: any) => snapshot?.context.selectionRanges
|
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() {
|
function handleChange() {
|
||||||
inputRef.current?.focus()
|
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 { getVariableDeclaration } from 'lang/queryAst/getVariableDeclaration'
|
||||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||||
import { getNodeFromPath } from 'lang/queryAst'
|
import { getNodeFromPath } from 'lang/queryAst'
|
||||||
|
import { IS_NIGHTLY_OR_DEBUG } from 'routes/Settings'
|
||||||
|
import { DEV } from 'env'
|
||||||
|
|
||||||
type OutputFormat = Models['OutputFormat3d_type']
|
type OutputFormat = Models['OutputFormat3d_type']
|
||||||
type OutputTypeKey = OutputFormat['type']
|
type OutputTypeKey = OutputFormat['type']
|
||||||
@ -153,6 +155,16 @@ export type ModelingCommandSchema = {
|
|||||||
nodeToEdit?: PathToNode
|
nodeToEdit?: PathToNode
|
||||||
color: string
|
color: string
|
||||||
}
|
}
|
||||||
|
'Boolean Subtract': {
|
||||||
|
target: Selections
|
||||||
|
tool: Selections
|
||||||
|
}
|
||||||
|
'Boolean Union': {
|
||||||
|
solids: Selections
|
||||||
|
}
|
||||||
|
'Boolean Intersect': {
|
||||||
|
solids: Selections
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
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': {
|
'Offset plane': {
|
||||||
description: 'Offset a plane.',
|
description: 'Offset a plane.',
|
||||||
icon: 'plane',
|
icon: 'plane',
|
||||||
@ -940,3 +1013,5 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
modelingMachineCommandConfig
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { CustomIconName } from 'components/CustomIcon'
|
import { CustomIconName } from 'components/CustomIcon'
|
||||||
import { AllMachines } from 'hooks/useStateMachineCommands'
|
import { AllMachines } from 'hooks/useStateMachineCommands'
|
||||||
import { Actor, AnyStateMachine, ContextFrom, EventFrom } from 'xstate'
|
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 { commandBarMachine } from 'machines/commandBarMachine'
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
import { MachineManager } from 'components/MachineManagerProvider'
|
import { MachineManager } from 'components/MachineManagerProvider'
|
||||||
import { Node } from '@rust/kcl-lib/bindings/Node'
|
import { Node } from '@rust/kcl-lib/bindings/Node'
|
||||||
import { Artifact } from 'lang/std/artifactGraph'
|
import { Artifact } from 'lang/std/artifactGraph'
|
||||||
import { CommandBarContext } from 'machines/commandBarMachine'
|
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
|
type Icon = CustomIconName
|
||||||
const _PLATFORMS = ['both', 'web', 'desktop'] as const
|
const _PLATFORMS = ['both', 'web', 'desktop'] as const
|
||||||
@ -160,6 +160,8 @@ export type CommandArgumentConfig<
|
|||||||
| {
|
| {
|
||||||
inputType: 'selection'
|
inputType: 'selection'
|
||||||
selectionTypes: Artifact['type'][]
|
selectionTypes: Artifact['type'][]
|
||||||
|
clearSelectionFirst?: boolean
|
||||||
|
selectionFilter?: EntityType_type[]
|
||||||
multiple: boolean
|
multiple: boolean
|
||||||
validation?: ({
|
validation?: ({
|
||||||
data,
|
data,
|
||||||
@ -172,6 +174,7 @@ export type CommandArgumentConfig<
|
|||||||
| {
|
| {
|
||||||
inputType: 'selectionMixed'
|
inputType: 'selectionMixed'
|
||||||
selectionTypes: Artifact['type'][]
|
selectionTypes: Artifact['type'][]
|
||||||
|
selectionFilter?: EntityType_type[]
|
||||||
multiple: boolean
|
multiple: boolean
|
||||||
allowNoSelection?: boolean
|
allowNoSelection?: boolean
|
||||||
validation?: ({
|
validation?: ({
|
||||||
@ -281,6 +284,8 @@ export type CommandArgument<
|
|||||||
| {
|
| {
|
||||||
inputType: 'selection'
|
inputType: 'selection'
|
||||||
selectionTypes: Artifact['type'][]
|
selectionTypes: Artifact['type'][]
|
||||||
|
clearSelectionFirst?: boolean
|
||||||
|
selectionFilter?: EntityType_type[]
|
||||||
multiple: boolean
|
multiple: boolean
|
||||||
validation?: ({
|
validation?: ({
|
||||||
data,
|
data,
|
||||||
@ -293,6 +298,7 @@ export type CommandArgument<
|
|||||||
| {
|
| {
|
||||||
inputType: 'selectionMixed'
|
inputType: 'selectionMixed'
|
||||||
selectionTypes: Artifact['type'][]
|
selectionTypes: Artifact['type'][]
|
||||||
|
selectionFilter?: EntityType_type[]
|
||||||
multiple: boolean
|
multiple: boolean
|
||||||
allowNoSelection?: boolean
|
allowNoSelection?: boolean
|
||||||
validation?: ({
|
validation?: ({
|
||||||
|
@ -188,6 +188,8 @@ export function buildCommandArgument<
|
|||||||
multiple: arg.multiple,
|
multiple: arg.multiple,
|
||||||
selectionTypes: arg.selectionTypes,
|
selectionTypes: arg.selectionTypes,
|
||||||
validation: arg.validation,
|
validation: arg.validation,
|
||||||
|
clearSelectionFirst: arg.clearSelectionFirst,
|
||||||
|
selectionFilter: arg.selectionFilter,
|
||||||
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
|
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
|
||||||
} else if (arg.inputType === 'selectionMixed') {
|
} else if (arg.inputType === 'selectionMixed') {
|
||||||
return {
|
return {
|
||||||
@ -198,6 +200,7 @@ export function buildCommandArgument<
|
|||||||
validation: arg.validation,
|
validation: arg.validation,
|
||||||
allowNoSelection: arg.allowNoSelection,
|
allowNoSelection: arg.allowNoSelection,
|
||||||
selectionSource: arg.selectionSource,
|
selectionSource: arg.selectionSource,
|
||||||
|
selectionFilter: arg.selectionFilter,
|
||||||
} satisfies CommandArgument<O, T> & { inputType: 'selectionMixed' }
|
} satisfies CommandArgument<O, T> & { inputType: 'selectionMixed' }
|
||||||
} else if (arg.inputType === 'kcl') {
|
} else if (arg.inputType === 'kcl') {
|
||||||
return {
|
return {
|
||||||
|
@ -209,9 +209,13 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
id: 'boolean-union',
|
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',
|
icon: 'booleanUnion',
|
||||||
status: 'unavailable',
|
status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'unavailable',
|
||||||
title: 'Union',
|
title: 'Union',
|
||||||
hotkey: 'Shift + B U',
|
hotkey: 'Shift + B U',
|
||||||
description: 'Combine two or more solids into a single solid.',
|
description: 'Combine two or more solids into a single solid.',
|
||||||
@ -224,9 +228,13 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'boolean-subtract',
|
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',
|
icon: 'booleanSubtract',
|
||||||
status: 'unavailable',
|
status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'unavailable',
|
||||||
title: 'Subtract',
|
title: 'Subtract',
|
||||||
hotkey: 'Shift + B S',
|
hotkey: 'Shift + B S',
|
||||||
description: 'Subtract one solid from another.',
|
description: 'Subtract one solid from another.',
|
||||||
@ -239,9 +247,13 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'boolean-intersect',
|
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',
|
icon: 'booleanIntersect',
|
||||||
status: 'unavailable',
|
status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'unavailable',
|
||||||
title: 'Intersect',
|
title: 'Intersect',
|
||||||
hotkey: 'Shift + B I',
|
hotkey: 'Shift + B I',
|
||||||
description: 'Create a solid from the intersection of two solids.',
|
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