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>
This commit is contained in:
max
2025-03-19 17:48:29 +01:00
committed by GitHub
parent 533fa749b2
commit cb1b08d6b6
7 changed files with 208 additions and 28 deletions

View File

@ -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 ({ test(`Chamfer point-and-click`, async ({
context, context,
page, page,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View 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)
}
}

View File

@ -43,6 +43,7 @@ import { KclManager } from 'lang/KclSingleton'
import { EngineCommandManager } from 'lang/std/engineConnection' import { EngineCommandManager } from 'lang/std/engineConnection'
import EditorManager from 'editor/manager' import EditorManager from 'editor/manager'
import CodeManager from 'lang/codeManager' import CodeManager from 'lang/codeManager'
import { updateModelingState } from 'lang/modelingWorkflows'
// Edge Treatment Types // Edge Treatment Types
export enum EdgeTreatmentType { export enum EdgeTreatmentType {
@ -83,7 +84,17 @@ export async function applyEdgeTreatmentToSelection(
const { modifiedAst, pathToEdgeTreatmentNode } = result const { modifiedAst, pathToEdgeTreatmentNode } = result
// 2. update ast // 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( export function modifyAstWithEdgeTreatmentAndTag(
@ -294,33 +305,6 @@ export function getPathToExtrudeForSegmentSelection(
return { pathToSegmentNode, pathToExtrudeNode } 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( export function mutateAstWithTagForSketchSegment(
astClone: Node<Program>, astClone: Node<Program>,
pathToSegmentNode: PathToNode pathToSegmentNode: PathToNode