Compare commits
82 Commits
v0.33.0
...
pierremtb/
Author | SHA1 | Date | |
---|---|---|---|
9407162543 | |||
067b83b468 | |||
c5b30341eb | |||
3e6441b563 | |||
acafcf2d4d | |||
b9f31d94d5 | |||
9e03b58ae5 | |||
c591f73c70 | |||
9330aaba13 | |||
5a14f0189e | |||
9af001f22e | |||
435d1ea52e | |||
54847139f2 | |||
9369a17ea7 | |||
9ee2e7c3b0 | |||
28ae261e5f | |||
e4b0de0ead | |||
12859598a3 | |||
e9a334f433 | |||
e470a7b4af | |||
602c39f63c | |||
4994aa6f61 | |||
4aa07b81db | |||
89ef4b3243 | |||
001c9a8219 | |||
4dde3f60e0 | |||
f407c53032 | |||
9ba584487a | |||
96b66d6bca | |||
8d66f3ffad | |||
f4c54cbbe4 | |||
5156b847f3 | |||
ded9f2c56b | |||
5db5f79f9a | |||
6a883f4a8d | |||
07b91f0fb1 | |||
1b2e213afe | |||
f48a23c35e | |||
7e1d102496 | |||
94cb2535c0 | |||
9e08ec9096 | |||
17bd8ec32a | |||
d0f12e85e5 | |||
94d185944e | |||
c38b2270c3 | |||
967ad66c98 | |||
afeca9ca39 | |||
61242282f0 | |||
0065df13ce | |||
01c8d45c13 | |||
8b25527f21 | |||
2abd980de9 | |||
f783deb706 | |||
f4dd295ca1 | |||
ceaa85fe3f | |||
3991bd9173 | |||
b8f9da36c0 | |||
283315b5d2 | |||
e204dfe564 | |||
208a36196b | |||
660a349588 | |||
56c37da317 | |||
a46734b76d | |||
4347e0cf84 | |||
df3e541cdf | |||
b1cec443b9 | |||
967f49055d | |||
fe977524b5 | |||
d6f271fb0f | |||
e851b2bcc4 | |||
be569c91de | |||
5080e304b9 | |||
f4e75b7b4f | |||
31cbc90f56 | |||
a7d3552472 | |||
e984b20664 | |||
0c2cd24bda | |||
9b2de237b8 | |||
c79c02f18e | |||
4851aa2d71 | |||
76fafa6fd0 | |||
647ca11e08 |
@ -7,6 +7,7 @@ export class ToolbarFixture {
|
|||||||
|
|
||||||
extrudeButton!: Locator
|
extrudeButton!: Locator
|
||||||
loftButton!: Locator
|
loftButton!: Locator
|
||||||
|
shellButton!: Locator
|
||||||
offsetPlaneButton!: Locator
|
offsetPlaneButton!: Locator
|
||||||
startSketchBtn!: Locator
|
startSketchBtn!: Locator
|
||||||
lineBtn!: Locator
|
lineBtn!: Locator
|
||||||
@ -28,6 +29,7 @@ export class ToolbarFixture {
|
|||||||
this.page = page
|
this.page = page
|
||||||
this.extrudeButton = page.getByTestId('extrude')
|
this.extrudeButton = page.getByTestId('extrude')
|
||||||
this.loftButton = page.getByTestId('loft')
|
this.loftButton = page.getByTestId('loft')
|
||||||
|
this.shellButton = page.getByTestId('shell')
|
||||||
this.offsetPlaneButton = page.getByTestId('plane-offset')
|
this.offsetPlaneButton = page.getByTestId('plane-offset')
|
||||||
this.startSketchBtn = page.getByTestId('sketch')
|
this.startSketchBtn = page.getByTestId('sketch')
|
||||||
this.lineBtn = page.getByTestId('line')
|
this.lineBtn = page.getByTestId('line')
|
||||||
|
@ -768,3 +768,166 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const shellPointAndClickCapCases = [
|
||||||
|
{ shouldPreselect: true },
|
||||||
|
{ shouldPreselect: false },
|
||||||
|
]
|
||||||
|
shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
|
||||||
|
test(`Shell point-and-click cap (preselected sketches: ${shouldPreselect})`, async ({
|
||||||
|
app,
|
||||||
|
scene,
|
||||||
|
editor,
|
||||||
|
toolbar,
|
||||||
|
cmdBar,
|
||||||
|
}) => {
|
||||||
|
const initialCode = `sketch001 = startSketchOn('XZ')
|
||||||
|
|> circle({ center = [0, 0], radius = 30 }, %)
|
||||||
|
extrude001 = extrude(30, sketch001)
|
||||||
|
`
|
||||||
|
await app.initialise(initialCode)
|
||||||
|
|
||||||
|
// One dumb hardcoded screen pixel value
|
||||||
|
const testPoint = { x: 575, y: 200 }
|
||||||
|
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||||
|
const shellDeclaration =
|
||||||
|
"shell001 = shell({ faces = ['end'], thickness = 5 }, extrude001)"
|
||||||
|
|
||||||
|
await test.step(`Look for the grey of the shape`, async () => {
|
||||||
|
await scene.expectPixelColor([127, 127, 127], testPoint, 15)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!shouldPreselect) {
|
||||||
|
await test.step(`Go through the command bar flow without preselected faces`, async () => {
|
||||||
|
await toolbar.shellButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'selection',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '',
|
||||||
|
Thickness: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'selection',
|
||||||
|
commandName: 'Shell',
|
||||||
|
})
|
||||||
|
await clickOnCap()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '1 cap',
|
||||||
|
Thickness: '5',
|
||||||
|
},
|
||||||
|
commandName: 'Shell',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await test.step(`Preselect the cap`, async () => {
|
||||||
|
await clickOnCap()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => {
|
||||||
|
await toolbar.shellButton.click()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '1 cap',
|
||||||
|
Thickness: '5',
|
||||||
|
},
|
||||||
|
commandName: 'Shell',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
||||||
|
await editor.expectEditor.toContain(shellDeclaration)
|
||||||
|
await editor.expectState({
|
||||||
|
diagnostics: [],
|
||||||
|
activeLines: [shellDeclaration],
|
||||||
|
highlightedCode: '',
|
||||||
|
})
|
||||||
|
await scene.expectPixelColor([146, 146, 146], testPoint, 15)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Shell point-and-click wall', async ({
|
||||||
|
app,
|
||||||
|
page,
|
||||||
|
scene,
|
||||||
|
editor,
|
||||||
|
toolbar,
|
||||||
|
cmdBar,
|
||||||
|
}) => {
|
||||||
|
const initialCode = `sketch001 = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-20, 20], %)
|
||||||
|
|> xLine(40, %)
|
||||||
|
|> yLine(-60, %)
|
||||||
|
|> xLine(-40, %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)
|
||||||
|
extrude001 = extrude(40, sketch001)
|
||||||
|
`
|
||||||
|
await app.initialise(initialCode)
|
||||||
|
|
||||||
|
// One dumb hardcoded screen pixel value
|
||||||
|
const testPoint = { x: 580, y: 180 }
|
||||||
|
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||||
|
const [clickOnWall] = scene.makeMouseHelpers(testPoint.x, testPoint.y + 70)
|
||||||
|
const mutatedCode = 'xLine(-40, %, $seg01)'
|
||||||
|
const shellDeclaration =
|
||||||
|
"shell001 = shell({ faces = ['end', seg01], thickness = 5}, extrude001)"
|
||||||
|
const formattedOutLastLine = '}, extrude001)'
|
||||||
|
|
||||||
|
await test.step(`Look for the grey of the shape`, async () => {
|
||||||
|
await scene.expectPixelColor([99, 99, 99], testPoint, 15)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Go through the command bar flow, selecting a wall and keeping default thickness`, async () => {
|
||||||
|
await toolbar.shellButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'selection',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '',
|
||||||
|
Thickness: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'selection',
|
||||||
|
commandName: 'Shell',
|
||||||
|
})
|
||||||
|
await clickOnCap()
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
await clickOnWall()
|
||||||
|
await app.page.waitForTimeout(500)
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '1 cap, 1 face',
|
||||||
|
Thickness: '5',
|
||||||
|
},
|
||||||
|
commandName: 'Shell',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
||||||
|
await editor.expectEditor.toContain(mutatedCode)
|
||||||
|
await editor.expectEditor.toContain(shellDeclaration)
|
||||||
|
await editor.expectState({
|
||||||
|
diagnostics: [],
|
||||||
|
activeLines: [formattedOutLastLine],
|
||||||
|
highlightedCode: '',
|
||||||
|
})
|
||||||
|
await scene.expectPixelColor([49, 49, 49], testPoint, 15)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
@ -51,6 +51,7 @@ import {
|
|||||||
Selections,
|
Selections,
|
||||||
updateSelections,
|
updateSelections,
|
||||||
canLoftSelection,
|
canLoftSelection,
|
||||||
|
canShellSelection,
|
||||||
} from 'lib/selections'
|
} from 'lib/selections'
|
||||||
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
||||||
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
||||||
@ -69,6 +70,7 @@ import {
|
|||||||
} from 'lang/modifyAst'
|
} from 'lang/modifyAst'
|
||||||
import { Program, parse, recast, resultIsOk } from 'lang/wasm'
|
import { Program, parse, recast, resultIsOk } from 'lang/wasm'
|
||||||
import {
|
import {
|
||||||
|
doesSceneHaveExtrudedSketch,
|
||||||
doesSceneHaveSweepableSketch,
|
doesSceneHaveSweepableSketch,
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
isSingleCursorInPipe,
|
isSingleCursorInPipe,
|
||||||
@ -585,6 +587,24 @@ export const ModelingMachineProvider = ({
|
|||||||
if (err(canLoft)) return false
|
if (err(canLoft)) return false
|
||||||
return canLoft
|
return canLoft
|
||||||
},
|
},
|
||||||
|
'has valid shell selection': ({
|
||||||
|
context: { selectionRanges },
|
||||||
|
event,
|
||||||
|
}) => {
|
||||||
|
const hasNoSelection =
|
||||||
|
selectionRanges.graphSelections.length === 0 ||
|
||||||
|
isRangeBetweenCharacters(selectionRanges) ||
|
||||||
|
isSelectionLastLine(selectionRanges, codeManager.code)
|
||||||
|
|
||||||
|
if (hasNoSelection) {
|
||||||
|
return doesSceneHaveExtrudedSketch(kclManager.ast)
|
||||||
|
}
|
||||||
|
|
||||||
|
const canShell = canShellSelection(selectionRanges)
|
||||||
|
console.log('canShellSelection', canShellSelection(selectionRanges))
|
||||||
|
if (err(canShell)) return false
|
||||||
|
return canShell
|
||||||
|
},
|
||||||
'has valid selection for deletion': ({
|
'has valid selection for deletion': ({
|
||||||
context: { selectionRanges },
|
context: { selectionRanges },
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -22,7 +22,7 @@ import {
|
|||||||
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
|
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
|
||||||
import { createLiteral } from 'lang/modifyAst'
|
import { createLiteral } from 'lang/modifyAst'
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selection, Selections } from 'lib/selections'
|
||||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||||
import { VITE_KC_DEV_TOKEN } from 'env'
|
import { VITE_KC_DEV_TOKEN } from 'env'
|
||||||
import { isOverlap } from 'lib/utils'
|
import { isOverlap } from 'lib/utils'
|
||||||
@ -118,13 +118,8 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
|
|||||||
code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length,
|
code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length,
|
||||||
true,
|
true,
|
||||||
]
|
]
|
||||||
const selection: Selections = {
|
const selection: Selection = {
|
||||||
graphSelections: [
|
|
||||||
{
|
|
||||||
codeRef: codeRefFromRange(segmentRange, ast),
|
codeRef: codeRefFromRange(segmentRange, ast),
|
||||||
},
|
|
||||||
],
|
|
||||||
otherSelections: [],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// executeAst and artifactGraph
|
// executeAst and artifactGraph
|
||||||
|
@ -29,7 +29,7 @@ import {
|
|||||||
sketchLineHelperMap,
|
sketchLineHelperMap,
|
||||||
} from '../std/sketch'
|
} from '../std/sketch'
|
||||||
import { err, trap } from 'lib/trap'
|
import { err, trap } from 'lib/trap'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selection, Selections } from 'lib/selections'
|
||||||
import { KclCommandValue } from 'lib/commandTypes'
|
import { KclCommandValue } from 'lib/commandTypes'
|
||||||
import {
|
import {
|
||||||
Artifact,
|
Artifact,
|
||||||
@ -99,14 +99,9 @@ export function modifyAstWithEdgeTreatmentAndTag(
|
|||||||
const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison
|
const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison
|
||||||
|
|
||||||
for (const selection of selections.graphSelections) {
|
for (const selection of selections.graphSelections) {
|
||||||
const singleSelection = {
|
|
||||||
graphSelections: [selection],
|
|
||||||
otherSelections: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = getPathToExtrudeForSegmentSelection(
|
const result = getPathToExtrudeForSegmentSelection(
|
||||||
clonedAstForGetExtrude,
|
clonedAstForGetExtrude,
|
||||||
singleSelection,
|
selection,
|
||||||
artifactGraph
|
artifactGraph
|
||||||
)
|
)
|
||||||
if (err(result)) return result
|
if (err(result)) return result
|
||||||
@ -259,12 +254,12 @@ function insertParametersIntoAst(
|
|||||||
|
|
||||||
export function getPathToExtrudeForSegmentSelection(
|
export function getPathToExtrudeForSegmentSelection(
|
||||||
ast: Program,
|
ast: Program,
|
||||||
selection: Selections,
|
selection: Selection,
|
||||||
artifactGraph: ArtifactGraph
|
artifactGraph: ArtifactGraph
|
||||||
): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error {
|
): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error {
|
||||||
const pathToSegmentNode = getNodePathFromSourceRange(
|
const pathToSegmentNode = getNodePathFromSourceRange(
|
||||||
ast,
|
ast,
|
||||||
selection.graphSelections[0]?.codeRef?.range
|
selection.codeRef?.range
|
||||||
)
|
)
|
||||||
|
|
||||||
const varDecNode = getNodeFromPath<VariableDeclaration>(
|
const varDecNode = getNodeFromPath<VariableDeclaration>(
|
||||||
@ -308,7 +303,7 @@ async function updateAstAndFocus(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mutateAstWithTagForSketchSegment(
|
export function mutateAstWithTagForSketchSegment(
|
||||||
astClone: Node<Program>,
|
astClone: Node<Program>,
|
||||||
pathToSegmentNode: PathToNode
|
pathToSegmentNode: PathToNode
|
||||||
): { modifiedAst: Program; tag: string } | Error {
|
): { modifiedAst: Program; tag: string } | Error {
|
||||||
|
124
src/lang/modifyAst/addShell.ts
Normal file
124
src/lang/modifyAst/addShell.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import { ArtifactGraph } from 'lang/std/artifactGraph'
|
||||||
|
import { Selections } from 'lib/selections'
|
||||||
|
import { Expr } from 'wasm-lib/kcl/bindings/Expr'
|
||||||
|
import { Program } from 'wasm-lib/kcl/bindings/Program'
|
||||||
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
|
import { PathToNode, VariableDeclarator } from 'lang/wasm'
|
||||||
|
import {
|
||||||
|
getPathToExtrudeForSegmentSelection,
|
||||||
|
mutateAstWithTagForSketchSegment,
|
||||||
|
} from './addEdgeTreatment'
|
||||||
|
import { getNodeFromPath } from 'lang/queryAst'
|
||||||
|
import { err } from 'lib/trap'
|
||||||
|
import {
|
||||||
|
createLiteral,
|
||||||
|
createIdentifier,
|
||||||
|
findUniqueName,
|
||||||
|
createCallExpressionStdLib,
|
||||||
|
createObjectExpression,
|
||||||
|
createArrayExpression,
|
||||||
|
createVariableDeclaration,
|
||||||
|
} from 'lang/modifyAst'
|
||||||
|
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
|
||||||
|
|
||||||
|
export function addShell({
|
||||||
|
node,
|
||||||
|
selection,
|
||||||
|
artifactGraph,
|
||||||
|
thickness,
|
||||||
|
}: {
|
||||||
|
node: Node<Program>
|
||||||
|
selection: Selections
|
||||||
|
artifactGraph: ArtifactGraph
|
||||||
|
thickness: Expr
|
||||||
|
}): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } {
|
||||||
|
const modifiedAst = structuredClone(node)
|
||||||
|
|
||||||
|
// Look up the corresponding extrude
|
||||||
|
const clonedAstForGetExtrude = structuredClone(modifiedAst)
|
||||||
|
|
||||||
|
const expressions: Expr[] = []
|
||||||
|
let pathToExtrudeNode: PathToNode | undefined = undefined
|
||||||
|
for (const graphSelection of selection.graphSelections) {
|
||||||
|
const extrudeLookupResult = getPathToExtrudeForSegmentSelection(
|
||||||
|
clonedAstForGetExtrude,
|
||||||
|
graphSelection,
|
||||||
|
artifactGraph
|
||||||
|
)
|
||||||
|
if (err(extrudeLookupResult)) {
|
||||||
|
return new Error("Couldn't find extrude")
|
||||||
|
}
|
||||||
|
|
||||||
|
pathToExtrudeNode = extrudeLookupResult.pathToExtrudeNode
|
||||||
|
// Get the sketch ref from the selection
|
||||||
|
// TODO: this assumes the segment is piped directly from the sketch, with no intermediate `VariableDeclarator` between.
|
||||||
|
// We must find a technique for these situations that is robust to intermediate declarations
|
||||||
|
const sketchNode = getNodeFromPath<VariableDeclarator>(
|
||||||
|
modifiedAst,
|
||||||
|
graphSelection.codeRef.pathToNode,
|
||||||
|
'VariableDeclarator'
|
||||||
|
)
|
||||||
|
if (err(sketchNode)) {
|
||||||
|
return sketchNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedArtifact = graphSelection.artifact
|
||||||
|
if (!selectedArtifact) {
|
||||||
|
return new Error('Bad artifact')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check on the selection, and handle the wall vs cap casees
|
||||||
|
let expr: Expr
|
||||||
|
if (selectedArtifact.type === 'cap') {
|
||||||
|
expr = createLiteral(selectedArtifact.subType)
|
||||||
|
} else if (selectedArtifact.type === 'wall') {
|
||||||
|
const tagResult = mutateAstWithTagForSketchSegment(
|
||||||
|
modifiedAst,
|
||||||
|
extrudeLookupResult.pathToSegmentNode
|
||||||
|
)
|
||||||
|
if (err(tagResult)) return tagResult
|
||||||
|
const { tag } = tagResult
|
||||||
|
expr = createIdentifier(tag)
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
expressions.push(expr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pathToExtrudeNode) return new Error('No extrude found')
|
||||||
|
|
||||||
|
const extrudeNode = getNodeFromPath<VariableDeclarator>(
|
||||||
|
modifiedAst,
|
||||||
|
pathToExtrudeNode,
|
||||||
|
'VariableDeclarator'
|
||||||
|
)
|
||||||
|
if (err(extrudeNode)) {
|
||||||
|
return extrudeNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SHELL)
|
||||||
|
const shell = createCallExpressionStdLib('shell', [
|
||||||
|
createObjectExpression({
|
||||||
|
faces: createArrayExpression(expressions),
|
||||||
|
thickness,
|
||||||
|
}),
|
||||||
|
createIdentifier(extrudeNode.node.id.name),
|
||||||
|
])
|
||||||
|
const declaration = createVariableDeclaration(name, shell)
|
||||||
|
|
||||||
|
// TODO: check if we should append at the end like here or right after the extrude
|
||||||
|
modifiedAst.body.push(declaration)
|
||||||
|
const pathToNode: PathToNode = [
|
||||||
|
['body', ''],
|
||||||
|
[modifiedAst.body.length - 1, 'index'],
|
||||||
|
['declarations', 'VariableDeclaration'],
|
||||||
|
['0', 'index'],
|
||||||
|
['init', 'VariableDeclarator'],
|
||||||
|
['arguments', 'CallExpression'],
|
||||||
|
[0, 'index'],
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
modifiedAst,
|
||||||
|
pathToNode,
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,7 @@ import {
|
|||||||
doesSceneHaveSweepableSketch,
|
doesSceneHaveSweepableSketch,
|
||||||
traverse,
|
traverse,
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
|
doesSceneHaveExtrudedSketch,
|
||||||
} from './queryAst'
|
} from './queryAst'
|
||||||
import { enginelessExecutor } from '../lib/testHelpers'
|
import { enginelessExecutor } from '../lib/testHelpers'
|
||||||
import {
|
import {
|
||||||
@ -654,6 +655,38 @@ extrude001 = extrude(10, sketch001)
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Testing doesSceneHaveExtrudedSketch', () => {
|
||||||
|
it('finds extruded sketch as variable', async () => {
|
||||||
|
const exampleCode = `sketch001 = startSketchOn('XZ')
|
||||||
|
|> circle({ center = [0, 0], radius = 1 }, %)
|
||||||
|
extrude001 = extrude(1, sketch001)
|
||||||
|
`
|
||||||
|
const ast = parse(exampleCode)
|
||||||
|
if (err(ast)) throw ast
|
||||||
|
const extrudable = doesSceneHaveExtrudedSketch(ast)
|
||||||
|
expect(extrudable).toBeTruthy()
|
||||||
|
})
|
||||||
|
it('finds extruded sketch in pipe', async () => {
|
||||||
|
const exampleCode = `extrude001 = startSketchOn('XZ')
|
||||||
|
|> circle({ center = [0, 0], radius = 1 }, %)
|
||||||
|
|> extrude(1, %)
|
||||||
|
`
|
||||||
|
const ast = parse(exampleCode)
|
||||||
|
if (err(ast)) throw ast
|
||||||
|
const extrudable = doesSceneHaveExtrudedSketch(ast)
|
||||||
|
expect(extrudable).toBeTruthy()
|
||||||
|
})
|
||||||
|
it('finds no extrusion with sketch only', async () => {
|
||||||
|
const exampleCode = `extrude001 = startSketchOn('XZ')
|
||||||
|
|> circle({ center = [0, 0], radius = 1 }, %)
|
||||||
|
`
|
||||||
|
const ast = parse(exampleCode)
|
||||||
|
if (err(ast)) throw ast
|
||||||
|
const extrudable = doesSceneHaveExtrudedSketch(ast)
|
||||||
|
expect(extrudable).toBeFalsy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('Testing traverse and pathToNode', () => {
|
describe('Testing traverse and pathToNode', () => {
|
||||||
it.each([
|
it.each([
|
||||||
['basic', '2.73'],
|
['basic', '2.73'],
|
||||||
|
@ -1064,6 +1064,35 @@ export function doesSceneHaveSweepableSketch(ast: Node<Program>, count = 1) {
|
|||||||
return Object.keys(theMap).length >= count
|
return Object.keys(theMap).length >= count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function doesSceneHaveExtrudedSketch(ast: Node<Program>) {
|
||||||
|
const theMap: any = {}
|
||||||
|
traverse(ast as any, {
|
||||||
|
enter(node) {
|
||||||
|
if (
|
||||||
|
node.type === 'VariableDeclarator' &&
|
||||||
|
node.init?.type === 'PipeExpression'
|
||||||
|
) {
|
||||||
|
for (const pipe of node.init.body) {
|
||||||
|
if (
|
||||||
|
pipe.type === 'CallExpression' &&
|
||||||
|
pipe.callee.name === 'extrude'
|
||||||
|
) {
|
||||||
|
theMap[node.id.name] = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
node.type === 'CallExpression' &&
|
||||||
|
node.callee.name === 'extrude' &&
|
||||||
|
node.arguments[1]?.type === 'Identifier'
|
||||||
|
) {
|
||||||
|
theMap[node.moduleId] = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return Object.keys(theMap).length > 0
|
||||||
|
}
|
||||||
|
|
||||||
export function getObjExprProperty(
|
export function getObjExprProperty(
|
||||||
node: ObjectExpression,
|
node: ObjectExpression,
|
||||||
propName: string
|
propName: string
|
||||||
|
@ -34,6 +34,10 @@ export type ModelingCommandSchema = {
|
|||||||
Loft: {
|
Loft: {
|
||||||
selection: Selections
|
selection: Selections
|
||||||
}
|
}
|
||||||
|
Shell: {
|
||||||
|
selection: Selections
|
||||||
|
thickness: KclCommandValue
|
||||||
|
}
|
||||||
Revolve: {
|
Revolve: {
|
||||||
selection: Selections
|
selection: Selections
|
||||||
angle: KclCommandValue
|
angle: KclCommandValue
|
||||||
@ -277,6 +281,25 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Shell: {
|
||||||
|
description: 'Hollow out a 3D solid.',
|
||||||
|
icon: 'shell',
|
||||||
|
needsReview: true,
|
||||||
|
args: {
|
||||||
|
selection: {
|
||||||
|
inputType: 'selection',
|
||||||
|
selectionTypes: ['cap', 'wall'],
|
||||||
|
multiple: true,
|
||||||
|
required: true,
|
||||||
|
skip: false,
|
||||||
|
},
|
||||||
|
thickness: {
|
||||||
|
inputType: 'kcl',
|
||||||
|
defaultValue: KCL_DEFAULT_LENGTH,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
// TODO: Update this configuration, copied from extrude for MVP of revolve, specifically the args.selection
|
// TODO: Update this configuration, copied from extrude for MVP of revolve, specifically the args.selection
|
||||||
Revolve: {
|
Revolve: {
|
||||||
description: 'Create a 3D body by rotating a sketch region about an axis.',
|
description: 'Create a 3D body by rotating a sketch region about an axis.',
|
||||||
|
@ -53,6 +53,7 @@ export const KCL_DEFAULT_CONSTANT_PREFIXES = {
|
|||||||
SKETCH: 'sketch',
|
SKETCH: 'sketch',
|
||||||
EXTRUDE: 'extrude',
|
EXTRUDE: 'extrude',
|
||||||
LOFT: 'loft',
|
LOFT: 'loft',
|
||||||
|
SHELL: 'shell',
|
||||||
SEGMENT: 'seg',
|
SEGMENT: 'seg',
|
||||||
REVOLVE: 'revolve',
|
REVOLVE: 'revolve',
|
||||||
PLANE: 'plane',
|
PLANE: 'plane',
|
||||||
|
@ -585,6 +585,17 @@ export function canLoftSelection(selection: Selections) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canShellSelection(selection: Selections) {
|
||||||
|
const commonNodes = selection.graphSelections.map((_, i) =>
|
||||||
|
buildCommonNodeFromSelection(selection, i)
|
||||||
|
)
|
||||||
|
return commonNodes.every(
|
||||||
|
(n) =>
|
||||||
|
n.selection.artifact?.type === 'cap' ||
|
||||||
|
n.selection.artifact?.type === 'wall'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// This accounts for non-geometry selections under "other"
|
// This accounts for non-geometry selections under "other"
|
||||||
export type ResolvedSelectionType = Artifact['type'] | 'other'
|
export type ResolvedSelectionType = Artifact['type'] | 'other'
|
||||||
export type SelectionCountsByType = Map<ResolvedSelectionType, number>
|
export type SelectionCountsByType = Map<ResolvedSelectionType, number>
|
||||||
|
@ -190,9 +190,15 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'shell',
|
id: 'shell',
|
||||||
onClick: () => console.error('Shell not yet implemented'),
|
onClick: ({ commandBarSend }) => {
|
||||||
|
commandBarSend({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: { name: 'Shell', groupId: 'modeling' },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
disabled: (state) => !state.can({ type: 'Shell' }),
|
||||||
icon: 'shell',
|
icon: 'shell',
|
||||||
status: 'kcl-only',
|
status: 'available',
|
||||||
title: 'Shell',
|
title: 'Shell',
|
||||||
description: 'Hollow out a 3D solid.',
|
description: 'Hollow out a 3D solid.',
|
||||||
links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/shell' }],
|
links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/shell' }],
|
||||||
|
@ -80,6 +80,7 @@ import { ToolbarModeName } from 'lib/toolbar'
|
|||||||
import { quaternionFromUpNForward } from 'clientSideScene/helpers'
|
import { quaternionFromUpNForward } from 'clientSideScene/helpers'
|
||||||
import { Vector3 } from 'three'
|
import { Vector3 } from 'three'
|
||||||
import { MachineManager } from 'components/MachineManagerProvider'
|
import { MachineManager } from 'components/MachineManagerProvider'
|
||||||
|
import { addShell } from 'lang/modifyAst/addShell'
|
||||||
|
|
||||||
export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY'
|
export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY'
|
||||||
|
|
||||||
@ -260,6 +261,7 @@ export type ModelingMachineEvent =
|
|||||||
| { type: 'Make'; data: ModelingCommandSchema['Make'] }
|
| { type: 'Make'; data: ModelingCommandSchema['Make'] }
|
||||||
| { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] }
|
| { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] }
|
||||||
| { type: 'Loft'; data?: ModelingCommandSchema['Loft'] }
|
| { type: 'Loft'; data?: ModelingCommandSchema['Loft'] }
|
||||||
|
| { type: 'Shell'; data?: ModelingCommandSchema['Shell'] }
|
||||||
| { type: 'Revolve'; data?: ModelingCommandSchema['Revolve'] }
|
| { type: 'Revolve'; data?: ModelingCommandSchema['Revolve'] }
|
||||||
| { type: 'Fillet'; data?: ModelingCommandSchema['Fillet'] }
|
| { type: 'Fillet'; data?: ModelingCommandSchema['Fillet'] }
|
||||||
| { type: 'Offset plane'; data: ModelingCommandSchema['Offset plane'] }
|
| { type: 'Offset plane'; data: ModelingCommandSchema['Offset plane'] }
|
||||||
@ -392,6 +394,7 @@ export const modelingMachine = setup({
|
|||||||
'Selection is on face': () => false,
|
'Selection is on face': () => false,
|
||||||
'has valid sweep selection': () => false,
|
'has valid sweep selection': () => false,
|
||||||
'has valid loft selection': () => false,
|
'has valid loft selection': () => false,
|
||||||
|
'has valid shell selection': () => false,
|
||||||
'has valid edge treatment selection': () => false,
|
'has valid edge treatment selection': () => false,
|
||||||
'Has exportable geometry': () => false,
|
'Has exportable geometry': () => false,
|
||||||
'has valid selection for deletion': () => false,
|
'has valid selection for deletion': () => false,
|
||||||
@ -1584,6 +1587,66 @@ export const modelingMachine = setup({
|
|||||||
updateAstResult.newAst
|
updateAstResult.newAst
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (updateAstResult?.selections) {
|
||||||
|
editorManager.selectRange(updateAstResult?.selections)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
shellAstMod: fromPromise(
|
||||||
|
async ({
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
input: ModelingCommandSchema['Shell'] | undefined
|
||||||
|
}) => {
|
||||||
|
if (!input) {
|
||||||
|
return new Error('No input provided')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract inputs
|
||||||
|
const ast = kclManager.ast
|
||||||
|
const { selection, thickness } = input
|
||||||
|
|
||||||
|
// Insert the thickness variable if it exists
|
||||||
|
if (
|
||||||
|
'variableName' in thickness &&
|
||||||
|
thickness.variableName &&
|
||||||
|
thickness.insertIndex !== undefined
|
||||||
|
) {
|
||||||
|
const newBody = [...ast.body]
|
||||||
|
newBody.splice(
|
||||||
|
thickness.insertIndex,
|
||||||
|
0,
|
||||||
|
thickness.variableDeclarationAst
|
||||||
|
)
|
||||||
|
ast.body = newBody
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the shell op
|
||||||
|
const shellResult = addShell({
|
||||||
|
node: ast,
|
||||||
|
selection,
|
||||||
|
artifactGraph: engineCommandManager.artifactGraph,
|
||||||
|
thickness:
|
||||||
|
'variableName' in thickness
|
||||||
|
? thickness.variableIdentifierAst
|
||||||
|
: thickness.valueAst,
|
||||||
|
})
|
||||||
|
if (err(shellResult)) {
|
||||||
|
return err(shellResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateAstResult = await kclManager.updateAst(
|
||||||
|
shellResult.modifiedAst,
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
focusPath: [shellResult.pathToNode],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||||
|
updateAstResult.newAst
|
||||||
|
)
|
||||||
|
|
||||||
if (updateAstResult?.selections) {
|
if (updateAstResult?.selections) {
|
||||||
editorManager.selectRange(updateAstResult?.selections)
|
editorManager.selectRange(updateAstResult?.selections)
|
||||||
}
|
}
|
||||||
@ -1627,6 +1690,13 @@ export const modelingMachine = setup({
|
|||||||
|
|
||||||
Loft: {
|
Loft: {
|
||||||
target: 'Applying loft',
|
target: 'Applying loft',
|
||||||
|
guard: 'has valid loft selection',
|
||||||
|
reenter: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
Shell: {
|
||||||
|
target: 'Applying shell',
|
||||||
|
guard: 'has valid shell selection',
|
||||||
reenter: true,
|
reenter: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -2391,6 +2461,19 @@ export const modelingMachine = setup({
|
|||||||
onError: ['idle'],
|
onError: ['idle'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'Applying shell': {
|
||||||
|
invoke: {
|
||||||
|
src: 'shellAstMod',
|
||||||
|
id: 'shellAstMod',
|
||||||
|
input: ({ event }) => {
|
||||||
|
if (event.type !== 'Shell') return undefined
|
||||||
|
return event.data
|
||||||
|
},
|
||||||
|
onDone: ['idle'],
|
||||||
|
onError: ['idle'],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
initial: 'idle',
|
initial: 'idle',
|
||||||
|
Reference in New Issue
Block a user