diff --git a/e2e/playwright/fixtures/sceneFixture.ts b/e2e/playwright/fixtures/sceneFixture.ts index c0f84bd2b..59426cb48 100644 --- a/e2e/playwright/fixtures/sceneFixture.ts +++ b/e2e/playwright/fixtures/sceneFixture.ts @@ -216,7 +216,7 @@ export class SceneFixture { } expectPixelColor = async ( - colour: [number, number, number], + colour: [number, number, number] | [number, number, number][], coords: { x: number; y: number }, diff: number ) => { @@ -237,22 +237,36 @@ export class SceneFixture { } } +function isColourArray( + colour: [number, number, number] | [number, number, number][] +): colour is [number, number, number][] { + return Array.isArray(colour[0]) +} + export async function expectPixelColor( page: Page, - colour: [number, number, number], + colour: [number, number, number] | [number, number, number][], coords: { x: number; y: number }, diff: number ) { let finalValue = colour await expect - .poll(async () => { - const pixel = (await getPixelRGBs(page)(coords, 1))[0] - if (!pixel) return null - finalValue = pixel - return pixel.every( - (channel, index) => Math.abs(channel - colour[index]) < diff - ) - }) + .poll( + async () => { + const pixel = (await getPixelRGBs(page)(coords, 1))[0] + if (!pixel) return null + finalValue = pixel + if (!isColourArray(colour)) { + return pixel.every( + (channel, index) => Math.abs(channel - colour[index]) < diff + ) + } + return colour.some((c) => + c.every((channel, index) => Math.abs(pixel[index] - channel) < diff) + ) + }, + { timeout: 10_000 } + ) .toBeTruthy() .catch((cause) => { throw new Error( diff --git a/e2e/playwright/sketch-tests.spec.ts b/e2e/playwright/sketch-tests.spec.ts index 5f42547ca..6c16fbb78 100644 --- a/e2e/playwright/sketch-tests.spec.ts +++ b/e2e/playwright/sketch-tests.spec.ts @@ -7,6 +7,7 @@ import { PERSIST_MODELING_CONTEXT, setup, tearDown, + TEST_COLORS, } from './test-utils' import { uuidv4, roundOff } from 'lib/utils' @@ -1350,7 +1351,7 @@ test2.describe('Sketch mode should be toleratant to syntax errors', () => { const [objClick] = scene.makeMouseHelpers(600, 250) const arrowHeadLocation = { x: 604, y: 129 } as const - const arrowHeadWhite: [number, number, number] = [255, 255, 255] + const arrowHeadWhite = TEST_COLORS.WHITE const backgroundGray: [number, number, number] = [28, 28, 28] const verifyArrowHeadColor = async (c: [number, number, number]) => scene.expectPixelColor(c, arrowHeadLocation, 15) @@ -1993,4 +1994,334 @@ extrude001 = extrude(75, thePart) ) } ) + test2( + 'Can enter sketch on sketch of wall and cap for segment, solid2d, extrude-wall, extrude-cap selections', + async ({ app, scene, toolbar, editor }) => { + // TODO this test should include a test for selecting revolve walls and caps + await app.initialise(`sketch001 = startSketchOn('XZ') +profile001 = startProfileAt([6.71, -3.66], sketch001) + |> line([2.65, 9.02], %, $seg02) + |> line([3.73, -9.36], %, $seg01) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +extrude001 = extrude(20, profile001) +sketch002 = startSketchOn(extrude001, seg01) +profile002 = startProfileAt([0.75, 13.46], sketch002) + |> line([4.52, 3.79], %) + |> line([5.98, -2.81], %) +profile003 = startProfileAt([3.19, 13.3], sketch002) + |> angledLine([0, 6.64], %, $rectangleSegmentA001) + |> angledLine([ + segAng(rectangleSegmentA001) - 90, + 2.81 + ], %) + |> angledLine([ + segAng(rectangleSegmentA001), + -segLen(rectangleSegmentA001) + ], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +profile004 = startProfileAt([3.15, 9.39], sketch002) + |> xLine(6.92, %) + |> line([-7.41, -2.85], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +profile005 = circle({ center = [5.15, 4.34], radius = 1.66 }, sketch002) +profile006 = startProfileAt([9.65, 3.82], sketch002) + |> line([2.38, 5.62], %) + |> line([2.13, -5.57], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +revolve001 = revolve({ + angle = 45, + axis = getNextAdjacentEdge(seg01) +}, profile004) +extrude002 = extrude(4, profile006) +sketch003 = startSketchOn('-XZ') +profile007 = startProfileAt([4.8, 7.55], sketch003) + |> line([7.39, 2.58], %) + |> line([7.02, -2.85], %) +profile008 = startProfileAt([5.54, 5.49], sketch003) + |> line([6.34, 2.64], %) + |> line([6.33, -2.96], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +profile009 = startProfileAt([5.23, 1.95], sketch003) + |> line([6.8, 2.17], %) + |> line([7.34, -2.75], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +profile010 = circle({ + center = [7.18, -2.11], + radius = 2.67 +}, sketch003) +profile011 = startProfileAt([5.07, -6.39], sketch003) + |> angledLine([0, 4.54], %, $rectangleSegmentA002) + |> angledLine([ + segAng(rectangleSegmentA002) - 90, + 4.17 + ], %) + |> angledLine([ + segAng(rectangleSegmentA002), + -segLen(rectangleSegmentA002) + ], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +extrude003 = extrude(2.5, profile011) +revolve002 = revolve({ angle = 45, axis = seg02 }, profile008) +`) + + const camPositionForSelectingSketchOnWallProfiles = () => + scene.moveCameraTo( + { x: 834, y: -680, z: 534 }, + { x: -54, y: -476, z: 148 } + ) + const camPositionForSelectingSketchOnCapProfiles = () => + scene.moveCameraTo( + { x: 404, y: 690, z: 38 }, + { x: 16, y: -140, z: -10 } + ) + const wallSelectionOptions = [ + { + title: 'select wall segment', + selectClick: scene.makeMouseHelpers(598, 211)[0], + }, + { + title: 'select wall solid 2d', + selectClick: scene.makeMouseHelpers(677, 236)[0], + }, + { + title: 'select wall circle', + selectClick: scene.makeMouseHelpers(811, 247)[0], + }, + { + title: 'select wall extrude wall', + selectClick: scene.makeMouseHelpers(793, 136)[0], + }, + { + title: 'select wall extrude cap', + selectClick: scene.makeMouseHelpers(836, 103)[0], + }, + ] as const + const capSelectionOptions = [ + { + title: 'select cap segment', + selectClick: scene.makeMouseHelpers(688, 91)[0], + }, + { + title: 'select cap solid 2d', + selectClick: scene.makeMouseHelpers(733, 204)[0], + }, + // TODO keeps failing + // { + // title: 'select cap circle', + // selectClick: scene.makeMouseHelpers(679, 290)[0], + // }, + { + title: 'select cap extrude wall', + selectClick: scene.makeMouseHelpers(649, 402)[0], + }, + { + title: 'select cap extrude cap', + selectClick: scene.makeMouseHelpers(693, 408)[0], + }, + ] as const + + const verifyWallProfilesAreDrawn = async () => + test2.step('verify wall profiles are drawn', async () => { + // open polygon + await scene.expectPixelColor( + TEST_COLORS.WHITE, + { x: 599, y: 168 }, + 15 + ) + // closed polygon + await scene.expectPixelColor( + TEST_COLORS.WHITE, + { x: 656, y: 171 }, + 15 + ) + // revolved profile + await scene.expectPixelColor( + TEST_COLORS.WHITE, + { x: 655, y: 264 }, + 15 + ) + // extruded profile + await scene.expectPixelColor( + TEST_COLORS.WHITE, + { x: 808, y: 396 }, + 15 + ) + // circle + await scene.expectPixelColor( + [ + TEST_COLORS.WHITE, + TEST_COLORS.BLUE, // When entering via the circle, it's selected and therefore blue + ], + { x: 742, y: 386 }, + 15 + ) + }) + + const verifyCapProfilesAreDrawn = async () => + test2.step('verify wall profiles are drawn', async () => { + // open polygon + await scene.expectPixelColor( + TEST_COLORS.WHITE, + // TEST_COLORS.BLUE, // When entering via the circle, it's selected and therefore blue + { x: 620, y: 58 }, + 15 + ) + // revolved profile + await scene.expectPixelColor( + TEST_COLORS.WHITE, + { x: 641, y: 110 }, + 15 + ) + // closed polygon + await scene.expectPixelColor( + TEST_COLORS.WHITE, + { x: 632, y: 200 }, + 15 + ) + // extruded profile + await scene.expectPixelColor( + TEST_COLORS.WHITE, + { x: 628, y: 410 }, + 15 + ) + // circle + await scene.expectPixelColor( + [ + TEST_COLORS.WHITE, + TEST_COLORS.BLUE, // When entering via the circle, it's selected and therefore blue + ], + { x: 681, y: 303 }, + 15 + ) + }) + + await test2.step('select wall profiles', async () => { + for (const { title, selectClick } of wallSelectionOptions) { + await test2.step(title, async () => { + await camPositionForSelectingSketchOnWallProfiles() + await selectClick() + await toolbar.editSketch() + await app.page.waitForTimeout(600) + await verifyWallProfilesAreDrawn() + await toolbar.exitSketchBtn.click() + await app.page.waitForTimeout(100) + }) + } + }) + + await test2.step('select cap profiles', async () => { + for (const { title, selectClick } of capSelectionOptions) { + await test2.step(title, async () => { + await camPositionForSelectingSketchOnCapProfiles() + await app.page.waitForTimeout(100) + await selectClick() + await app.page.waitForTimeout(100) + await toolbar.editSketch() + await app.page.waitForTimeout(600) + await verifyCapProfilesAreDrawn() + await toolbar.exitSketchBtn.click() + await app.page.waitForTimeout(100) + }) + } + }) + } + ) + test2( + 'Can enter sketch loft edges, base and continue sketch', + async ({ app, scene, toolbar, editor }) => { + await app.initialise(`sketch001 = startSketchOn('XZ') +profile001 = startProfileAt([34, 42.66], sketch001) + |> line([102.65, 151.99], %) + |> line([76, -138.66], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +plane001 = offsetPlane('XZ', 50) +sketch002 = startSketchOn(plane001) +profile002 = startProfileAt([39.43, 172.21], sketch002) + |> xLine(183.99, %) + |> line([-77.95, -145.93], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) + +loft([profile001, profile002]) +`) + const [baseProfileEdgeClick] = scene.makeMouseHelpers(621, 292) + + const [rect1Crn1] = scene.makeMouseHelpers(592, 283) + const [rect1Crn2] = scene.makeMouseHelpers(797, 268) + + await baseProfileEdgeClick() + await toolbar.editSketch() + await app.page.waitForTimeout(600) + await scene.expectPixelColor(TEST_COLORS.WHITE, { x: 562, y: 172 }, 15) + + await toolbar.rectangleBtn.click() + await app.page.waitForTimeout(100) + await rect1Crn1() + await editor.expectEditor.toContain( + `profile003 = startProfileAt([50.72, -18.19], sketch001)` + ) + await rect1Crn2() + await editor.expectEditor.toContain( + `angledLine([0, 113.01], %, $rectangleSegmentA001)` + ) + } + ) + test2( + 'Can enter sketch loft edges offsetPlane and continue sketch', + async ({ app, scene, toolbar, editor }) => { + await app.initialise(`sketch001 = startSketchOn('XZ') +profile001 = startProfileAt([34, 42.66], sketch001) + |> line([102.65, 151.99], %) + |> line([76, -138.66], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +plane001 = offsetPlane('XZ', 50) +sketch002 = startSketchOn(plane001) +profile002 = startProfileAt([39.43, 172.21], sketch002) + |> xLine(183.99, %) + |> line([-77.95, -145.93], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) + +loft([profile001, profile002]) +`) + const topProfileEdgeClickCoords = { x: 602, y: 185 } as const + const [topProfileEdgeClick] = scene.makeMouseHelpers( + topProfileEdgeClickCoords.x, + topProfileEdgeClickCoords.y + ) + + const [rect1Crn1] = scene.makeMouseHelpers(592, 283) + const [rect1Crn2] = scene.makeMouseHelpers(797, 268) + + await scene.moveCameraTo( + { x: 8171, y: -7740, z: 1624 }, + { x: 3302, y: -627, z: 2892 } + ) + + await topProfileEdgeClick() + await toolbar.editSketch() + await app.page.waitForTimeout(600) + await scene.expectPixelColor(TEST_COLORS.BLUE, { x: 788, y: 188 }, 15) + + await toolbar.rectangleBtn.click() + await app.page.waitForTimeout(100) + await rect1Crn1() + await editor.expectEditor.toContain( + `profile003 = startProfileAt([47.76, -17.13], plane001)` + ) + await rect1Crn2() + await editor.expectEditor.toContain( + `angledLine([0, 106.42], %, $rectangleSegmentA001)` + ) + } + ) }) diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/code-color-goober-code-color-goober-opening-window-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/code-color-goober-code-color-goober-opening-window-1-Google-Chrome-linux.png index 5f235e51c..c01783eb2 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/code-color-goober-code-color-goober-opening-window-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/code-color-goober-code-color-goober-opening-window-1-Google-Chrome-linux.png differ diff --git a/src/clientSideScene/sceneEntities.ts b/src/clientSideScene/sceneEntities.ts index d5332d610..30cb5ac12 100644 --- a/src/clientSideScene/sceneEntities.ts +++ b/src/clientSideScene/sceneEntities.ts @@ -1776,8 +1776,7 @@ export class SceneEntities { structuredClone(pathToNode) nodePathWithCorrectedIndexForTruncatedAst[1][0] = Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) - - Number(planeNodePath[1][0]) - - 1 + Number(sketchNodePaths[0][1][0]) const _node = getNodeFromPath>( modifiedAst, diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx index dea0c20fd..63a5b356f 100644 --- a/src/components/Spinner.tsx +++ b/src/components/Spinner.tsx @@ -2,7 +2,12 @@ import { SVGProps } from 'react' export const Spinner = (props: SVGProps) => { return ( - + , pathToSketchNode: PathToNode, - shouldPipe = false, angle: Expr = createLiteral(4), - axis: Selections + axis: Selections, + artifact?: Artifact ): | { modifiedAst: Node @@ -37,6 +36,11 @@ export function revolveSketch( pathToRevolveArg: PathToNode } | Error { + const orderedSketchNodePaths = getPathsFromArtifact({ + artifact: artifact, + sketchPathToNode: pathToSketchNode, + }) + if (err(orderedSketchNodePaths)) return orderedSketchNodePaths const clonedAst = structuredClone(ast) const sketchNode = getNodeFromPath(clonedAst, pathToSketchNode) if (err(sketchNode)) return sketchNode @@ -67,29 +71,13 @@ export function revolveSketch( if (err(tagResult)) return tagResult const { tag } = tagResult - /* Original Code */ - const { node: sketchExpression } = sketchNode - - // determine if sketchExpression is in a pipeExpression or not - const sketchPipeExpressionNode = getNodeFromPath( - clonedAst, - pathToSketchNode, - 'PipeExpression' - ) - if (err(sketchPipeExpressionNode)) return sketchPipeExpressionNode - const { node: sketchPipeExpression } = sketchPipeExpressionNode - const isInPipeExpression = sketchPipeExpression.type === 'PipeExpression' - const sketchVariableDeclaratorNode = getNodeFromPath( clonedAst, pathToSketchNode, 'VariableDeclarator' ) if (err(sketchVariableDeclaratorNode)) return sketchVariableDeclaratorNode - const { - node: sketchVariableDeclarator, - shallowPath: sketchPathToDecleration, - } = sketchVariableDeclaratorNode + const { node: sketchVariableDeclarator } = sketchVariableDeclaratorNode const axisSelection = axis?.graphSelections[0]?.artifact @@ -103,37 +91,13 @@ export function revolveSketch( createIdentifier(sketchVariableDeclarator.id.name), ]) - if (shouldPipe) { - const pipeChain = createPipeExpression( - isInPipeExpression - ? [...sketchPipeExpression.body, revolveCall] - : [sketchExpression as any, revolveCall] - ) - - sketchVariableDeclarator.init = pipeChain - const pathToRevolveArg: PathToNode = [ - ...sketchPathToDecleration, - ['init', 'VariableDeclarator'], - ['body', ''], - [pipeChain.body.length - 1, 'index'], - ['arguments', 'CallExpression'], - [0, 'index'], - ] - - return { - modifiedAst: clonedAst, - pathToSketchNode, - pathToRevolveArg, - } - } - // We're not creating a pipe expression, // but rather a separate constant for the extrusion const name = findUniqueName(clonedAst, KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE) const VariableDeclaration = createVariableDeclaration(name, revolveCall) - const sketchIndexInPathToNode = - sketchPathToDecleration.findIndex((a) => a[0] === 'body') + 1 - const sketchIndexInBody = sketchPathToDecleration[sketchIndexInPathToNode][0] + const lastSketchNodePath = + orderedSketchNodePaths[orderedSketchNodePaths.length - 1] + const sketchIndexInBody = Number(lastSketchNodePath[1][0]) if (typeof sketchIndexInBody !== 'number') return new Error('expected sketchIndexInBody to be a number') clonedAst.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration) diff --git a/src/lang/std/__snapshots__/artifactGraph.test.ts.snap b/src/lang/std/__snapshots__/artifactGraph.test.ts.snap index 0cbb1ebd8..db2304b74 100644 --- a/src/lang/std/__snapshots__/artifactGraph.test.ts.snap +++ b/src/lang/std/__snapshots__/artifactGraph.test.ts.snap @@ -212,19 +212,7 @@ Map { "type": "wall", }, "UUID-10" => { - "codeRef": { - "pathToNode": [ - [ - "body", - "", - ], - ], - "range": [ - 501, - 522, - true, - ], - }, + "codeRef": undefined, "edgeCutEdgeIds": [], "id": "UUID", "pathIds": [ diff --git a/src/lang/std/artifactGraph.test.ts b/src/lang/std/artifactGraph.test.ts index 2870fe42a..1c75c1a6e 100644 --- a/src/lang/std/artifactGraph.test.ts +++ b/src/lang/std/artifactGraph.test.ts @@ -22,6 +22,7 @@ import * as d3 from 'd3-force' import path from 'path' import pixelmatch from 'pixelmatch' import { PNG } from 'pngjs' +import { Node } from 'wasm-lib/kcl/bindings/Node' /* Note this is an integration test, these tests connect to our real dev server and make websocket commands. @@ -171,7 +172,7 @@ afterAll(() => { describe('testing createArtifactGraph', () => { describe('code with offset planes and a sketch:', () => { - let ast: Program + let ast: Node let theMap: ReturnType it('setup', () => { @@ -217,7 +218,7 @@ describe('testing createArtifactGraph', () => { }) }) describe('code with an extrusion, fillet and sketch of face:', () => { - let ast: Program + let ast: Node let theMap: ReturnType it('setup', () => { // putting this logic in here because describe blocks runs before beforeAll has finished @@ -312,7 +313,7 @@ describe('testing createArtifactGraph', () => { }) describe(`code with sketches but no extrusions or other 3D elements`, () => { - let ast: Program + let ast: Node let theMap: ReturnType it(`setup`, () => { // putting this logic in here because describe blocks runs before beforeAll has finished @@ -377,7 +378,7 @@ describe('testing createArtifactGraph', () => { describe('capture graph of sketchOnFaceOnFace...', () => { describe('code with an extrusion, fillet and sketch of face:', () => { - let ast: Program + let ast: Node let theMap: ReturnType it('setup', async () => { // putting this logic in here because describe blocks runs before beforeAll has finished @@ -399,7 +400,9 @@ describe('capture graph of sketchOnFaceOnFace...', () => { }) }) -function getCommands(codeKey: CodeKey): CacheShape[CodeKey] & { ast: Program } { +function getCommands( + codeKey: CodeKey +): CacheShape[CodeKey] & { ast: Node } { const ast = assertParse(codeKey) const file = fs.readFileSync(fullPath, 'utf-8') const parsed: CacheShape = JSON.parse(file) diff --git a/src/lang/std/artifactGraph.ts b/src/lang/std/artifactGraph.ts index 390f4f28a..9ecab816d 100644 --- a/src/lang/std/artifactGraph.ts +++ b/src/lang/std/artifactGraph.ts @@ -1,8 +1,19 @@ -import { Expr, PathToNode, Program, SourceRange } from 'lang/wasm' +import { + Expr, + PathToNode, + Program, + SourceRange, + VariableDeclaration, +} from 'lang/wasm' import { Models } from '@kittycad/lib' -import { getNodePathFromSourceRange } from 'lang/queryAst' +import { + getNodeFromPath, + getNodePathFromSourceRange, + traverse, +} from 'lang/queryAst' import { err } from 'lib/trap' import { engineCommandManager, kclManager } from 'lib/singletons' +import { Node } from 'wasm-lib/kcl/bindings/Node' export type ArtifactId = string @@ -42,7 +53,7 @@ interface Solid2DArtifact extends BaseArtifact { export interface PathArtifactRich extends BaseArtifact { type: 'path' /** A path must always lie on a plane */ - plane: PlaneArtifact | WallArtifact + plane: PlaneArtifact | WallArtifact | CapArtifact /** A path must always contain 0 or more segments */ segments: Array /** A path may not result in a sweep artifact */ @@ -101,6 +112,9 @@ interface CapArtifact extends BaseArtifact { edgeCutEdgeIds: Array sweepId: ArtifactId pathIds: Array + // codeRef is for the sketchOnFace plane, not for the wall itself + // traverse to the extrude and or segment to get the wall's codeRef + codeRef?: CodeRef } interface SweepEdgeArtifact extends BaseArtifact { @@ -163,7 +177,7 @@ export function createArtifactGraph({ }: { orderedCommands: Array responseMap: ResponseMap - ast: Program + ast: Node }) { const myMap = new Map() @@ -242,7 +256,7 @@ export function getArtifactsToUpdate({ /** Passing in a getter because we don't wan this function to update the map directly */ getArtifact: (id: ArtifactId) => Artifact | undefined currentPlaneId: ArtifactId - ast: Program + ast: Node }): Array<{ id: ArtifactId artifact: Artifact @@ -278,6 +292,13 @@ export function getArtifactsToUpdate({ plane?.type === 'plane' ? plane?.codeRef : { range, pathToNode } const existingPlane = getArtifact(currentPlaneId) if (existingPlane?.type === 'wall') { + let existingPlaneCodeRef = existingPlane.codeRef + if (!existingPlaneCodeRef) { + const astWalkCodeRef = getWallOrCapPlaneCodeRef(ast, codeRef.pathToNode) + if (!err(astWalkCodeRef)) { + existingPlaneCodeRef = astWalkCodeRef + } + } return [ { id: currentPlaneId, @@ -288,7 +309,29 @@ export function getArtifactsToUpdate({ edgeCutEdgeIds: existingPlane.edgeCutEdgeIds, sweepId: existingPlane.sweepId, pathIds: existingPlane.pathIds, - codeRef, + codeRef: existingPlaneCodeRef, + }, + }, + ] + } else if (existingPlane?.type === 'cap') { + let existingPlaneCodeRef = existingPlane.codeRef + if (!existingPlaneCodeRef) { + const astWalkCodeRef = getWallOrCapPlaneCodeRef(ast, codeRef.pathToNode) + if (!err(astWalkCodeRef)) { + existingPlaneCodeRef = astWalkCodeRef + } + } + return [ + { + id: currentPlaneId, + artifact: { + type: 'cap', + subType: existingPlane.subType, + id: currentPlaneId, + edgeCutEdgeIds: existingPlane.edgeCutEdgeIds, + sweepId: existingPlane.sweepId, + pathIds: existingPlane.pathIds, + codeRef: existingPlaneCodeRef, }, }, ] @@ -333,6 +376,18 @@ export function getArtifactsToUpdate({ pathIds: [id], }, }) + } else if (plane?.type === 'cap') { + returnArr.push({ + id: currentPlaneId, + artifact: { + type: 'cap', + id: currentPlaneId, + subType: plane.subType, + edgeCutEdgeIds: plane.edgeCutEdgeIds, + sweepId: plane.sweepId, + pathIds: [id], + }, + }) } return returnArr } else if (cmd.type === 'extend_path' || cmd.type === 'close_path') { @@ -880,9 +935,9 @@ export function codeRefFromRange(range: SourceRange, ast: Program): CodeRef { function getPlaneFromPath( path: PathArtifact, graph: ArtifactGraph -): PlaneArtifact | WallArtifact | Error { +): PlaneArtifact | WallArtifact | CapArtifact | Error { const plane = getArtifactOfTypes( - { key: path.planeId, types: ['plane', 'wall'] }, + { key: path.planeId, types: ['plane', 'wall', 'cap'] }, graph ) if (err(plane)) return plane @@ -892,7 +947,7 @@ function getPlaneFromPath( function getPlaneFromSegment( segment: SegmentArtifact, graph: ArtifactGraph -): PlaneArtifact | WallArtifact | Error { +): PlaneArtifact | WallArtifact | CapArtifact | Error { const path = getArtifactOfTypes( { key: segment.pathId, types: ['path'] }, graph @@ -903,7 +958,7 @@ function getPlaneFromSegment( function getPlaneFromSolid2D( solid2D: Solid2DArtifact, graph: ArtifactGraph -): PlaneArtifact | WallArtifact | Error { +): PlaneArtifact | WallArtifact | CapArtifact | Error { const path = getArtifactOfTypes( { key: solid2D.pathId, types: ['path'] }, graph @@ -914,7 +969,7 @@ function getPlaneFromSolid2D( function getPlaneFromCap( cap: CapArtifact, graph: ArtifactGraph -): PlaneArtifact | WallArtifact | Error { +): PlaneArtifact | WallArtifact | CapArtifact | Error { const sweep = getArtifactOfTypes( { key: cap.sweepId, types: ['sweep'] }, graph @@ -927,7 +982,7 @@ function getPlaneFromCap( function getPlaneFromWall( wall: WallArtifact, graph: ArtifactGraph -): PlaneArtifact | WallArtifact | Error { +): PlaneArtifact | WallArtifact | CapArtifact | Error { const sweep = getArtifactOfTypes( { key: wall.sweepId, types: ['sweep'] }, graph @@ -951,7 +1006,7 @@ function getPlaneFromSweepEdge(edge: SweepEdgeArtifact, graph: ArtifactGraph) { export function getPlaneFromArtifact( artifact: Artifact | undefined, graph: ArtifactGraph -): PlaneArtifact | WallArtifact | Error { +): PlaneArtifact | WallArtifact | CapArtifact | Error { if (!artifact) return new Error(`Artifact is undefined`) if (artifact.type === 'plane') return artifact if (artifact.type === 'path') return getPlaneFromPath(artifact, graph) @@ -1075,3 +1130,82 @@ function isNodeSafe(node: Expr): boolean { } return false } + +/** {@deprecated} this information should come from the ArtifactGraph not digging around in the AST */ +function getWallOrCapPlaneCodeRef( + ast: Node, + pathToNode: PathToNode +): CodeRef | Error { + const varDec = getNodeFromPath( + ast, + pathToNode, + 'VariableDeclaration' + ) + if (err(varDec)) return varDec + if (varDec.node.type !== 'VariableDeclaration') + return new Error('Expected VariableDeclaration') + const init = varDec.node.declaration.init + let varName = '' + if ( + init.type === 'CallExpression' && + init.callee.type === 'Identifier' && + (init.callee.name === 'circle' || init.callee.name === 'startProfileAt') + ) { + const secondArg = init.arguments[1] + if (secondArg.type === 'Identifier') { + varName = secondArg.name + } + } else if (init.type === 'PipeExpression') { + const firstExpr = init.body[0] + if ( + firstExpr.type === 'CallExpression' && + firstExpr.callee.type === 'Identifier' && + firstExpr.callee.name === 'startProfileAt' + ) { + const secondArg = firstExpr.arguments[1] + if (secondArg.type === 'Identifier') { + varName = secondArg.name + } + } + } + if (varName === '') return new Error('Could not find variable name') + + let currentVariableName = '' + const planeCodeRef: Array<{ + path: PathToNode + sketchName: string + range: SourceRange + }> = [] + traverse(ast, { + leave: (node) => { + if (node.type === 'VariableDeclaration') { + currentVariableName = '' + } + }, + enter: (node, path) => { + if (node.type === 'VariableDeclaration') { + currentVariableName = node.declaration.id.name + } + if ( + // match `${varName} = startSketchOn(...)` + node.type === 'CallExpression' && + node.callee.name === 'startSketchOn' && + node.arguments[0].type === 'Identifier' && + currentVariableName === varName + ) { + planeCodeRef.push({ + path, + sketchName: currentVariableName, + range: [node.start, node.end, true], + }) + } + }, + }) + if (!planeCodeRef.length) + return new Error('No paths found depending on extrude') + + return { + pathToNode: planeCodeRef[0].path, + range: planeCodeRef[0].range, + } +} diff --git a/src/lang/std/artifactMapGraphs/sketchOnFaceOnFaceEtc.png b/src/lang/std/artifactMapGraphs/sketchOnFaceOnFaceEtc.png index 1bec224d5..f24510872 100644 Binary files a/src/lang/std/artifactMapGraphs/sketchOnFaceOnFaceEtc.png and b/src/lang/std/artifactMapGraphs/sketchOnFaceOnFaceEtc.png differ diff --git a/src/lang/std/engineConnection.ts b/src/lang/std/engineConnection.ts index c40292b65..a59131744 100644 --- a/src/lang/std/engineConnection.ts +++ b/src/lang/std/engineConnection.ts @@ -37,6 +37,7 @@ import { KclManager } from 'lang/KclSingleton' import { reportRejection } from 'lib/trap' import { markOnce } from 'lib/performance' import { MachineManager } from 'components/MachineManagerProvider' +import { Node } from 'wasm-lib/kcl/bindings/Node' // TODO(paultag): This ought to be tweakable. const pingIntervalMs = 5_000 @@ -2115,7 +2116,7 @@ export class EngineCommandManager extends EventTarget { Object.values(this.pendingCommands).map((a) => a.promise) ) } - updateArtifactGraph(ast: Program) { + updateArtifactGraph(ast: Node) { this.artifactGraph = createArtifactGraph({ orderedCommands: this.orderedCommands, responseMap: this.responseMap, diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index c70804d8d..f8f14d78b 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -713,11 +713,11 @@ export const modelingMachine = setup({ const revolveSketchRes = revolveSketch( ast, pathToNode, - false, 'variableName' in angle ? angle.variableIdentifierAst : angle.valueAst, - axis + axis, + selection.graphSelections[0]?.artifact ) if (trap(revolveSketchRes)) return const { modifiedAst, pathToRevolveArg } = revolveSketchRes @@ -1755,7 +1755,7 @@ export const modelingMachine = setup({ }, // end services }).createMachine({ - /** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0ANhoBWAHQAOAMwB2KQEY5AFgCcGqWqkAaEAE9Ew0RLEqa64TIBMKmTUXCAvk71oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBEEpYSkJOUUaOWsxeylrWzk9QwQClQkrVOsZNWExFItnVxB3LDxCXwCAW1QAVyDA9hJ2MAjeGI4ueNBE5KkaCWspZfMM+XE1YsQZMQWxNQtxao0ZeRc3dDavTv9ybhG+djGoibjeWZSZCRUxJa1flTCNTWLYIYS2CT2RSKdRyGhZawWc4tS6eDp+fy+KBdMC4AIAeQAbmAAE6YEj6WDPZisSbcd5CYHCSFqOQ1NSKWRiMRA0HqSTKblSX48+pKZGtNHEXEjEm3Eg4kkkfzcQLBUJTanRWlvBKM1LpFSKXKOTKKDmbAyIMwSGgHRFyXL5OE0KQS1HtCTYCCYMAEACieNJgQA1n5yAALLWvKYMpI5RTfNQyYRm-LqHKg7JqdJSBR5hTCI05d0eT3e30BoNy2Bh9iRqiKSI02KxvXxiEKaxVc35uogq1g1OmbnQotiOTaDmlq5QL0+v3+x4k3oYaM6tszIQT6zpcHj+FqI2nUGCtI7UTaeYFLJiGdo+eVgBKYEJqEwxPXrfp7cEE8Tvz5PMKhSCoCgyKewppLsyb7DIoE2DI97lguBAADKoAAZk89DjBuP5bkkE7MoBppLIiSgQYO1i9ukJqTtYGQgTsyH4I+freBGWCYF+dLTPw25yMy0I7rk9TgkeoLVPB0gTkoVjzI43asXOFZ+gAYtgmC+jhzbat+-GzBOXxHssNDGGy8IFKeKjmOkORZDIijVPkSHNJKKGVkuLAkrpeEGXGf6qJCxpst2xz2AOJSOPUiyuqywI0DYGiKCp7EEMgJBhrxuqEUFkg-CBrK-KI3JUdFZjMsIqhWOZPzyKoaVqQQAAiwQjGqvoauEuEvPhhmCV8SjwkJVjQsBp5qLFKjWMmQmInas1uRcZZsc1AAqYCPII7CoIIRAAIItTlm4CURwUKNoig1LZMgzaed0LFowJCjytlGk1qH4phmFBAETDkrgoy9S2fGBROkhJeIqjwcm3YqFJ5m7ssj2usK5rXZ9voSBG3pgAACoDcAEAdvnYJhJChP4UBKkwEb+CwTC9OSIwQCdBFnSI4gVCBWRgRYzpRYgmRFpCqQKDCuSprYWNgBIsARqgADuhNkMTpOcBTVM0yQdP+GAXRMJwkDswNRHaHRqiMRk3ZwkLCDOZyFQWFNwK22yTQrbOEjeHWkbEGQlCYL74ZRiD+lg+2V6mLNNCzea3JiFm8jfCmC1w4x5lpSH9YRq17VgGq2IyqbcaS7aRZ5ox9qIuVQjVJISz7EltRyNkSxyNnft53cGD6xAHD+BAvQku0oah6X7bQoCe4WHdM31PDfJ2JCOyMeY1VaElKhd6HPvdwAkqhmVhkXOJ4v4xJk+QJA8eHMYc4kmTJs7tkdylsi6IOtQ5qmqjGo4JytRd6533qHI+lYT6FyCMXC+itR4AC97i30noRTIbdbRQhSAUBiCNBwpDkNIWqHJuwwk3iAyMYDc4QL9EQbgsB2BKjwP4eB2AkF4lvkPbADDA7Az0g-M2mQeTfAUgcKaI0JyTV+JCbIW94oijvO5D0bEc6UNURGGhxB6GMJIMwq+nAb6YC4TwigfD-KRzQVkRM8hbqMR5LYRQkFZrSCNBOWyWRyFKNWnOdRVDIyaLobgBhTDcD+AOgAIW8P4AAGqgs6mQ8yyTEaIQEtlgRSTENCRY-8gR1AUp7FE3i-ERmKQE7RISwmRP8AATTiU-V0hDjA2JTHka6Ulxy2hmkeZyNRQoUJKeospQSdHMLIFAX0dThbl3mGYO0QJTg7C-iUREVhTCOlbvBbIzkCkeRUd3UpqFAnBN0aE30+B2Bh34f1Mu10cwKAyPaMCYF7Y0UdLaFI8xHSOlsMArx3tfGDMOeUk5DNSRMFxAPcgzMSBygHiYygkyHZQwkIlLQOxlAOEdFJeQiYkqyLZGYBoO8-kPgBYfIFwyKmsPYUMbS+h-C32wFAXAiKRaN3mOZIspUxqngRBUa61QCWTiEv0g5lYjkjNCfo7AhjMD0sZcy1lrpEzClsqIMa3J7bjUTAcDIKNrrEq9qS-ZgLxXAuYbAXAut-C7RiaykSFRhQ1EPKcV2p59jQTsOIcEadhSitNbQ81oTLXWttbU++1yp4OthFoZYvxSGnjhENYwIkzD3M7iSz0ZLwEUuOcwsAABHXonCzlQAuUqqaKKWmiHyBYIRp4ljMlUEJLQqQgRKDUP68lZrKUgqYDC2+vo75XIClG5xtkO3JnerNd1SUUXr3EJk4UmKu05p7Xm0JJIDaoGJLcIN7AqQRtHZYzJmDzDyHZE5TJD1EmnDsJkcyd0NCruoahF8ggtohF6CMVlNjFhAhbbVeCIFQT7EIcKXYMJgTXUvC+-xqFe6F0gIPYeo98Dj1zvameshQquu0ElYQoGsiQm5HHeEUIcidszXsveAatG9uYUrDg9MrU4ggJfW+vQzF9WPfEuO5QEJ2DsPMOojgpKTibeYMCZhlDzF+UarNJru1+kjGMwuu13yIoKLFaeQJ40lUcYOIEOZbKOmqv2NkxpRUHSVrogI1LkFGLhUMUx-g8CYVQAQCA3A5Z4DfGGCQMB2CCAcxwzAgh3OoC0yLIhk4DgQYNZNISEgMU0WrrUeZ1nbOD1C7S4xLnKBudwB5ggpISSoBJBIAGwwPMki6IFvwIWKtsMcxF4rUWj0WLOjRYjYkNC2BSYCOuDtkzngnFObQTkPZZbs5fUkBjOHOd4UVkr3mgZelwP5uWQXBDSsMW1jz0WYQjmTByEC1R1B4OiqBBY5p4bKCsPUECM3B57cW9wgrhdIulZJOVyr1X2C1fqztt74XItHcboxR6scNBFEHMoC253+sODzA4F7AQIlROiStzza3fObdQAFnbJAABGsBBB8AOx1kdXXEg0XNClrksgUx5hTFdxACY0g-AUOZR9SJqM+P2TZ2bmOYk45+39qrLMgcNeC6T8nlPweddyt16EOYIOpLhBOCw7PSjPIqEJCwUPMngnR5UqJ1Txd4421t2Xgh5eCH0FTo7aQ25snWLZE07TqjSEAeYGid1qpm9F5b77ZWKtS5qxV4HjWHdO6VzTlXdO1ffAzJyeCUGlkc+yAsTZ4hORKAUIohTNHQHC8HmM30VufM28J9t2P+BfTO+V6dZPDP24ZCsI6dBcPlnQi+IULehYljiDN5Xr77WJcR4BzL4njewDN8T63jnjsUs5DhMCFMAtdcOi+F612ZlTs7OUYLveB0mAA3paWi5e6GN4i8zXvzdeJAHW8OtQQdw7+CGv5c8xSeV-KCyQ1DfJor7A74OKQifLmT9bOTF6FL-JC4X5yr+A-634brsBT7-bS7R4v5v4f5Brf64hlq-48a04AHQQ0SnZLBpyWh94WwJR5gcjmQF5wG7Kn5l7Zb-RgoQoyrQqwofbLbfbW5P5E6Nbgokjgq4CQp8GCBLamKL5-7L6lDmSSDyAaDQzyCyC0Ec4Z6OpxrgizSjZm7iGSHSHkj8HwoT4lbh5YFR51Z24mE8FQrmGyECHyEJ6KGPwc4qEyLqHGTgTaGlALJ6HCgGEnBugC7FISBoR4DqaoDvgECqb4BxGaYt5eEIAphvKIgiRtyNAiZZiIiLDGSAipDJh3TLTwHGp7wxFAw2rxGYASAHy4AcAkwQDsY8K+QMyoB4B+SkH-4IDFhfAEpTiMTjj2wAamD2CTgzTGBTisEn5RE1EpENFNEtGkCmLDqeGCJHgLCtwRRtwlEZLJpZBCQzSZF1DzFFK+JLF1HvgSC4DR63zECYCsDQLdyIpGjZBr7ZA1BAicigjLDq6fCaDaAzQpCiqrEYGIpKBpC6pLDPIEZWBZhaC5iQY8gHDmYREl7sGUKQkECNhL7pGqohS2DCjNrGj5AAkgQpZxxCrolIyirrRqZ4jYCcIwrkB1GJERhqa3GbF9FKGZGNwOChRwiAgSYNrVRFGAisjmDXTQyMnMmcBskkgcm7SNHNEYGBKcC4Bcb6x8AfZjxMDlaYRaTcagz9F2A0QuLGCgTjQ2CGbRRQYpZKQzRkKgmGqVGKZ7xMnJEsnKmqmoD3GPGYDPGvEYYNhpFmxGhZI7FCZ2k0QNqxSpjgiunSmgQelsFRG9BSGoCDAwrsDqL4i4AP7rYiFyyv7v7ZneaCAdEFndxFmsqUGmC3hwgJwIjDbOQLCylxynC2ALK-CiovihDj68lck8kaZ8nmkCnVRClmhCSVQ5BJzURQTOmFj3KpC5CDkhAubjLLEv6cFjxBCf7sZ3AkhAwkgECaTNEKz+BbrDnz4fF6ZAGaqCjdiEaDhOTgguI5CZycj1DKBbn3m7m8n7l2ZjzNZQB4CtHsZ3k7lV4QV4CPnzDOnGi87whQRSTdK2jOQchfLVTNqAVwV7lXncJcTsZDlEUln4625-S9BMAf44Ayg1l4w1ndw1lgCwIHqIq9LlCbK2Bxz7DXRLklBTG7gH5u6pDCYZrYlREUUjkTkSAkBtFjwQBKjYS3nbnj5UW14BZ-SCB0WyFqXBawXj6PkmA54zIzLOS97bBG7pDiLGgaDqAsSRG+JyXz4gVKUDzoaqUkDqUmXz6YGR6A44F6UGW+XYSCABW7kfFOTlACUzlQ4gQOkc5Hi7iZJNwpi7CTgVGZm+JEAyjBjuXAUTljnJG8ncUzlxQJJCQSa-BOLq5GjmBTj8ZWauX7IFXVj+DFVV4KXl7alQB7pnmkiXl4CkW3CFVyjRUTKRlxiAgol5jmCCU3hYrw5HiELu43i-AiR1CiqdWyjdWaUeV9UHnoYIXFkHRtETVdXTWFznWPnUmF6MGwwHC66OB1AVD2A7G1BLr1B7WTWHVAW9X1ESAkUKyQD+D7VFVHW7naVlnyx+AGXkCMV4jMUYCsWhzsWcWwDWDcUHB778YrLXjaCQRwgkYbl1Du7pn-VdU9V7leVDxGXXUHW3Vw0E66WNbhVGUf6TVRUw0zWElRn7jpAphTSZElHciJrCiLA3io5Giuh2A00HV02eVXURUBCUA3X81+g2HBUy5hX0Xq083Vh81A1mkRwWndgmb2jepryZjw6pBPTgjLr5Bpb84yW+LI1gBkCBAsx+iNmAG0m3KVCpigizGQgXZOUwjnpYmeml6UKBLSroa2qsYQ2EicZ+jCHs1ywkAMISohIAByCokAAAahnVpicZMQ6EVOCH+eJm8pkuyByAnDYntdwEnYNSncXexunZgFxkFTPjgbnewPnSckXWxmXX3ebQInGIiMCNIP1tkASktDvhyMJJ7lkBlpyCKu1WfpwWgZKhpT0Lup-ugbAGzbbtWafZKnzcfQvtfSEr0VOekQ6MyEoJBktKQlNDerikBFlc5D8JcQgXvbNg-SCrloOvoBfc-lfQQRA3StFhYNhaqvnmSUlACbFHHDCO7hsK7WbmA3ovNjKpA9AwFrA1-qDnKkdomGsLIEcEmvbFNJIKoLSXGoA85Pg0Giws1jSpAwyjgMyqQ3LOQxuk1ogo5nKvbgIyyrNe2HPeUHNCLHqs6O+dFD4eFEHQxIuZw3fnNtfHwwqsWVnZfXtAQ7gLtkQ7Kk7oYxXclnmG4pkqFCTfDo4AsI7Y6CBG3AcJyDoxuoEFakwHUTEkIxICIzfSGvRbtBTog18NDLNICNXJiu6hyKnJNiVKyOnr44fRE0E9UiE2EyEjWQEztHtPoIg2kGij8BjCkLJqeFUJ9bUPkNCIKssFkxUoWsWkYj-vk6YwQR07fIQeciQc-WbPI5CENjYKmPYGcQ2l8fNSjtHdJXHTiSUv1QfRUv2kqNpFgD0-gV-ps4OtQLI4RPTtYtQZUBvjsDZEeCOGBLkT8MmDCHtdyeVetPUW5r9Picc91v1tILeLktJDMgCZOLaHaE5OaGKfBM8zyW8++B8+fQSVsbPb858uiT6vBOIKCHNKC48xC9VFC7vaAkQC8zAP4LC0YuTAi9YILci-PaiwC6cEC9-JWi6mrpC7HXlR1SS4XOS-C-iTIDS3Iyi-87UIC5i-DuILE2C2y-i5ONC68+85S-iSoIKyc8K2ZqK4y+K46Vkqy3i2yHK4Swndy2S4q581QGIKqz83SyK+i0y46UCDi+C0eLK1Rh7R1dgCqcDQkUkaSxOZVVkQ4JinUAk5LdRJXVbfUBi-YDyLlQsflZ617SBf1eBaPJBRdVdeQIm-BWm4hd84kPensAA85FDk+tihOJgqcD6qkPBGcEayUkQNm3uSm+hk+EpdgP0KNdefTFm169PZGoRJacyANo6OibzIEYuY0vBLVHmFyBmfGx6326rd5YNerbcE2yEwbYZX5cFr217R8X8CijNHPOZKBDZXrsImoUeGvVYKyHtU28uypUzXu5WLrYPfYVu0bS+-27xokDUD7p-MoDNLNOsuexdmJYvIxDeAcO7cs1EcSzCyDbAADBwANeGfTEwNgOCpuyh8FrWJjZh+ChFphIIEDJACbPmxzuiSIjDIbtgm3LykaN8KEW-J2YUPK6S+S-LLh2h-h7nAzFhzrb9tPtgR+7hxjbnIIIRwvuTKR2AOR2zJRwMbUKeEJZ0nzFoVlcCBxzy0h4jYE3x5GP4LVqOcY8-rRfRcjdgExWwOjYZxGFjefAejIFpjKaYOmddEoIxJkLrjVC4jYKBMKI839fWxIAhwq3cRZ+h8ZxVqOW+6J-VhZwxdZ6jbZwvvZ45zKLAC50p645IHDMYN2P8MYMJYgExDLVygF8BI4P0v4A8cZ5TBxOqAEBgBTMzP9ETIioIAnCOFM7UICNtTvsYP+gSonGltNpEWQNgF0MMGPLatVkDCE1NzNyMCU4IFrD+2QQMSkCis-Ajt0mLe0htbK7AU5HVGlMt7N8nbmQt0J5Lu+-Vpd6t1Ext42Y3EWG3PMPBPE0JKCMAZ9ax9oN58pJN80St3N7mVtAaehvZ0t2D8MAvlE-Zx8Tt0wVNMNMaLMao0YF+YcBlQJSPkAw+E9xD3qdD4NbD-F3YY9-D893tMj0p+vHuNdN8lrrYFi4ASkMCEeJknaK2mlOfpfuBT9H9AzETCE1hL9H4KrEDAdAwu4Fps4naNmHdLILMfBIjKLLW1W67D1nG0UoL3KsL1Lx12rAPQlxIJL39DL2AHL+wAr4z84kJM3TsLqnqjvp2aYEaP18xG4gL0gfoGPC8dhCE8H+wHbw71a3Tj8IsE5XdH8FoJyNj6UJyG45RJqssOZv70L+hmH+b9TxIGHxH+gFpjHxk0sDscukJMNgUHdKYOaNATrqlJEYb4HzD1xNpJux35gMX4p1HxzkxzYJleZHCNmNioUXijDJkmmtyNn0b+39xPnyFR+93736XzqutcqikKkB8tihdNyFNAavYsflcYu0mwpWDWRZDRu2ZxzewEjSjXhyxRlzAk57ACoJVbkJMfjf+A4CeNRLz3nQ1BfgrocSIomaAPEMA8AKIGwSRa-gtctoMokD0PDiAs8SQIfqiTSry11A71WWHALyiU1EBrqWtHaFQEAkDguYY0E+jSxMhZYOMPGDb2gEjNAol4IgTUBIEHB6gqnBnIwSK5AglgtyOgQrGViMD8BnMNeLmCcpJQ6GcWXlDkEWDv044rILIFXH6RiCC2IGQcIIADz2QkYf8RdFNCWactaMymdQcLERDMMG+ogdkB7HPaOguy4kREPdhq6hc1m8DfLIIXaxmCHY1UJ2ouSB4MRz2ELNIIVyBBKDGmBLd1iA1eyWN3slhHHN4McCgR3OjlAbos0CKp9mQd0UKM-FyR1sohHBEXFUmxyRZEhT5XYGBB85tx18YHTxukH-hmZNczfAoZQjWYh4Eh-JdIo4BubiIHA8cWCKkG9wAQukzyWaA4iMELtohAQEcqUM6GCJ20toE3LYEbptoXkPhGEAA1+DRsiw1mAPigSII34zG7AMoXSwTDSk7oWwj3i-GfgNQvqXPYwtwSkK8FzCHg1zLMOYFTw6gOYGiPMH+DZEnB2KGSHilJLD4GkoqG4hOW8HwQv+Y0JSNvG57ntUwCwKGFoWqACx8hcHa4rERAqQkoR3jT6mOFrgZhWQWYZLBkE5AYojQ79Inl6VAQQiQaDxOrLfG8F2APqCtN2ICR5ApUEAMyeoUJEcCpIioNI+OiUlxFzC4w2CCuC6Gn47BmIycQhKyPThD84QJ-YBqAh9IwA-SRidknUShENBEBNpaMrIATISthwyZDuNCAsD3IFSvpJUtqJVJ1F1SHAPEa6ANGSV4y3I6eDmEJGpl20tkG0ZqLtEMoHRapRkTN0wAui0gcyd0enhNHRRTgbjF0jRDTL+jQuVZXMrWULL8QZ6U8HASim1wWQYI-wOQfUMeZUiHkQEQivJXqJ6jhufFf3IJSubURvk5NYiNvjAhVjjqINFthTxCDcATyFWc8iyIwH8xWQW+MwJIg-JjhxmccLQPiiSidiSq3Y06oNXOosjZokgADCkiC6uIpIWhWSCmjSSOweQi471g0Uv4Q06a64lOIy0yJ0N8Wf3DGCiiSg4Jmm7iVUVUVAQq0FKXlJ9juw0pm1rxhCRlpLCtrZBV6L8W4WBnWoDlQuUNOUN+JrHij2w0IgqITVPZKC0BMIZIZBi84iQNhHLSYUSwBqIS7iPYoakOOQmDtkomCJNB43RhBDhQ6VNNPHGwk+ola0NM2smxXEqhc22YgdmdAXi7hT2yURaEw3dRfF4Qf5RdJOCEQcSEJ2tEChePYzwTAaRFdccaAUFbiwIKYSaIxBpI8gq4aSDIBMNP57xVJpEhogzTXaa0Wa2tDSeyjhDv1oQksRNEsFTiJwWckrSIZiP2Re0fasAP2okINRHtnQhKQlMn3sT19iomKUfrB2MFEt26RDa7nV27ocYp6ZQ-YDLVrhHgxSm9eulVDWALxDcNgUyWqNaH70zGR9HdIXCOFMCLaShAUVkIRwuwEk5oZPjBhzBfUh2pkLQNYDabgMeGEjEoB8LQRcoQok4Vqa6HtigRh2YEH6t5yrb9TXBlUrhpQxGkNSuh40qbK9SYKUlv4Noa6AnE+QwwdhK00Blw3gbIFDGZQmeDtKWpXpNUvKL-iBHRhih+B8UoiRVIum6N1p-DJlPxN-bCxtpCOB6caG5BBDsJz4rZN2FIxrwBpFqAJkE2iSZTvhPw2ZD0mugdlxsFQJpMcAOICoEZwaJGWGlRky0-yspaoFjLqbJCiwU0UQMoDlLQgiZ+sItCWgOERhbpzU6YpK3Bn1V4cOQIYnczSSCUJYLMg5tswjFUT4khA0UFOkbqcosJ52SgceHlqxp9e5Uhtia15aUsoR8IfLhcxSBbDJxJQJposNOBcjkyTqe9ku0hHSy-2pUb-klF-7UzqITpdqY0DbhLSbZ5-ZcWBTOp8S8R0tGxLUApEFANAUkaoFVFqCHg0qF4DWZ+IToPsTq-swam2wHj9A8R89EUGe2yAgDAQGSO0N8BcjeNnoTlH2WeNBpjVwaKkptnqMrRHTq+PyaQcn3Xy7gecQXfFHdArn01lKPlZ9nXPtnbAMSiwXsrnNvDcj+KXwL0ZkCvSXYdOprd8N4IGy7Fvk9QJNNVCzBMdKMOQI0Lkh6QLyuOyHHAOwF47dwBO4KZebsFhJrzUcmRLFg4DoiJQrA80s6S0K1mIdIu+naLiZztmjTusmScoGYA3h2AJwGQUrg7HBmLAcK6YPMGaGFErM6uuZDbt4O0HSISunjYDi3GGw1Bk0uQCwZQX6wILFKtPUnrd28GJJq2yYlnGnBr4fVnKwIdQD9RqgXdSFKUqHgwkPLdxvBILaYtyBtIBc95YdHbjeB6SrB+8c-NvquJF4YhyFQ8pIEkMdSph6gtQNeNenDY2gwoEmJNNCOWkyVW+QfLCMcPkXdd9J2QWunc0tFdgpIf5Z0ikjplrw3WcHAxQv20ioK1O1UKdDeFr7aZsUmknYDzh-L3o2QLgFwEAA */ + /** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0ANhoBWAHQAOAMwB2KQEY5AFgCcGqWqkAaEAE9Ew0RLEqa64TIBMKmTUXCAvk71oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBEEpYSkJOUUaOWsxeylrWzk9QwQClQkrVOsZNWExFItnVxB3LDxCXwCAW1QAVyDA9hJ2MAjeGI4ueNBE5KkaCWspZfMM+XE1YsQZMQWxNQtxao0ZeRc3dDavTv9ybhG+djGoibjeWZSZCRUxJa1flTCNTWLYIYS2CT2RSKdRyGhZawWc4tS6eDp+fy+KBdMC4AIAeQAbmAAE6YEj6WDPZisSbcd5CYHCSFqOQ1NSKWRiMRA0HqSTKblSX48+pKZGtNHEXEjEm3Eg4kkkfzcQLBUJTanRWlvBKM1LpFSKXKOTKKDmbAyIMwSGgHRFyXL5OE0KQS1HtCTYCCYMAEACieNJgQA1n5yAALLWvKYMpI5RTfNQyYRm-LqHKg7JqdJSBR5hTCI05d0eT3e30BoNy2Bh9iRqiKSI02KxvXxiEKaxVc35uogq1g1OmbnQotiOTaDmlq5QL0+v3+x4k3oYaM6tszIQT6zpcHj+FqI2nUGCtI7UTaeYFLJiGdo+eVgBKYEJqEwxPXrfp7cEE8Tvz5PMKhSCoCgyKewppLsyb7DIoE2DI97lguBAADKoAAZk89DjBuP5bkkE7MoBppLIiSgQYO1i9ukJqTtYGQgTsyH4I+freBGWCYF+dLTPw25yMy0I7rk9TgkeoLVPB0gTkoVjzI43asXOFZ+gAYtgmC+jhzbat+-GzBOXxHssNDGGy8IFKeKjmOkORZDIijVPkSHNJKKGVkuLAkrpeEGXGf6qJCxpst2xz2AOJSOPUiyuqywI0DYGiKCp7EEMgJBhrxuqEUFkg-CBrK-KI3JUdFZjMsIqhWOZPzyKoaVqQQAAiwQjGqvoauEuEvPhhmCV8SjwkJVjQsBp5qLFKjWMmQmInas1uRcZZsc1AAqYCPII7CoIIRAAIItTlm4CURwUKNoig1LZMgzaed0LFowJCjytlGk1qH4phmFBAETDkrgoy9S2fGBROkhJeIqjwcm3YqFJ5m7ssj2usK5rXZ9voSBG3pgAACoDcAEAdvnYJhJChP4UBKkwEb+CwTC9OSIwQCdBFnSI4gVCBWRgRYzpRYgmRFpCqQKDCuSprYWNgBIsARqgADuhNkMTpOcBTVM0yQdP+GAXRMJwkDswNRHaHRqiMRk3ZwkLCDOZyFQWFNwK22yTQrbOEjeHWkbEGQlCYL74ZRiD+lg+2V6mLNNCzea3JiFm8jfCmC1w4x5lpSH9YRq17VgGq2IyqbcaS7aRZ5ox9qIuVQjVJISz7EltRyNkSxyNnft53cGD6xAHD+BAvQku0oah6X7bQoCe4WHdM31PDfJ2JCOyMeY1VaElKhd6HPvdwAkqhmVhkXOJ4v4xJk+QJA8eHMYc4kmTJs7tkdylsi6IOtQ5qmqjGo4JytRd6533qHI+lYT6FyCMXC+itR4AC97i30noRTIbdbRQhSAUBiCNBwpDkNIWqHJuwwk3iAyMYDc4QL9EQbgsB2BKjwP4eB2AkF4lvkPbADDA7Az0g-M2mQeTfAUgcKaI0JyTV+JCbIW94oijvO5D0bEc6UNURGGhxB6GMJIMwq+nAb6YC4TwigfD-KRzQVkRM8hbqMR5LYRQkFZrSCNBOWyWRyFKNWnOdRVDIyaLobgBhTDcD+AOgAIW8P4AAGqgs6mQ8yyTEaIQEtlgRSTENCRY-8gR1AUp7FE3i-ERmKQE7RISwmRP8AATTiU-V0hDjA2JTHka6Ulxy2hmkeZyNRQoUJKeospQSdHMLIFAX0dThbl3mGYO0QJTg7C-iUREVhTCOlbvBbIzkCkeRUd3UpqFAnBN0aE30+B2Bh34f1Mu10cwKAyPaMCYF7Y0UdLaFI8xHSOlsMArx3tfGDMOeUk5DNSRMFxAPcgzMSBygHiYygkyHZQwkIlLQOxlAOEdFJeQiYkqyLZGYBoO8-kPgBYfIFwyKmsPYUMbS+h-C32wFAXAiKRaN3mOZIspUxqngRBUa61QCWTiEv0g5lYjkjNCfo7AhjMD0sZcy1lrpEzClsqIMa3J7bjUTAcDIKNrrEq9qS-ZgLxXAuYbAXAut-C7RiaykSFRhQ1EPKcV2p59jQTsOIcEadhSitNbQ81oTLXWttbU++1yp4OthFoZYvxSGnjhENYwIkzD3M7iSz0ZLwEUuOcwsAABHXonCzlQAuUqqaKKWmiHyBYIRp4ljMlUEJLQqQgRKDUP68lZrKUgqYDC2+vo75XIClG5xtkO3JnerNd1SUUXr3EJk4UmKu05p7Xm0JJIDaoGJLcIN7AqQRtHZYzJmDzDyHZE5TJD1EmnDsJkcyd0NCruoahF8ggtohF6CMVlNjFhAhbbVeCIFQT7EIcKXYMJgTXUvC+-xqFe6F0gIPYeo98Dj1zvameshQquu0ElYQoGsiQm5HHeEUIcidszXsveAatG9uYUrDg9MrU4ggJfW+vQzF9WPfEuO5QEJ2DsPMOojgpKTibeYMCZhlDzF+UarNJru1+kjGMwuu13yIoKLFaeQJ40lUcYOIEOZbKOmqv2NkxpRUHSVrogI1LkFGLhUMUx-g8CYVQAQCA3A5Z4DfGGCQMB2CCAcxwzAgh3OoC0yLIhk4DgQYNZNISEgMU0WrrUeZ1nbOD1C7S4xLnKBudwB5ggpISSoBJBIAGwwPMki6IFvwIWKtsMcxF4rUWj0WLOjRYjYkNC2BSYCOuDtkzngnFObQTkPZZbs5fUkBjOHOd4UVkr3mgZelwP5uWQXBDSsMW1jz0WYQjmTByEC1R1B4OiqBBY5p4bKCsPUECM3B57cW9wgrhdIulZJOVyr1X2C1fqztt74XItHcboxR6scNBFEHMoC253+sODzA4F7AQIlROiStzza3fObdQAFnbJAABGsBBB8AOx1kdXXEg0XNClrksgUx5hTFdxACY0g-AUOZR9SJqM+P2TZ2bmOYk45+39qrLMgcNeC6T8nlPweddyt16EOYIOpLhBOCw7PSjPIqEJCwUPMngnR5UqJ1Txd4421t2Xgh5eCH0FTo7aQ25snWLZE07TqjSEAeYGid1qpm9F5b77ZWKtS5qxV4HjWHdO6VzTlXdO1ffAzJyeCUGlkc+yAsTZ4hORKAUIohTNHQHC8HmM30VufM28J9t2P+BfTO+V6dZPDP24ZCsI6dBcPlnQi+IULehYljiDN5Xr77WJcR4BzL4njewDN8T63jnjsUs5DhMCFMAtdcOi+F612ZlTs7OUYLveB0mAA3paWi5e6GN4i8zXvzdeJAHW8OtQQdw7+CGv5c8xSeV-KCyQ1DfJor7A74OKQifLmT9bOTF6FL-JC4X5yr+A-634brsBT7-bS7R4v5v4f5Brf64hlq-48a04AHQQ0SnZLBpyWh94WwJR5gcjmQF5wG7Kn5l7Zb-RgoQoyrQqwofbLbfbW5P5E6Nbgokjgq4CQp8GCBLamKL5-7L6lDmSSDyAaDQzyCyC0Ec4Z6OpxrgizSjZm7iGSHSHkj8HwoT4lbh5YFR51Z24mE8FQrmGyECHyEJ6KGPwc4qEyLqHGTgTaGlALJ6HCgGEnBugC7FISBoR4DqaoDvgECqb4BxGaYt5eEIAphvKIgiRtyNAiZZiIiLDGSAipDJh3TLTwHGp7wxFAw2rxGYASAHy4AcAkwQDsY8K+QMyoB4B+SkH-4IDFhfAEpTiMTjj2wAamD2CTgzTGBTisEn5RE1EpENFNEtGkCmLDqeGCJHgLCtwRRtwlEZLJpZBCQzSZF1DzFFK+JLF1HvgSC4DR63zECYCsDQLdyIpGjZBr7ZA1BAicigjLDq6fCaDaAzQpCiqrEYGIpKBpC6pLDPIEZWBZhaC5iQY8gHDmYREl7sGUKQkECNhL7pGqohS2DCjNrGj5AAkgQpZxxCrolIyirrRqZ4jYCcIwrkB1GJERhqa3GbF9FKGZGNwOChRwiAgSYNrVRFGAisjmDXTQyMnMmcBskkgcm7SNHNEYGBKcC4Bcb6x8AfZjxMDlaYRaTcagz9F2A0QuLGCgTjQ2CGbRRQYpZKQzRkKgmGqVGKZ7xMnJEsnKmqmoD3GPGYDPGvEYYNhpFmxGhZI7FCZ2k0QNqxSpjgiunSmgQelsFRG9BSGoCDAwrsDqL4i4AP7rYiFyyv7v7ZneaCAdEFndxFmsqUGmC3hwgJwIjDbOQLCylxynC2ALK-CiovihDj68lck8kaZ8nmkCnVRClmhCSVQ5BJzURQTOmFj3KpC5CDkhAubjLLEv6cFjxBCf7sZ3AkhAwkgECaTNEKz+BbrDnz4fF6ZAGaqCjdiEaDhOTgguI5CZycj1DKBbn3m7m8n7l2ZjzNZQB4CtHsZ3k7lV4QV4CPnzDOnGi87whQRSTdK2jOQchfLVTNqAVwV7lXncJcTsZDlEUln4625-S9BMAf44Ayg1l4w1ndw1lgCwIHqIq9LlCbK2Bxz7DXRLklBTG7gH5u6pDCYZrYlREUUjkTkSAkBtFjwQBKjYS3nbnj5UW14BZ-SCB0WyFqXBawXj6PkmA54zIzLOS97bBG7pDiLGgaDqAsSRG+JyXz4gVKUDzoaqUkDqUmXz6YGR6A44F6UGW+XYSCABW7kfFOTlACUzlQ4gQOkc5Hi7iZJNwpi7CTgVGZm+JEAyjBjuXAUTljnJG8ncUzlxQJJCQSa-BOLq5GjmBTj8ZWauX7IFXVj+DFVV4KXl7alQB7pnmkiXl4CkW3CFVyjRUTKRlxiAgol5jmCCU3hYrw5HiELu43i-AiR1CiqdWyjdWaUeV9UHnoYIXFkHRtETVdXTWFznWPnUmF6MGwwHC66OB1AVD2A7G1BLr1B7WTWHVAW9X1ESAkUKyQD+D7VFVHW7naVlnyx+AGXkCMV4jMUYCsWhzsWcWwDWDcUHB778YrLXjaCQRwgkYbl1Du7pn-VdU9V7leVDxGXXUHW3Vw0E66WNbhVGUf6TVRUw0zWElRn7jpAphTSZElHciJrCiLA3io5Giuh2A00HV02eVXURUBCUA3X81+g2HBUy5hX0Xq083Vh81A1mkRwWndgmb2jepryZjw6pBPTgjLr5Bpb84yW+LI1gBkCBAsx+iNmAG0m3KVCpigizGQgXZOUwjnpYmeml6UKBLSroa2qsYQ2EicZ+jCHs1ywkAMISohIAByCokAAAahnVpicZMQ6EVOCH+eJm8pkuyByAnDYntdwEnYNSncXexunZgFxkFTPjgbnewPnSckXWxmXX3ebQInGIiMCNIP1tkASktDvhyMJJ7lkBlpyCKu1WfpwWgZKhpT0Lup-ugbAGzbbtWafZKnzcfQvtfSEr0VOekQ6MyEoJBktKQlNDerikBFlc5D8JcQgXvbNg-SCrloOvoBfc-lfQQRA3StFhYNhaqvnmSUlACbFHHDCO7hsK7WbmA3ovNjKpA9AwFrA1-qDnKkdomGsLIEcEmvbFNJIKoLSXGoA85Pg0Giws1jSpAwyjgMyqQ3LOQxuk1ogo5nKvbgIyyrNe2HPeUHNCLHqs6O+dFD4eFEHQxIuZw3fnNtfHwwqsWVnZfXtAQ7gLtkQ7Kk7oYxXclnmG4pkqFCTfDo4AsI7Y6CBG3AcJyDoxuoEFakwHUTEkIxICIzfSGvRbtBTog18NDLNICNXJiu6hyKnJNiVKyOnr44fRE0E9UiE2EyEjWQEztHtPoIg2kGij8BjCkLJqeFUJ9bUPkNCIKssFkxUoWsWkYj-vk6YwQR07fIQeciQc-WbPI5CENjYKmPYGcQ2l8fNSjtHdJXHTiSUv1QfRUv2kqNpFgD0-gV-ps4OtQLI4RPTtYtQZUBvjsDZEeCOGBLkT8MmDCHtdyeVetPUW5r9Picc91v1tILeLktJDMgCZOLaHaE5OaGKfBM8zyW8++B8+fQSVsbPb858uiT6vBOIKCHNKC48xC9VFC7vaAkQC8zAP4LC0YuTAi9YILci-PaiwC6cEC9-JWi6mrpC7HXlR1SS4XOS-C-iTIDS3Iyi-87UIC5i-DuILE2C2y-i5ONC68+85S-iSoIKyc8K2ZqK4y+K46Vkqy3i2yHK4Swndy2S4q581QGIKqz83SyK+i0y46UCDi+C0eLK1Rh7R1dgCqcDQkUkaSxOZVVkQ4JinUAk5LdRJXVbfUBi-YDyLlQsflZ617SBf1eBaPJBRdVdeQIm-BWm4hd84kKkpIFNGyI0943HNikaHuD9ZRswUeHtdm3uSm+hk+EpdgP0KNdefTFm169PZGoRCUYmH8Sw+C-YhkjNDImng+vUKyPWz2yBWDWRZDQ2yE7RfRcjdgExWwOjbWJjTAufAeioHjcsCijdrXN2MsEWFJPyBUKILYNUMqmjkayUkQA26rd5YNerbcMu8Y8-gbYZX5cFt217Y+RCHnqMcKGUFe5W2OBFBe6cLO0mwpV5SpUzUB5WLrYPfYX+0bWh727xgW5RosDhmKICI3dioxBUEeJkqoPeimPK6S+S-LADBwANeGfTEwNgOCiu8x8Fju7nIIBx+ChFphIIEDJACbPmxzuiSIjDIbtgm3LypWyBG9K6YiIUPRzyyDbADx6x3x5GAzJxzrb9tPtgVhzxxjfx4JwvuTKJ2AOJ2zJJ8oQUClqyHmHdhluCFmKsilJkM64cBy-G1yzC1p4jYE3p-TLVqOT+xzewEjSjbxyxeF1jfu7ADIFpjKaYOmddEoIxJkLrjVC4jYKBMKI839U+xIMS8F3cau2x-4JF6VRh6Z-VquwxRu6jVuwvkl3uzKKl6yqVFWm+ee1NMYMJYgExDLVykV8BI4P0v4A8XV5TBxOqAEBgBTMzP9ETIioIAnCOFM7UKRzRDvsYP+gSonGltNpEWQNgF0MMGPLatVkDCE1dzdyMCU4IFrHh2QQMSkCis-Ajt0mLe0htbK7AU5HVGlM97d8nbmQ90Z5Lph-VpD691Ex942Y3EWG3PMPBPE0JKCMAZ9W-OaM6NshD80S93d7mVtAaehuF092T8MAvlE+Fx8T90wVNMNMaLMao0YF+YcBlQJSPkAw+EjxT3qdT4NbT413YYj-T8j3tMz45+vHuNdN8lrrYFi4ASkMCFR8-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-S9tRERkDdlA3eLIO-SNmNtSJKoNccOMKKTpUksceqtRAKCJhLS9gBwOOGBAuzuJkwZtq236CezCErIV0IhCz7sDqIl2FLKIGb7FgG44chSleKUkNtgJewWqmBA1k5UMkQs6jlBnEQ-xipL0hOq+yQ7KUAJAHL9j21vGVtN4VtTZLsFG5BFEk6MBaLkBqEzt4JJrclj4IGy7Fvk9QJNNVCzCVtKMxoCKGIjllVEiWo8rTjp0PLdwDO4KcebsFhJTzUcmRLFg4DoiJQrAYEfbhp1NbVdQutXerjWIFndZMk5QMwBvDsATgMgPc1NIsBwrpg3ODgIUSszm65kPuPg3QdIhG6eMZomU58S-A1k2Ajg5ofrEAsUqy9ResPHwYkh9S10WRrvaoO0gKhUF1AP1GqKT2u5Q9O6lPfUgwi3mhwfBILaYtyBtJFcjQjfNtDLQGGPQyRK8z0CvyD4m9MFT82YCkMdSphp2j2BMIjBtBhQJMSaWEb1OX4B8e+WEU4cIqEDWV7IuCtuNCCdkvI-yzpFJGTLXhutPJ-C+-tpHAVCUDcU6G8DP20zYp1JOwHnD+XvRsgXALgIAA */ id: 'Modeling', context: ({ input }) => ({ @@ -2789,7 +2789,8 @@ export function isEditingExistingSketch({ const maybePipeExpression = variableDeclaration.node.init if ( maybePipeExpression.type === 'CallExpression' && - maybePipeExpression.callee.name === 'startProfileAt' + (maybePipeExpression.callee.name === 'startProfileAt' || + maybePipeExpression.callee.name === 'circle') ) return true if (maybePipeExpression.type !== 'PipeExpression') return false