diff --git a/e2e/playwright/point-click.spec.ts b/e2e/playwright/point-click.spec.ts index b81f2a87a..5800a7b7d 100644 --- a/e2e/playwright/point-click.spec.ts +++ b/e2e/playwright/point-click.spec.ts @@ -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, diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-5-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-5-Google-Chrome-linux.png index 28ca70811..49d0a5024 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-5-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-5-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-linux.png index d555f98b6..c78594e17 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--YZ-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--YZ-1-Google-Chrome-linux.png index c209d829d..4bc53b366 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--YZ-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--YZ-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-YZ-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-YZ-1-Google-Chrome-linux.png index 6a1ce492c..6542fe9b0 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-YZ-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-YZ-1-Google-Chrome-linux.png differ diff --git a/src/lang/modelingWorkflows.ts b/src/lang/modelingWorkflows.ts new file mode 100644 index 000000000..9882cfc65 --- /dev/null +++ b/src/lang/modelingWorkflows.ts @@ -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, + dependencies: { + kclManager: KclManager + editorManager: EditorManager + codeManager: CodeManager + }, + options?: { + focusPath?: Array + zoomToFit?: boolean + zoomOnRangeAndType?: { + range: SourceRange + type: string + } + } +): Promise { + // 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) + } +} diff --git a/src/lang/modifyAst/addEdgeTreatment.ts b/src/lang/modifyAst/addEdgeTreatment.ts index 235d0c4b2..40a47a363 100644 --- a/src/lang/modifyAst/addEdgeTreatment.ts +++ b/src/lang/modifyAst/addEdgeTreatment.ts @@ -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, - pathToEdgeTreatmentNode: Array, - dependencies: { - kclManager: KclManager - engineCommandManager: EngineCommandManager - editorManager: EditorManager - codeManager: CodeManager - } -): Promise { - 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, pathToSegmentNode: PathToNode