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,7 +1333,41 @@ loft001 = loft([sketch001, sketch002]) | ||||
|     }) | ||||
|   }) | ||||
|  | ||||
|   test(`Sweep point-and-click`, async ({ | ||||
|   const sweepCases = [ | ||||
|     { | ||||
|       targetType: 'circle', | ||||
|       testPoint: { x: 700, y: 250 }, | ||||
|       initialCode: `sketch001 = startSketchOn('YZ') | ||||
| profile001 = circle(sketch001, center = [0, 0], radius = 500) | ||||
| sketch002 = startSketchOn('XZ') | ||||
|   |> startProfileAt([0, 0], %) | ||||
|   |> xLine(length = -500) | ||||
|   |> tangentialArcTo([-2000, 500], %)`, | ||||
|     }, | ||||
|     { | ||||
|       targetType: 'rectangle', | ||||
|       testPoint: { x: 710, y: 255 }, | ||||
|       initialCode: `sketch001 = startSketchOn('YZ') | ||||
| profile001 = startProfileAt([-400, -400], sketch001) | ||||
|   |> 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, | ||||
| @ -1342,16 +1376,6 @@ loft001 = loft([sketch001, sketch002]) | ||||
|       toolbar, | ||||
|       cmdBar, | ||||
|     }) => { | ||||
|     const initialCode = `sketch001 = startSketchOn('YZ') | ||||
|   |> circle( | ||||
|        center = [0, 0], | ||||
|        radius = 500 | ||||
|      ) | ||||
| sketch002 = startSketchOn('XZ') | ||||
|   |> startProfileAt([0, 0], %) | ||||
|   |> xLine(length = -500) | ||||
|   |> tangentialArcTo([-2000, 500], %) | ||||
| ` | ||||
|       await context.addInitScript((initialCode) => { | ||||
|         localStorage.setItem('persistCode', initialCode) | ||||
|       }, initialCode) | ||||
| @ -1360,13 +1384,15 @@ sketch002 = startSketchOn('XZ') | ||||
|       await scene.waitForExecutionDone() | ||||
|  | ||||
|       // One dumb hardcoded screen pixel value | ||||
|     const testPoint = { x: 700, y: 250 } | ||||
|       const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y) | ||||
|       const [clickOnSketch2] = scene.makeMouseHelpers( | ||||
|         testPoint.x - 50, | ||||
|         testPoint.y | ||||
|       ) | ||||
|     const sweepDeclaration = 'sweep001 = sweep(sketch001, path = sketch002)' | ||||
|       const sweepDeclaration = | ||||
|         'sweep001 = sweep(profile001, path = sketch002, sectional = false)' | ||||
|       const editedSweepDeclaration = | ||||
|         'sweep001 = sweep(profile001, path = sketch002, sectional = true)' | ||||
|  | ||||
|       await test.step(`Look for sketch001`, async () => { | ||||
|         await toolbar.closePane('code') | ||||
| @ -1380,6 +1406,7 @@ sketch002 = startSketchOn('XZ') | ||||
|           currentArgKey: 'target', | ||||
|           currentArgValue: '', | ||||
|           headerArguments: { | ||||
|             Sectional: '', | ||||
|             Target: '', | ||||
|             Trajectory: '', | ||||
|           }, | ||||
| @ -1392,6 +1419,7 @@ sketch002 = startSketchOn('XZ') | ||||
|           currentArgKey: 'trajectory', | ||||
|           currentArgValue: '', | ||||
|           headerArguments: { | ||||
|             Sectional: '', | ||||
|             Target: '1 face', | ||||
|             Trajectory: '', | ||||
|           }, | ||||
| @ -1401,25 +1429,64 @@ sketch002 = startSketchOn('XZ') | ||||
|         await clickOnSketch2() | ||||
|         await page.waitForTimeout(500) | ||||
|         await cmdBar.progressCmdBar() | ||||
|       await toolbar.openPane('code') | ||||
|       await page.waitForTimeout(500) | ||||
|         await cmdBar.expectState({ | ||||
|           commandName: 'Sweep', | ||||
|           headerArguments: { | ||||
|             Target: '1 face', | ||||
|             Trajectory: '1 segment', | ||||
|             Sectional: '', | ||||
|           }, | ||||
|           stage: 'review', | ||||
|         }) | ||||
|         await cmdBar.progressCmdBar() | ||||
|       }) | ||||
|  | ||||
|       await test.step(`Confirm code is added to the editor, scene has changed`, async () => { | ||||
|       // await scene.expectPixelColor([135, 64, 73], testPoint, 15) // FIXME | ||||
|         await toolbar.openPane('code') | ||||
|         await editor.expectEditor.toContain(sweepDeclaration) | ||||
|       await editor.expectState({ | ||||
|         diagnostics: [], | ||||
|         activeLines: [sweepDeclaration], | ||||
|         highlightedCode: '', | ||||
|         await scene.expectPixelColor([120, 120, 120], testPoint, 40) | ||||
|         await toolbar.closePane('code') | ||||
|       }) | ||||
|  | ||||
|       await test.step('Edit sweep via feature tree selection works', async () => { | ||||
|         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) | ||||
|         const operationButton = await toolbar.getFeatureTreeOperation( | ||||
|           'Sweep', | ||||
|           0 | ||||
|         ) | ||||
|         await operationButton.click({ button: 'left' }) | ||||
|         await page.keyboard.press('Delete') | ||||
|         await page.waitForTimeout(500) | ||||
| @ -1427,6 +1494,7 @@ sketch002 = startSketchOn('XZ') | ||||
|         await scene.expectPixelColor([53, 53, 53], testPoint, 15) | ||||
|       }) | ||||
|     }) | ||||
|   }) | ||||
|  | ||||
|   test(`Sweep point-and-click failing validation`, async ({ | ||||
|     context, | ||||
| @ -1473,6 +1541,7 @@ sketch002 = startSketchOn('XZ') | ||||
|         currentArgKey: 'target', | ||||
|         currentArgValue: '', | ||||
|         headerArguments: { | ||||
|           Sectional: '', | ||||
|           Target: '', | ||||
|           Trajectory: '', | ||||
|         }, | ||||
| @ -1485,6 +1554,7 @@ sketch002 = startSketchOn('XZ') | ||||
|         currentArgKey: 'trajectory', | ||||
|         currentArgValue: '', | ||||
|         headerArguments: { | ||||
|           Sectional: '', | ||||
|           Target: '1 face', | ||||
|           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![] | ||||
|     }; | ||||
|  | ||||
|     // 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 | ||||
|         .iter() | ||||
|         .filter(|face_info| face_info.cap == ExtrusionFaceCapType::None) | ||||
| @ -215,6 +230,7 @@ pub(crate) async fn do_post_extrude<'a>( | ||||
|                 None | ||||
|             } | ||||
|         }) | ||||
|         .take(yielded_sides_count) | ||||
|     { | ||||
|         // Batch these commands, because the Rust code doesn't actually care about the outcome. | ||||
|         // So, there's no need to await them. | ||||
|  | ||||
| @ -515,30 +515,54 @@ export function addShell({ | ||||
|   } | ||||
| } | ||||
|  | ||||
| export function addSweep( | ||||
|   node: Node<Program>, | ||||
|   profileDeclarator: VariableDeclarator, | ||||
|   pathDeclarator: VariableDeclarator | ||||
| ): { | ||||
| export function addSweep({ | ||||
|   node, | ||||
|   targetDeclarator, | ||||
|   trajectoryDeclarator, | ||||
|   sectional, | ||||
|   variableName, | ||||
|   insertIndex, | ||||
| }: { | ||||
|   node: Node<Program> | ||||
|   targetDeclarator: VariableDeclarator | ||||
|   trajectoryDeclarator: VariableDeclarator | ||||
|   sectional: boolean | ||||
|   variableName?: string | ||||
|   insertIndex?: number | ||||
| }): { | ||||
|   modifiedAst: Node<Program> | ||||
|   pathToNode: PathToNode | ||||
| } { | ||||
|   const modifiedAst = structuredClone(node) | ||||
|   const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SWEEP) | ||||
|   const sweep = createCallExpressionStdLibKw( | ||||
|   const name = | ||||
|     variableName ?? findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SWEEP) | ||||
|   const call = createCallExpressionStdLibKw( | ||||
|     'sweep', | ||||
|     createIdentifier(profileDeclarator.id.name), | ||||
|     [createLabeledArg('path', createIdentifier(pathDeclarator.id.name))] | ||||
|     createIdentifier(targetDeclarator.id.name), | ||||
|     [ | ||||
|       createLabeledArg('path', createIdentifier(trajectoryDeclarator.id.name)), | ||||
|       createLabeledArg('sectional', createLiteral(sectional)), | ||||
|     ] | ||||
|   ) | ||||
|   const declaration = createVariableDeclaration(name, sweep) | ||||
|   modifiedAst.body.push(declaration) | ||||
|   const variable = createVariableDeclaration(name, call) | ||||
|   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 = [ | ||||
|     ['body', ''], | ||||
|     [modifiedAst.body.length - 1, 'index'], | ||||
|     [insertAt, 'index'], | ||||
|     ['declaration', 'VariableDeclaration'], | ||||
|     ['init', 'VariableDeclarator'], | ||||
|     ['arguments', 'CallExpressionKw'], | ||||
|     [0, ARG_INDEX_FIELD], | ||||
|     [argIndex, ARG_INDEX_FIELD], | ||||
|     ['arg', LABELED_ARG_FIELD], | ||||
|   ] | ||||
|  | ||||
|  | ||||
| @ -47,8 +47,12 @@ export type ModelingCommandSchema = { | ||||
|     distance: KclCommandValue | ||||
|   } | ||||
|   Sweep: { | ||||
|     // Enables editing workflow | ||||
|     nodeToEdit?: PathToNode | ||||
|     // Arguments | ||||
|     target: Selections | ||||
|     trajectory: Selections | ||||
|     sectional: boolean | ||||
|   } | ||||
|   Loft: { | ||||
|     selection: Selections | ||||
| @ -345,22 +349,40 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< | ||||
|     description: | ||||
|       'Create a 3D body by moving a sketch region along an arbitrary path.', | ||||
|     icon: 'sweep', | ||||
|     needsReview: false, | ||||
|     needsReview: true, | ||||
|     args: { | ||||
|       nodeToEdit: { | ||||
|         description: | ||||
|           'Path to the node in the AST to edit. Never shown to the user.', | ||||
|         skip: true, | ||||
|         inputType: 'text', | ||||
|         required: false, | ||||
|       }, | ||||
|       target: { | ||||
|         inputType: 'selection', | ||||
|         selectionTypes: ['solid2d'], | ||||
|         required: true, | ||||
|         skip: true, | ||||
|         multiple: false, | ||||
|         hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit), | ||||
|       }, | ||||
|       trajectory: { | ||||
|         inputType: 'selection', | ||||
|         selectionTypes: ['segment', 'path'], | ||||
|         selectionTypes: ['segment'], | ||||
|         required: true, | ||||
|         skip: false, | ||||
|         skip: true, | ||||
|         multiple: false, | ||||
|         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 baseCommand = { | ||||
|     name: 'Helix', | ||||
| @ -511,6 +648,7 @@ export const stdLibMap: Record<string, StdLibCallInfo> = { | ||||
|   sweep: { | ||||
|     label: 'Sweep', | ||||
|     icon: 'sweep', | ||||
|     prepareToEdit: prepareToEditSweep, | ||||
|     supportsAppearance: true, | ||||
|   }, | ||||
| } | ||||
|  | ||||
| @ -78,7 +78,7 @@ import { | ||||
| import { ModelingCommandSchema } from 'lib/commandBarConfigs/modelingCommandConfig' | ||||
| import { err, reportRejection, trap } from 'lib/trap' | ||||
| import { DefaultPlaneStr } from 'lib/planes' | ||||
| import { uuidv4 } from 'lib/utils' | ||||
| import { isArray, uuidv4 } from 'lib/utils' | ||||
| import { Coords2d } from 'lang/std/sketch' | ||||
| import { deleteSegment } from 'clientSideScene/ClientSideSceneComp' | ||||
| import toast from 'react-hot-toast' | ||||
| @ -97,6 +97,7 @@ import { createProfileStartHandle } from 'clientSideScene/segments' | ||||
| import { DRAFT_POINT } from 'clientSideScene/sceneInfra' | ||||
| import { setAppearance } from 'lang/modifyAst/setAppearance' | ||||
| import { DRAFT_DASHED_LINE } from 'clientSideScene/sceneEntities' | ||||
| import { updateModelingState } from 'lang/modelingWorkflows' | ||||
|  | ||||
| export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY' | ||||
|  | ||||
| @ -1988,55 +1989,88 @@ export const modelingMachine = setup({ | ||||
|         if (!input) return new Error('No input provided') | ||||
|         // Extract inputs | ||||
|         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( | ||||
|           ast, | ||||
|           target.graphSelections[0].codeRef.range | ||||
|         ) | ||||
|         const targetNode = getNodeFromPath<VariableDeclarator>( | ||||
|           ast, | ||||
|           targetNodePath, | ||||
|           'VariableDeclarator' | ||||
|         ) | ||||
|         // Gotchas, not sure why | ||||
|         // - it seems like in some cases we get a list on edit, especially the state that e2e hits | ||||
|         // - looking for a VariableDeclaration seems more robust than VariableDeclarator | ||||
|         const targetNode = getNodeFromPath< | ||||
|           VariableDeclaration | VariableDeclaration[] | ||||
|         >(ast, targetNodePath, 'VariableDeclaration') | ||||
|         if (err(targetNode)) { | ||||
|           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( | ||||
|           ast, | ||||
|           trajectory.graphSelections[0].codeRef.range | ||||
|         ) | ||||
|         const trajectoryNode = getNodeFromPath<VariableDeclarator>( | ||||
|         // Also looking for VariableDeclaration for consistency here | ||||
|         const trajectoryNode = getNodeFromPath<VariableDeclaration>( | ||||
|           ast, | ||||
|           trajectoryNodePath, | ||||
|           'VariableDeclarator' | ||||
|           'VariableDeclaration' | ||||
|         ) | ||||
|         if (err(trajectoryNode)) { | ||||
|           return new Error("Couldn't parse path selection") | ||||
|         } | ||||
|         const trajectoryDeclarator = trajectoryNode.node | ||||
|  | ||||
|         const trajectoryDeclarator = trajectoryNode.node.declaration | ||||
|  | ||||
|         // Perform the sweep | ||||
|         const sweepRes = addSweep(ast, targetDeclarator, trajectoryDeclarator) | ||||
|         const updateAstResult = await kclManager.updateAst( | ||||
|           sweepRes.modifiedAst, | ||||
|           true, | ||||
|         const { modifiedAst, pathToNode } = addSweep({ | ||||
|           node: ast, | ||||
|           targetDeclarator, | ||||
|           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( | ||||
|  | ||||
