Compare commits
	
		
			60 Commits
		
	
	
		
			jtran/pars
			...
			pierremtb/
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| f94cc40fcc | |||
| f9b2750356 | |||
| 82ca178e77 | |||
| 6416d9e0d9 | |||
| 785a63ce90 | |||
| 9e82921f08 | |||
| 2dd98c8a96 | |||
| ab2075df9d | |||
| 45fd7134d3 | |||
| 5865a08fc8 | |||
| 0cabc461a9 | |||
| a0ea54e33f | |||
| 1268e62b97 | |||
| e8fcd805a1 | |||
| 698e648de7 | |||
| 5751ac1aed | |||
| dbb18b13a2 | |||
| e0e7db6cbd | |||
| 289cfa86f1 | |||
| e869e2395d | |||
| 6976eb2041 | |||
| df53436ed0 | |||
| 431de3d666 | |||
| d296a9eb4a | |||
| 5bd15932d1 | |||
| 804a544196 | |||
| 97cc1863e9 | |||
| 38b934255a | |||
| b298e4cb24 | |||
| c67baa34a0 | |||
| 9a6ca2dfce | |||
| f47801a22d | |||
| 52c0fe6144 | |||
| 5584180957 | |||
| ee414bb5dc | |||
| dc48823e9c | |||
| 9f8a7cd2d2 | |||
| cbf455c7c6 | |||
| b55ecfdea9 | |||
| 19dd060912 | |||
| 3dc9bf282d | |||
| cc864681f4 | |||
| 9744d13d4f | |||
| 434f1045ef | |||
| 575844ff45 | |||
| d08f671ab3 | |||
| 0512e7d404 | |||
| 638a6d0761 | |||
| 416299b47a | |||
| 9bd33ed256 | |||
| 2818f5c897 | |||
| 97355d1e86 | |||
| 0dfb2fae39 | |||
| 4bf580192c | |||
| a8d02ac197 | |||
| f68310898b | |||
| d1563ead8c | |||
| 0199a30178 | |||
| ced2f5aa5d | |||
| 4ef1312c91 | 
| @ -1333,98 +1333,166 @@ loft001 = loft([sketch001, sketch002]) | |||||||
|     }) |     }) | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   test(`Sweep point-and-click`, async ({ |   const sweepCases = [ | ||||||
|     context, |     { | ||||||
|     page, |       targetType: 'circle', | ||||||
|     homePage, |       testPoint: { x: 700, y: 250 }, | ||||||
|     scene, |       initialCode: `sketch001 = startSketchOn('YZ') | ||||||
|     editor, | profile001 = circle(sketch001, center = [0, 0], radius = 500) | ||||||
|     toolbar, |  | ||||||
|     cmdBar, |  | ||||||
|   }) => { |  | ||||||
|     const initialCode = `sketch001 = startSketchOn('YZ') |  | ||||||
|   |> circle( |  | ||||||
|        center = [0, 0], |  | ||||||
|        radius = 500 |  | ||||||
|      ) |  | ||||||
| sketch002 = startSketchOn('XZ') | sketch002 = startSketchOn('XZ') | ||||||
|   |> startProfileAt([0, 0], %) |   |> startProfileAt([0, 0], %) | ||||||
|   |> xLine(length = -500) |   |> xLine(length = -500) | ||||||
|   |> tangentialArcTo([-2000, 500], %) |   |> tangentialArcTo([-2000, 500], %)`, | ||||||
| ` |     }, | ||||||
|     await context.addInitScript((initialCode) => { |     { | ||||||
|       localStorage.setItem('persistCode', initialCode) |       targetType: 'rectangle', | ||||||
|     }, initialCode) |       testPoint: { x: 710, y: 255 }, | ||||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) |       initialCode: `sketch001 = startSketchOn('YZ') | ||||||
|     await homePage.goToModelingScene() | profile001 = startProfileAt([-400, -400], sketch001) | ||||||
|     await scene.waitForExecutionDone() |   |> angledLine([0, 800], %, $rectangleSegmentA001) | ||||||
|  |   |> angledLine([ | ||||||
|  |        segAng(rectangleSegmentA001) + 90, | ||||||
|  |        800 | ||||||
|  |      ], %) | ||||||
|  |   |> angledLine([ | ||||||
|  |        segAng(rectangleSegmentA001), | ||||||
|  |        -segLen(rectangleSegmentA001) | ||||||
|  |      ], %) | ||||||
|  |   |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) | ||||||
|  |   |> close() | ||||||
|  | sketch002 = startSketchOn('XZ') | ||||||
|  |   |> startProfileAt([0, 0], %) | ||||||
|  |   |> xLine(length = -500) | ||||||
|  |   |> tangentialArcTo([-2000, 500], %)`, | ||||||
|  |     }, | ||||||
|  |   ] | ||||||
|  |   sweepCases.map(({ initialCode, targetType, testPoint }) => { | ||||||
|  |     test(`Sweep point-and-click ${targetType}`, async ({ | ||||||
|  |       context, | ||||||
|  |       page, | ||||||
|  |       homePage, | ||||||
|  |       scene, | ||||||
|  |       editor, | ||||||
|  |       toolbar, | ||||||
|  |       cmdBar, | ||||||
|  |     }) => { | ||||||
|  |       await context.addInitScript((initialCode) => { | ||||||
|  |         localStorage.setItem('persistCode', initialCode) | ||||||
|  |       }, initialCode) | ||||||
|  |       await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||||
|  |       await homePage.goToModelingScene() | ||||||
|  |       await scene.waitForExecutionDone() | ||||||
|  |  | ||||||
|     // One dumb hardcoded screen pixel value |       // One dumb hardcoded screen pixel value | ||||||
|     const testPoint = { x: 700, y: 250 } |       const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y) | ||||||
|     const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y) |       const [clickOnSketch2] = scene.makeMouseHelpers( | ||||||
|     const [clickOnSketch2] = scene.makeMouseHelpers( |         testPoint.x - 50, | ||||||
|       testPoint.x - 50, |         testPoint.y | ||||||
|       testPoint.y |       ) | ||||||
|     ) |       const sweepDeclaration = | ||||||
|     const sweepDeclaration = 'sweep001 = sweep(sketch001, path = sketch002)' |         'sweep001 = sweep(profile001, path = sketch002, sectional = false)' | ||||||
|  |       const editedSweepDeclaration = | ||||||
|  |         'sweep001 = sweep(profile001, path = sketch002, sectional = true)' | ||||||
|  |  | ||||||
|     await test.step(`Look for sketch001`, async () => { |       await test.step(`Look for sketch001`, async () => { | ||||||
|       await toolbar.closePane('code') |         await toolbar.closePane('code') | ||||||
|       await scene.expectPixelColor([53, 53, 53], testPoint, 15) |         await scene.expectPixelColor([53, 53, 53], testPoint, 15) | ||||||
|     }) |  | ||||||
|  |  | ||||||
|     await test.step(`Go through the command bar flow`, async () => { |  | ||||||
|       await toolbar.sweepButton.click() |  | ||||||
|       await cmdBar.expectState({ |  | ||||||
|         commandName: 'Sweep', |  | ||||||
|         currentArgKey: 'target', |  | ||||||
|         currentArgValue: '', |  | ||||||
|         headerArguments: { |  | ||||||
|           Target: '', |  | ||||||
|           Trajectory: '', |  | ||||||
|         }, |  | ||||||
|         highlightedHeaderArg: 'target', |  | ||||||
|         stage: 'arguments', |  | ||||||
|       }) |       }) | ||||||
|       await clickOnSketch1() |  | ||||||
|       await cmdBar.expectState({ |  | ||||||
|         commandName: 'Sweep', |  | ||||||
|         currentArgKey: 'trajectory', |  | ||||||
|         currentArgValue: '', |  | ||||||
|         headerArguments: { |  | ||||||
|           Target: '1 face', |  | ||||||
|           Trajectory: '', |  | ||||||
|         }, |  | ||||||
|         highlightedHeaderArg: 'trajectory', |  | ||||||
|         stage: 'arguments', |  | ||||||
|       }) |  | ||||||
|       await clickOnSketch2() |  | ||||||
|       await page.waitForTimeout(500) |  | ||||||
|       await cmdBar.progressCmdBar() |  | ||||||
|       await toolbar.openPane('code') |  | ||||||
|       await page.waitForTimeout(500) |  | ||||||
|     }) |  | ||||||
|  |  | ||||||
|     await test.step(`Confirm code is added to the editor, scene has changed`, async () => { |       await test.step(`Go through the command bar flow`, async () => { | ||||||
|       // await scene.expectPixelColor([135, 64, 73], testPoint, 15) // FIXME |         await toolbar.sweepButton.click() | ||||||
|       await editor.expectEditor.toContain(sweepDeclaration) |         await cmdBar.expectState({ | ||||||
|       await editor.expectState({ |           commandName: 'Sweep', | ||||||
|         diagnostics: [], |           currentArgKey: 'target', | ||||||
|         activeLines: [sweepDeclaration], |           currentArgValue: '', | ||||||
|         highlightedCode: '', |           headerArguments: { | ||||||
|  |             Sectional: '', | ||||||
|  |             Target: '', | ||||||
|  |             Trajectory: '', | ||||||
|  |           }, | ||||||
|  |           highlightedHeaderArg: 'target', | ||||||
|  |           stage: 'arguments', | ||||||
|  |         }) | ||||||
|  |         await clickOnSketch1() | ||||||
|  |         await cmdBar.expectState({ | ||||||
|  |           commandName: 'Sweep', | ||||||
|  |           currentArgKey: 'trajectory', | ||||||
|  |           currentArgValue: '', | ||||||
|  |           headerArguments: { | ||||||
|  |             Sectional: '', | ||||||
|  |             Target: '1 face', | ||||||
|  |             Trajectory: '', | ||||||
|  |           }, | ||||||
|  |           highlightedHeaderArg: 'trajectory', | ||||||
|  |           stage: 'arguments', | ||||||
|  |         }) | ||||||
|  |         await clickOnSketch2() | ||||||
|  |         await page.waitForTimeout(500) | ||||||
|  |         await cmdBar.progressCmdBar() | ||||||
|  |         await cmdBar.expectState({ | ||||||
|  |           commandName: 'Sweep', | ||||||
|  |           headerArguments: { | ||||||
|  |             Target: '1 face', | ||||||
|  |             Trajectory: '1 segment', | ||||||
|  |             Sectional: '', | ||||||
|  |           }, | ||||||
|  |           stage: 'review', | ||||||
|  |         }) | ||||||
|  |         await cmdBar.progressCmdBar() | ||||||
|       }) |       }) | ||||||
|       await toolbar.closePane('code') |  | ||||||
|     }) |  | ||||||
|  |  | ||||||
|     await test.step('Delete sweep via feature tree selection', async () => { |       await test.step(`Confirm code is added to the editor, scene has changed`, async () => { | ||||||
|       await toolbar.openPane('feature-tree') |         await toolbar.openPane('code') | ||||||
|       await page.waitForTimeout(500) |         await editor.expectEditor.toContain(sweepDeclaration) | ||||||
|       const operationButton = await toolbar.getFeatureTreeOperation('Sweep', 0) |         await scene.expectPixelColor([120, 120, 120], testPoint, 40) | ||||||
|       await operationButton.click({ button: 'left' }) |         await toolbar.closePane('code') | ||||||
|       await page.keyboard.press('Delete') |       }) | ||||||
|       await page.waitForTimeout(500) |  | ||||||
|       await toolbar.closePane('feature-tree') |       await test.step('Edit sweep via feature tree selection works', async () => { | ||||||
|       await scene.expectPixelColor([53, 53, 53], testPoint, 15) |         await toolbar.openPane('feature-tree') | ||||||
|  |         const operationButton = await toolbar.getFeatureTreeOperation( | ||||||
|  |           'Sweep', | ||||||
|  |           0 | ||||||
|  |         ) | ||||||
|  |         await operationButton.dblclick({ button: 'left' }) | ||||||
|  |         await cmdBar.expectState({ | ||||||
|  |           commandName: 'Sweep', | ||||||
|  |           currentArgKey: 'sectional', | ||||||
|  |           currentArgValue: '', | ||||||
|  |           headerArguments: { | ||||||
|  |             Sectional: '', | ||||||
|  |           }, | ||||||
|  |           highlightedHeaderArg: 'sectional', | ||||||
|  |           stage: 'arguments', | ||||||
|  |         }) | ||||||
|  |         await cmdBar.selectOption({ name: 'True' }).click() | ||||||
|  |         await cmdBar.expectState({ | ||||||
|  |           commandName: 'Sweep', | ||||||
|  |           headerArguments: { | ||||||
|  |             Sectional: '', | ||||||
|  |           }, | ||||||
|  |           stage: 'review', | ||||||
|  |         }) | ||||||
|  |         await cmdBar.progressCmdBar() | ||||||
|  |         await toolbar.closePane('feature-tree') | ||||||
|  |         await toolbar.openPane('code') | ||||||
|  |         await editor.expectEditor.toContain(editedSweepDeclaration) | ||||||
|  |         await toolbar.closePane('code') | ||||||
|  |       }) | ||||||
|  |  | ||||||
|  |       await test.step('Delete sweep via feature tree selection', async () => { | ||||||
|  |         await toolbar.openPane('feature-tree') | ||||||
|  |         await page.waitForTimeout(500) | ||||||
|  |         const operationButton = await toolbar.getFeatureTreeOperation( | ||||||
|  |           'Sweep', | ||||||
|  |           0 | ||||||
|  |         ) | ||||||
|  |         await operationButton.click({ button: 'left' }) | ||||||
|  |         await page.keyboard.press('Delete') | ||||||
|  |         await page.waitForTimeout(500) | ||||||
|  |         await toolbar.closePane('feature-tree') | ||||||
|  |         await scene.expectPixelColor([53, 53, 53], testPoint, 15) | ||||||
|  |       }) | ||||||
|     }) |     }) | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
| @ -1473,6 +1541,7 @@ sketch002 = startSketchOn('XZ') | |||||||
|         currentArgKey: 'target', |         currentArgKey: 'target', | ||||||
|         currentArgValue: '', |         currentArgValue: '', | ||||||
|         headerArguments: { |         headerArguments: { | ||||||
|  |           Sectional: '', | ||||||
|           Target: '', |           Target: '', | ||||||
|           Trajectory: '', |           Trajectory: '', | ||||||
|         }, |         }, | ||||||
| @ -1485,6 +1554,7 @@ sketch002 = startSketchOn('XZ') | |||||||
|         currentArgKey: 'trajectory', |         currentArgKey: 'trajectory', | ||||||
|         currentArgValue: '', |         currentArgValue: '', | ||||||
|         headerArguments: { |         headerArguments: { | ||||||
|  |           Sectional: '', | ||||||
|           Target: '1 face', |           Target: '1 face', | ||||||
|           Trajectory: '', |           Trajectory: '', | ||||||
|         }, |         }, | ||||||
|  | |||||||
| Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB | 
| Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 54 KiB | 
| Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB | 
| Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB | 
| Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 143 KiB | 
| Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB | 
| @ -205,6 +205,21 @@ pub(crate) async fn do_post_extrude<'a>( | |||||||
|         vec![] |         vec![] | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|  |     // Face filtering attempt in order to resolve https://github.com/KittyCAD/modeling-app/issues/5328 | ||||||
|  |     // In case of a sectional sweep, empirically it looks that the first n faces that are yielded from the sweep | ||||||
|  |     // are the ones that work with GetOppositeEdge and GetNextAdjacentEdge, aka the n sides in the sweep. | ||||||
|  |     // So here we're figuring out that n number as yielded_sides_count here, | ||||||
|  |     // making sure that circle() calls count but close() don't (no length) | ||||||
|  |     let yielded_sides_count = sketch | ||||||
|  |         .paths | ||||||
|  |         .iter() | ||||||
|  |         .filter(|p| { | ||||||
|  |             let is_circle = matches!(p, Path::Circle { .. }); | ||||||
|  |             let has_length = p.get_base().from != p.get_base().to; | ||||||
|  |             return is_circle || has_length; | ||||||
|  |         }) | ||||||
|  |         .count(); | ||||||
|  |  | ||||||
|     for (curve_id, face_id) in face_infos |     for (curve_id, face_id) in face_infos | ||||||
|         .iter() |         .iter() | ||||||
|         .filter(|face_info| face_info.cap == ExtrusionFaceCapType::None) |         .filter(|face_info| face_info.cap == ExtrusionFaceCapType::None) | ||||||
| @ -215,6 +230,7 @@ pub(crate) async fn do_post_extrude<'a>( | |||||||
|                 None |                 None | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
|  |         .take(yielded_sides_count) | ||||||
|     { |     { | ||||||
|         // Batch these commands, because the Rust code doesn't actually care about the outcome. |         // Batch these commands, because the Rust code doesn't actually care about the outcome. | ||||||
|         // So, there's no need to await them. |         // So, there's no need to await them. | ||||||
|  | |||||||
| @ -515,30 +515,54 @@ export function addShell({ | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| export function addSweep( | export function addSweep({ | ||||||
|   node: Node<Program>, |   node, | ||||||
|   profileDeclarator: VariableDeclarator, |   targetDeclarator, | ||||||
|   pathDeclarator: VariableDeclarator |   trajectoryDeclarator, | ||||||
| ): { |   sectional, | ||||||
|  |   variableName, | ||||||
|  |   insertIndex, | ||||||
|  | }: { | ||||||
|  |   node: Node<Program> | ||||||
|  |   targetDeclarator: VariableDeclarator | ||||||
|  |   trajectoryDeclarator: VariableDeclarator | ||||||
|  |   sectional: boolean | ||||||
|  |   variableName?: string | ||||||
|  |   insertIndex?: number | ||||||
|  | }): { | ||||||
|   modifiedAst: Node<Program> |   modifiedAst: Node<Program> | ||||||
|   pathToNode: PathToNode |   pathToNode: PathToNode | ||||||
| } { | } { | ||||||
|   const modifiedAst = structuredClone(node) |   const modifiedAst = structuredClone(node) | ||||||
|   const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SWEEP) |   const name = | ||||||
|   const sweep = createCallExpressionStdLibKw( |     variableName ?? findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SWEEP) | ||||||
|  |   const call = createCallExpressionStdLibKw( | ||||||
|     'sweep', |     'sweep', | ||||||
|     createIdentifier(profileDeclarator.id.name), |     createIdentifier(targetDeclarator.id.name), | ||||||
|     [createLabeledArg('path', createIdentifier(pathDeclarator.id.name))] |     [ | ||||||
|  |       createLabeledArg('path', createIdentifier(trajectoryDeclarator.id.name)), | ||||||
|  |       createLabeledArg('sectional', createLiteral(sectional)), | ||||||
|  |     ] | ||||||
|   ) |   ) | ||||||
|   const declaration = createVariableDeclaration(name, sweep) |   const variable = createVariableDeclaration(name, call) | ||||||
|   modifiedAst.body.push(declaration) |   const insertAt = | ||||||
|  |     insertIndex !== undefined | ||||||
|  |       ? insertIndex | ||||||
|  |       : modifiedAst.body.length | ||||||
|  |       ? modifiedAst.body.length | ||||||
|  |       : 0 | ||||||
|  |  | ||||||
|  |   modifiedAst.body.length | ||||||
|  |     ? modifiedAst.body.splice(insertAt, 0, variable) | ||||||
|  |     : modifiedAst.body.push(variable) | ||||||
|  |   const argIndex = 0 | ||||||
|   const pathToNode: PathToNode = [ |   const pathToNode: PathToNode = [ | ||||||
|     ['body', ''], |     ['body', ''], | ||||||
|     [modifiedAst.body.length - 1, 'index'], |     [insertAt, 'index'], | ||||||
|     ['declaration', 'VariableDeclaration'], |     ['declaration', 'VariableDeclaration'], | ||||||
|     ['init', 'VariableDeclarator'], |     ['init', 'VariableDeclarator'], | ||||||
|     ['arguments', 'CallExpressionKw'], |     ['arguments', 'CallExpressionKw'], | ||||||
|     [0, ARG_INDEX_FIELD], |     [argIndex, ARG_INDEX_FIELD], | ||||||
|     ['arg', LABELED_ARG_FIELD], |     ['arg', LABELED_ARG_FIELD], | ||||||
|   ] |   ] | ||||||
|  |  | ||||||
|  | |||||||
| @ -47,8 +47,12 @@ export type ModelingCommandSchema = { | |||||||
|     distance: KclCommandValue |     distance: KclCommandValue | ||||||
|   } |   } | ||||||
|   Sweep: { |   Sweep: { | ||||||
|  |     // Enables editing workflow | ||||||
|  |     nodeToEdit?: PathToNode | ||||||
|  |     // Arguments | ||||||
|     target: Selections |     target: Selections | ||||||
|     trajectory: Selections |     trajectory: Selections | ||||||
|  |     sectional: boolean | ||||||
|   } |   } | ||||||
|   Loft: { |   Loft: { | ||||||
|     selection: Selections |     selection: Selections | ||||||
| @ -345,22 +349,40 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< | |||||||
|     description: |     description: | ||||||
|       'Create a 3D body by moving a sketch region along an arbitrary path.', |       'Create a 3D body by moving a sketch region along an arbitrary path.', | ||||||
|     icon: 'sweep', |     icon: 'sweep', | ||||||
|     needsReview: false, |     needsReview: true, | ||||||
|     args: { |     args: { | ||||||
|  |       nodeToEdit: { | ||||||
|  |         description: | ||||||
|  |           'Path to the node in the AST to edit. Never shown to the user.', | ||||||
|  |         skip: true, | ||||||
|  |         inputType: 'text', | ||||||
|  |         required: false, | ||||||
|  |       }, | ||||||
|       target: { |       target: { | ||||||
|         inputType: 'selection', |         inputType: 'selection', | ||||||
|         selectionTypes: ['solid2d'], |         selectionTypes: ['solid2d'], | ||||||
|         required: true, |         required: true, | ||||||
|         skip: true, |         skip: true, | ||||||
|         multiple: false, |         multiple: false, | ||||||
|  |         hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit), | ||||||
|       }, |       }, | ||||||
|       trajectory: { |       trajectory: { | ||||||
|         inputType: 'selection', |         inputType: 'selection', | ||||||
|         selectionTypes: ['segment', 'path'], |         selectionTypes: ['segment'], | ||||||
|         required: true, |         required: true, | ||||||
|         skip: false, |         skip: true, | ||||||
|         multiple: false, |         multiple: false, | ||||||
|         validation: sweepValidator, |         validation: sweepValidator, | ||||||
|  |         hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit), | ||||||
|  |       }, | ||||||
|  |       sectional: { | ||||||
|  |         inputType: 'options', | ||||||
|  |         required: true, | ||||||
|  |         options: [ | ||||||
|  |           { name: 'False', value: false }, | ||||||
|  |           { name: 'True', value: true }, | ||||||
|  |         ], | ||||||
|  |         // No validation possible here until we have rollback | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  | |||||||
| @ -311,6 +311,143 @@ const prepareToEditOffsetPlane: PrepareToEditCallback = async ({ | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | const prepareToEditSweep: PrepareToEditCallback = async ({ | ||||||
|  |   artifact, | ||||||
|  |   operation, | ||||||
|  | }) => { | ||||||
|  |   const baseCommand = { | ||||||
|  |     name: 'Sweep', | ||||||
|  |     groupId: 'modeling', | ||||||
|  |   } | ||||||
|  |   if ( | ||||||
|  |     operation.type !== 'StdLibCall' || | ||||||
|  |     !operation.labeledArgs || | ||||||
|  |     !operation.unlabeledArg || | ||||||
|  |     !('sectional' in operation.labeledArgs) || | ||||||
|  |     !operation.labeledArgs.sectional | ||||||
|  |   ) { | ||||||
|  |     return baseCommand | ||||||
|  |   } | ||||||
|  |   if (!artifact || !('pathId' in artifact) || operation.type !== 'StdLibCall') { | ||||||
|  |     return baseCommand | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // We have to go a little roundabout to get from the original artifact | ||||||
|  |   // to the solid2DId that we need to pass to the Sweep command, just like Extrude. | ||||||
|  |   const pathArtifact = getArtifactOfTypes( | ||||||
|  |     { | ||||||
|  |       key: artifact.pathId, | ||||||
|  |       types: ['path'], | ||||||
|  |     }, | ||||||
|  |     engineCommandManager.artifactGraph | ||||||
|  |   ) | ||||||
|  |  | ||||||
|  |   if ( | ||||||
|  |     err(pathArtifact) || | ||||||
|  |     pathArtifact.type !== 'path' || | ||||||
|  |     !pathArtifact.solid2dId | ||||||
|  |   ) { | ||||||
|  |     return baseCommand | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const targetArtifact = getArtifactOfTypes( | ||||||
|  |     { | ||||||
|  |       key: pathArtifact.solid2dId, | ||||||
|  |       types: ['solid2d'], | ||||||
|  |     }, | ||||||
|  |     engineCommandManager.artifactGraph | ||||||
|  |   ) | ||||||
|  |  | ||||||
|  |   if (err(targetArtifact) || targetArtifact.type !== 'solid2d') { | ||||||
|  |     return baseCommand | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const target = { | ||||||
|  |     graphSelections: [ | ||||||
|  |       { | ||||||
|  |         artifact: targetArtifact, | ||||||
|  |         codeRef: pathArtifact.codeRef, | ||||||
|  |       }, | ||||||
|  |     ], | ||||||
|  |     otherSelections: [], | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Same roundabout but twice for 'path' aka trajectory: sketch -> path -> segment | ||||||
|  |   if (!('path' in operation.labeledArgs) || !operation.labeledArgs.path) { | ||||||
|  |     return baseCommand | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (operation.labeledArgs.path.value.type !== 'Sketch') { | ||||||
|  |     return baseCommand | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const trajectoryPathArtifact = getArtifactOfTypes( | ||||||
|  |     { | ||||||
|  |       key: operation.labeledArgs.path.value.value.artifactId, | ||||||
|  |       types: ['path'], | ||||||
|  |     }, | ||||||
|  |     engineCommandManager.artifactGraph | ||||||
|  |   ) | ||||||
|  |  | ||||||
|  |   if (err(trajectoryPathArtifact) || trajectoryPathArtifact.type !== 'path') { | ||||||
|  |     return baseCommand | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const trajectoryArtifact = getArtifactOfTypes( | ||||||
|  |     { | ||||||
|  |       key: trajectoryPathArtifact.segIds[0], | ||||||
|  |       types: ['segment'], | ||||||
|  |     }, | ||||||
|  |     engineCommandManager.artifactGraph | ||||||
|  |   ) | ||||||
|  |  | ||||||
|  |   if (err(trajectoryArtifact) || trajectoryArtifact.type !== 'segment') { | ||||||
|  |     return baseCommand | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const trajectory = { | ||||||
|  |     graphSelections: [ | ||||||
|  |       { | ||||||
|  |         artifact: trajectoryArtifact, | ||||||
|  |         codeRef: trajectoryArtifact.codeRef, | ||||||
|  |       }, | ||||||
|  |     ], | ||||||
|  |     otherSelections: [], | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // sectional options boolean arg | ||||||
|  |   if ( | ||||||
|  |     !('sectional' in operation.labeledArgs) || | ||||||
|  |     !operation.labeledArgs.sectional | ||||||
|  |   ) { | ||||||
|  |     return baseCommand | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const sectional = | ||||||
|  |     codeManager.code.slice( | ||||||
|  |       operation.labeledArgs.sectional.sourceRange[0], | ||||||
|  |       operation.labeledArgs.sectional.sourceRange[1] | ||||||
|  |     ) === 'true' | ||||||
|  |  | ||||||
|  |   // Assemble the default argument values for the Offset Plane command, | ||||||
|  |   // with `nodeToEdit` set, which will let the Offset Plane actor know | ||||||
|  |   // to edit the node that corresponds to the StdLibCall. | ||||||
|  |   const argDefaultValues: ModelingCommandSchema['Sweep'] = { | ||||||
|  |     target: target, | ||||||
|  |     trajectory, | ||||||
|  |     sectional, | ||||||
|  |     nodeToEdit: getNodePathFromSourceRange( | ||||||
|  |       kclManager.ast, | ||||||
|  |       sourceRangeFromRust(operation.sourceRange) | ||||||
|  |     ), | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return { | ||||||
|  |     ...baseCommand, | ||||||
|  |     argDefaultValues, | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| const prepareToEditHelix: PrepareToEditCallback = async ({ operation }) => { | const prepareToEditHelix: PrepareToEditCallback = async ({ operation }) => { | ||||||
|   const baseCommand = { |   const baseCommand = { | ||||||
|     name: 'Helix', |     name: 'Helix', | ||||||
| @ -511,6 +648,7 @@ export const stdLibMap: Record<string, StdLibCallInfo> = { | |||||||
|   sweep: { |   sweep: { | ||||||
|     label: 'Sweep', |     label: 'Sweep', | ||||||
|     icon: 'sweep', |     icon: 'sweep', | ||||||
|  |     prepareToEdit: prepareToEditSweep, | ||||||
|     supportsAppearance: true, |     supportsAppearance: true, | ||||||
|   }, |   }, | ||||||
| } | } | ||||||
|  | |||||||
| @ -78,7 +78,7 @@ import { | |||||||
| import { ModelingCommandSchema } from 'lib/commandBarConfigs/modelingCommandConfig' | import { ModelingCommandSchema } from 'lib/commandBarConfigs/modelingCommandConfig' | ||||||
| import { err, reportRejection, trap } from 'lib/trap' | import { err, reportRejection, trap } from 'lib/trap' | ||||||
| import { DefaultPlaneStr } from 'lib/planes' | import { DefaultPlaneStr } from 'lib/planes' | ||||||
| import { uuidv4 } from 'lib/utils' | import { isArray, uuidv4 } from 'lib/utils' | ||||||
| import { Coords2d } from 'lang/std/sketch' | import { Coords2d } from 'lang/std/sketch' | ||||||
| import { deleteSegment } from 'clientSideScene/ClientSideSceneComp' | import { deleteSegment } from 'clientSideScene/ClientSideSceneComp' | ||||||
| import toast from 'react-hot-toast' | import toast from 'react-hot-toast' | ||||||
| @ -97,6 +97,7 @@ import { createProfileStartHandle } from 'clientSideScene/segments' | |||||||
| import { DRAFT_POINT } from 'clientSideScene/sceneInfra' | import { DRAFT_POINT } from 'clientSideScene/sceneInfra' | ||||||
| import { setAppearance } from 'lang/modifyAst/setAppearance' | import { setAppearance } from 'lang/modifyAst/setAppearance' | ||||||
| import { DRAFT_DASHED_LINE } from 'clientSideScene/sceneEntities' | import { DRAFT_DASHED_LINE } from 'clientSideScene/sceneEntities' | ||||||
|  | import { updateModelingState } from 'lang/modelingWorkflows' | ||||||
|  |  | ||||||
| export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY' | export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY' | ||||||
|  |  | ||||||
| @ -1988,55 +1989,88 @@ export const modelingMachine = setup({ | |||||||
|         if (!input) return new Error('No input provided') |         if (!input) return new Error('No input provided') | ||||||
|         // Extract inputs |         // Extract inputs | ||||||
|         const ast = kclManager.ast |         const ast = kclManager.ast | ||||||
|         const { target, trajectory } = input |         const { target, trajectory, sectional, nodeToEdit } = input | ||||||
|  |         let variableName: string | undefined = undefined | ||||||
|  |         let insertIndex: number | undefined = undefined | ||||||
|  |  | ||||||
|         // Find the profile declaration |         // If this is an edit flow, first we're going to remove the old one | ||||||
|  |         if (nodeToEdit !== undefined && typeof nodeToEdit[1][0] === 'number') { | ||||||
|  |           // Extract the plane name from the node to edit | ||||||
|  |           const variableNode = getNodeFromPath<VariableDeclaration>( | ||||||
|  |             ast, | ||||||
|  |             nodeToEdit, | ||||||
|  |             'VariableDeclaration' | ||||||
|  |           ) | ||||||
|  |  | ||||||
|  |           if (err(variableNode)) { | ||||||
|  |             console.error('Error extracting name') | ||||||
|  |           } else { | ||||||
|  |             variableName = variableNode.node.declaration.id.name | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           // Removing the old statement | ||||||
|  |           const newBody = [...ast.body] | ||||||
|  |           newBody.splice(nodeToEdit[1][0], 1) | ||||||
|  |           ast.body = newBody | ||||||
|  |           insertIndex = nodeToEdit[1][0] | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Find the target declaration | ||||||
|         const targetNodePath = getNodePathFromSourceRange( |         const targetNodePath = getNodePathFromSourceRange( | ||||||
|           ast, |           ast, | ||||||
|           target.graphSelections[0].codeRef.range |           target.graphSelections[0].codeRef.range | ||||||
|         ) |         ) | ||||||
|         const targetNode = getNodeFromPath<VariableDeclarator>( |         // Gotchas, not sure why | ||||||
|           ast, |         // - it seems like in some cases we get a list on edit, especially the state that e2e hits | ||||||
|           targetNodePath, |         // - looking for a VariableDeclaration seems more robust than VariableDeclarator | ||||||
|           'VariableDeclarator' |         const targetNode = getNodeFromPath< | ||||||
|         ) |           VariableDeclaration | VariableDeclaration[] | ||||||
|  |         >(ast, targetNodePath, 'VariableDeclaration') | ||||||
|         if (err(targetNode)) { |         if (err(targetNode)) { | ||||||
|           return new Error("Couldn't parse profile selection") |           return new Error("Couldn't parse profile selection") | ||||||
|         } |         } | ||||||
|         const targetDeclarator = targetNode.node |  | ||||||
|  |  | ||||||
|         // Find the path declaration |         const targetDeclarator = isArray(targetNode.node) | ||||||
|  |           ? targetNode.node[0].declaration | ||||||
|  |           : targetNode.node.declaration | ||||||
|  |  | ||||||
|  |         // Find the trajectory (or path) declaration | ||||||
|         const trajectoryNodePath = getNodePathFromSourceRange( |         const trajectoryNodePath = getNodePathFromSourceRange( | ||||||
|           ast, |           ast, | ||||||
|           trajectory.graphSelections[0].codeRef.range |           trajectory.graphSelections[0].codeRef.range | ||||||
|         ) |         ) | ||||||
|         const trajectoryNode = getNodeFromPath<VariableDeclarator>( |         // Also looking for VariableDeclaration for consistency here | ||||||
|  |         const trajectoryNode = getNodeFromPath<VariableDeclaration>( | ||||||
|           ast, |           ast, | ||||||
|           trajectoryNodePath, |           trajectoryNodePath, | ||||||
|           'VariableDeclarator' |           'VariableDeclaration' | ||||||
|         ) |         ) | ||||||
|         if (err(trajectoryNode)) { |         if (err(trajectoryNode)) { | ||||||
|           return new Error("Couldn't parse path selection") |           return new Error("Couldn't parse path selection") | ||||||
|         } |         } | ||||||
|         const trajectoryDeclarator = trajectoryNode.node |  | ||||||
|  |         const trajectoryDeclarator = trajectoryNode.node.declaration | ||||||
|  |  | ||||||
|         // Perform the sweep |         // Perform the sweep | ||||||
|         const sweepRes = addSweep(ast, targetDeclarator, trajectoryDeclarator) |         const { modifiedAst, pathToNode } = addSweep({ | ||||||
|         const updateAstResult = await kclManager.updateAst( |           node: ast, | ||||||
|           sweepRes.modifiedAst, |           targetDeclarator, | ||||||
|           true, |           trajectoryDeclarator, | ||||||
|  |           sectional, | ||||||
|  |           variableName, | ||||||
|  |           insertIndex, | ||||||
|  |         }) | ||||||
|  |         await updateModelingState( | ||||||
|  |           modifiedAst, | ||||||
|           { |           { | ||||||
|             focusPath: [sweepRes.pathToNode], |             kclManager, | ||||||
|  |             editorManager, | ||||||
|  |             codeManager, | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             focusPath: [pathToNode], | ||||||
|           } |           } | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         await codeManager.updateEditorWithAstAndWriteToFile( |  | ||||||
|           updateAstResult.newAst |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         if (updateAstResult?.selections) { |  | ||||||
|           editorManager.selectRange(updateAstResult?.selections) |  | ||||||
|         } |  | ||||||
|       } |       } | ||||||
|     ), |     ), | ||||||
|     loftAstMod: fromPromise( |     loftAstMod: fromPromise( | ||||||
|  | |||||||
