Add modelingWorkflows module with resilient update pattern for adding features (#5821)
* rule them all * swap AST updater * add test * yellow colour fix * A snapshot a day keeps the bugs away! 📷🐛 * A snapshot a day keeps the bugs away! 📷🐛 * A snapshot a day keeps the bugs away! 📷🐛 * A snapshot a day keeps the bugs away! 📷🐛 --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
@ -1878,6 +1878,119 @@ fillet04 = fillet(extrude001, radius = 5, tags = [getOppositeEdge(seg02)])
|
||||
})
|
||||
})
|
||||
|
||||
test(`Fillet with large radius should update code even if engine fails`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
// Create a cube with small edges that will cause some fillets to fail
|
||||
const initialCode = `sketch001 = startSketchOn('XY')
|
||||
profile001 = startProfileAt([0, 0], sketch001)
|
||||
|> yLine(length = -1)
|
||||
|> xLine(length = -10)
|
||||
|> yLine(length = 10)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(profile001, length = 5)
|
||||
`
|
||||
const taggedSegment = `yLine(length = -1, tag = $seg01)`
|
||||
const filletExpression = `fillet(radius = 1000, tags = [getNextAdjacentEdge(seg01)])`
|
||||
|
||||
// Locators
|
||||
const edgeLocation = { x: 659, y: 313 }
|
||||
const bodyLocation = { x: 594, y: 313 }
|
||||
|
||||
// Colors
|
||||
const edgeColorWhite: [number, number, number] = [248, 248, 248]
|
||||
const edgeColorYellow: [number, number, number] = [251, 251, 120] // Mac:B=251,251,90 Ubuntu:240,241,180, Windows:240,241,180
|
||||
const backgroundColor: [number, number, number] = [30, 30, 30]
|
||||
const bodyColor: [number, number, number] = [155, 155, 155]
|
||||
const lowTolerance = 20
|
||||
const highTolerance = 70
|
||||
|
||||
// Setup
|
||||
await test.step(`Initial test setup`, async () => {
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
|
||||
// verify modeling scene is loaded
|
||||
await scene.expectPixelColor(backgroundColor, edgeLocation, lowTolerance)
|
||||
|
||||
// wait for stream to load
|
||||
await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance)
|
||||
})
|
||||
|
||||
// Test
|
||||
await test.step('Select edges and apply oversized fillet', async () => {
|
||||
await test.step(`Select the edge`, async () => {
|
||||
await scene.expectPixelColor(edgeColorWhite, edgeLocation, lowTolerance)
|
||||
const [clickOnTheEdge] = scene.makeMouseHelpers(
|
||||
edgeLocation.x,
|
||||
edgeLocation.y
|
||||
)
|
||||
await clickOnTheEdge()
|
||||
await scene.expectPixelColor(
|
||||
edgeColorYellow,
|
||||
edgeLocation,
|
||||
highTolerance // Ubuntu color mismatch can require high tolerance
|
||||
)
|
||||
})
|
||||
|
||||
await test.step(`Apply fillet`, async () => {
|
||||
await page.waitForTimeout(100)
|
||||
await toolbar.filletButton.click()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Fillet',
|
||||
highlightedHeaderArg: 'selection',
|
||||
currentArgKey: 'selection',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Selection: '',
|
||||
Radius: '',
|
||||
},
|
||||
stage: 'arguments',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Fillet',
|
||||
highlightedHeaderArg: 'radius',
|
||||
currentArgKey: 'radius',
|
||||
currentArgValue: '5',
|
||||
headerArguments: {
|
||||
Selection: '1 sweepEdge',
|
||||
Radius: '',
|
||||
},
|
||||
stage: 'arguments',
|
||||
})
|
||||
// Set a large radius (1000)
|
||||
await cmdBar.currentArgumentInput.locator('.cm-content').fill('1000')
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Fillet',
|
||||
headerArguments: {
|
||||
Selection: '1 sweepEdge',
|
||||
Radius: '1000',
|
||||
},
|
||||
stage: 'review',
|
||||
})
|
||||
// Apply fillet with large radius
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
})
|
||||
|
||||
await test.step('Verify code is updated regardless of execution errors', async () => {
|
||||
await editor.expectEditor.toContain(taggedSegment)
|
||||
await editor.expectEditor.toContain(filletExpression)
|
||||
})
|
||||
})
|
||||
|
||||
test(`Chamfer point-and-click`, async ({
|
||||
context,
|
||||
page,
|
||||
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
83
src/lang/modelingWorkflows.ts
Normal file
@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Modeling Workflows
|
||||
*
|
||||
* This module contains higher-level CAD operation workflows that
|
||||
* coordinate between different subsystems in the modeling app:
|
||||
* AST, code editor, file system and 3D engine.
|
||||
*/
|
||||
|
||||
import { Node } from '@rust/kcl-lib/bindings/Node'
|
||||
import { KclManager } from 'lang/KclSingleton'
|
||||
import { PathToNode, Program, SourceRange } from 'lang/wasm'
|
||||
import EditorManager from 'editor/manager'
|
||||
import CodeManager from 'lang/codeManager'
|
||||
|
||||
/**
|
||||
* Updates the complete modeling state:
|
||||
* AST, code editor, file, and 3D scene.
|
||||
*
|
||||
* Steps:
|
||||
* 1. Updates the AST and internal state
|
||||
* 2. Updates the code editor and writes to file
|
||||
* 3. Sets focus in the editor if needed
|
||||
* 4. Attempts to execute the model in the 3D engine
|
||||
*
|
||||
* This function follows common CAD application patterns where:
|
||||
*
|
||||
* - The feature tree reflects user's actions
|
||||
* - The engine does its best to visualize what's possible
|
||||
* - Invalid operations appear in feature tree but may not render fully
|
||||
*
|
||||
* This ensures the user can edit the feature tree,
|
||||
* regardless of geometric validity issues.
|
||||
*
|
||||
* @param ast - AST to commit
|
||||
* @param dependencies - Required system components
|
||||
* @param options - Optional parameters for focus, zoom, etc.
|
||||
*/
|
||||
|
||||
export async function updateModelingState(
|
||||
ast: Node<Program>,
|
||||
dependencies: {
|
||||
kclManager: KclManager
|
||||
editorManager: EditorManager
|
||||
codeManager: CodeManager
|
||||
},
|
||||
options?: {
|
||||
focusPath?: Array<PathToNode>
|
||||
zoomToFit?: boolean
|
||||
zoomOnRangeAndType?: {
|
||||
range: SourceRange
|
||||
type: string
|
||||
}
|
||||
}
|
||||
): Promise<void> {
|
||||
// Step 1: Update AST without executing (prepare selections)
|
||||
const updatedAst = await dependencies.kclManager.updateAst(
|
||||
ast,
|
||||
false, // Execution handled separately for error resilience
|
||||
options
|
||||
)
|
||||
|
||||
// Step 2: Update the code editor and save file
|
||||
await dependencies.codeManager.updateEditorWithAstAndWriteToFile(
|
||||
updatedAst.newAst
|
||||
)
|
||||
|
||||
// Step 3: Set focus on the newly added code if needed
|
||||
if (updatedAst.selections) {
|
||||
dependencies.editorManager.selectRange(updatedAst.selections)
|
||||
}
|
||||
|
||||
// Step 4: Try to execute the new code in the engine
|
||||
// and continue regardless of errors
|
||||
try {
|
||||
await dependencies.kclManager.executeAst({
|
||||
ast: updatedAst.newAst,
|
||||
zoomToFit: options?.zoomToFit,
|
||||
zoomOnRangeAndType: options?.zoomOnRangeAndType,
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Engine execution error (UI is still updated):', e)
|
||||
}
|
||||
}
|
@ -43,6 +43,7 @@ import { KclManager } from 'lang/KclSingleton'
|
||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||
import EditorManager from 'editor/manager'
|
||||
import CodeManager from 'lang/codeManager'
|
||||
import { updateModelingState } from 'lang/modelingWorkflows'
|
||||
|
||||
// Edge Treatment Types
|
||||
export enum EdgeTreatmentType {
|
||||
@ -83,7 +84,17 @@ export async function applyEdgeTreatmentToSelection(
|
||||
const { modifiedAst, pathToEdgeTreatmentNode } = result
|
||||
|
||||
// 2. update ast
|
||||
await updateAstAndFocus(modifiedAst, pathToEdgeTreatmentNode, dependencies)
|
||||
await updateModelingState(
|
||||
modifiedAst,
|
||||
{
|
||||
kclManager: dependencies.kclManager,
|
||||
editorManager: dependencies.editorManager,
|
||||
codeManager: dependencies.codeManager,
|
||||
},
|
||||
{
|
||||
focusPath: pathToEdgeTreatmentNode,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function modifyAstWithEdgeTreatmentAndTag(
|
||||
@ -294,33 +305,6 @@ export function getPathToExtrudeForSegmentSelection(
|
||||
return { pathToSegmentNode, pathToExtrudeNode }
|
||||
}
|
||||
|
||||
async function updateAstAndFocus(
|
||||
modifiedAst: Node<Program>,
|
||||
pathToEdgeTreatmentNode: Array<PathToNode>,
|
||||
dependencies: {
|
||||
kclManager: KclManager
|
||||
engineCommandManager: EngineCommandManager
|
||||
editorManager: EditorManager
|
||||
codeManager: CodeManager
|
||||
}
|
||||
): Promise<void> {
|
||||
const updatedAst = await dependencies.kclManager.updateAst(
|
||||
modifiedAst,
|
||||
true,
|
||||
{
|
||||
focusPath: pathToEdgeTreatmentNode,
|
||||
}
|
||||
)
|
||||
|
||||
await dependencies.codeManager.updateEditorWithAstAndWriteToFile(
|
||||
updatedAst.newAst
|
||||
)
|
||||
|
||||
if (updatedAst?.selections) {
|
||||
dependencies.editorManager.selectRange(updatedAst?.selections)
|
||||
}
|
||||
}
|
||||
|
||||
export function mutateAstWithTagForSketchSegment(
|
||||
astClone: Node<Program>,
|
||||
pathToSegmentNode: PathToNode
|
||||
|