diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XY-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XY-1-Google-Chrome-linux.png index 06b144cfc..c8a6296c3 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XY-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XY-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-win32.png index ed11f9eec..4442073d2 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-win32.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-linux.png index c14c85ee2..0a3640c69 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XZ-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XZ-1-Google-Chrome-linux.png index af3ff5c42..b78f6260d 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XZ-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XZ-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-YZ-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-YZ-1-Google-Chrome-linux.png index 2e8fff59e..f8410c37f 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-YZ-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-YZ-1-Google-Chrome-linux.png differ diff --git a/src/Toolbar.tsx b/src/Toolbar.tsx index 7396f5b86..e6e680721 100644 --- a/src/Toolbar.tsx +++ b/src/Toolbar.tsx @@ -38,9 +38,8 @@ export function Toolbar({ '!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary' const sketchPathId = useMemo(() => { - if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) { + if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) return false - } return isCursorInSketchCommandRange( engineCommandManager.artifactGraph, context.selectionRanges diff --git a/src/clientSideScene/sceneEntities.ts b/src/clientSideScene/sceneEntities.ts index 90d542621..9af84c565 100644 --- a/src/clientSideScene/sceneEntities.ts +++ b/src/clientSideScene/sceneEntities.ts @@ -578,10 +578,8 @@ export class SceneEntities { draftExpressionsIndices && index <= draftExpressionsIndices.end && index >= draftExpressionsIndices.start - const isSelected = selectionRanges?.codeBasedSelections.some( - (selection) => { - return isOverlap(selection.range, segment.__geoMeta.sourceRange) - } + const isSelected = selectionRanges?.graphSelections.some((selection) => + isOverlap(selection?.codeRef?.range, segment.__geoMeta.sourceRange) ) let seg: Group diff --git a/src/components/AstExplorer.tsx b/src/components/AstExplorer.tsx index 338efd9ed..00b105f56 100644 --- a/src/components/AstExplorer.tsx +++ b/src/components/AstExplorer.tsx @@ -1,15 +1,17 @@ import { useModelingContext } from 'hooks/useModelingContext' -import { editorManager, kclManager } from 'lib/singletons' +import { editorManager, engineCommandManager, kclManager } from 'lib/singletons' import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst' import { useEffect, useRef, useState } from 'react' import { trap } from 'lib/trap' +import { codeToIdSelections } from 'lib/selections' +import { codeRefFromRange } from 'lang/std/artifactGraph' export function AstExplorer() { const { context } = useModelingContext() const pathToNode = getNodePathFromSourceRange( // TODO maybe need to have callback to make sure it stays in sync kclManager.ast, - context.selectionRanges.codeBasedSelections?.[0]?.range + context.selectionRanges.graphSelections?.[0]?.codeRef?.range ) const [filterKeys, setFilterKeys] = useState(['start', 'end']) @@ -121,13 +123,21 @@ function DisplayObj({ editorManager.setHighlightRange([[obj?.start || 0, obj.end]]) }} onClick={(e) => { + const range: [number, number] = [obj?.start || 0, obj.end || 0] + const idInfo = codeToIdSelections([ + { codeRef: codeRefFromRange(range, kclManager.ast) }, + ])[0] + const artifact = engineCommandManager.artifactGraph.get( + idInfo?.id || '' + ) + if (!artifact) return send({ type: 'Set selection', data: { selectionType: 'singleCodeCursor', selection: { - type: 'default', - range: [obj?.start || 0, obj.end || 0], + artifact: artifact, + codeRef: codeRefFromRange(range, kclManager.ast), }, }, }) diff --git a/src/components/AvailableVarsHelpers.tsx b/src/components/AvailableVarsHelpers.tsx index f94b571d8..6dbfb492c 100644 --- a/src/components/AvailableVarsHelpers.tsx +++ b/src/components/AvailableVarsHelpers.tsx @@ -96,7 +96,8 @@ export function useCalc({ } { const { programMemory } = useKclContext() const { context } = useModelingContext() - const selectionRange = context.selectionRanges.codeBasedSelections[0].range + const selectionRange = + context.selectionRanges?.graphSelections[0]?.codeRef?.range const inputRef = useRef(null) const [availableVarInfo, setAvailableVarInfo] = useState< ReturnType @@ -157,6 +158,7 @@ export function useCalc({ engineCommandManager, useFakeExecutor: true, programMemoryOverride: kclManager.programMemory.clone(), + idGenerator: kclManager.execState.idGenerator, }).then(({ execState }) => { const resultDeclaration = ast.body.find( (a) => diff --git a/src/components/CommandBar/CommandBarSelectionInput.tsx b/src/components/CommandBar/CommandBarSelectionInput.tsx index 5dfe4f4d1..c4be65f6a 100644 --- a/src/components/CommandBar/CommandBarSelectionInput.tsx +++ b/src/components/CommandBar/CommandBarSelectionInput.tsx @@ -1,9 +1,9 @@ import { useSelector } from '@xstate/react' import { useCommandsContext } from 'hooks/useCommandsContext' import { useKclContext } from 'lang/KclProvider' +import { Artifact } from 'lang/std/artifactGraph' import { CommandArgument } from 'lib/commandTypes' import { - Selection, canSubmitSelectionArg, getSelectionType, getSelectionTypeDisplayText, @@ -12,13 +12,13 @@ import { modelingMachine } from 'machines/modelingMachine' import { useEffect, useMemo, useRef, useState } from 'react' import { StateFrom } from 'xstate' -const semanticEntityNames: { [key: string]: Array } = { - face: ['extrude-wall', 'start-cap', 'end-cap'], - edge: ['edge', 'line', 'arc'], - point: ['point', 'line-end', 'line-mid'], +const semanticEntityNames: { [key: string]: Array } = { + face: ['wall', 'cap', 'solid2D'], + edge: ['segment', 'sweepEdge', 'edgeCutEdge'], + point: [], } -function getSemanticSelectionType(selectionType: Array) { +function getSemanticSelectionType(selectionType: Array) { const semanticSelectionType = new Set() selectionType.forEach((type) => { Object.entries(semanticEntityNames).forEach(([entity, entityTypes]) => { @@ -49,8 +49,12 @@ function CommandBarSelectionInput({ const [hasSubmitted, setHasSubmitted] = useState(false) const selection = useSelector(arg.machineActor, selectionSelector) const selectionsByType = useMemo(() => { - const selectionRangeEnd = selection?.codeBasedSelections[0]?.range[1] - return !selectionRangeEnd || selectionRangeEnd === code.length + const selectionRangeEnd = !selection + ? null + : selection?.graphSelections[0]?.codeRef?.range[1] + return !selectionRangeEnd || selectionRangeEnd === code.length || !selection + ? 'none' + : !selection ? 'none' : getSelectionType(selection) }, [selection, code]) diff --git a/src/components/CustomIcon.tsx b/src/components/CustomIcon.tsx index 17a2700f3..1ab49edb3 100644 --- a/src/components/CustomIcon.tsx +++ b/src/components/CustomIcon.tsx @@ -1170,15 +1170,15 @@ const CustomIconMap = { xmlns="http://www.w3.org/2000/svg" > diff --git a/src/components/EngineCommands.tsx b/src/components/EngineCommands.tsx index 40ff8785a..ace639518 100644 --- a/src/components/EngineCommands.tsx +++ b/src/components/EngineCommands.tsx @@ -64,7 +64,10 @@ export const EngineCommands = () => { ) })} -
diff --git a/src/components/ModelingMachineProvider.tsx b/src/components/ModelingMachineProvider.tsx index d01f89885..48985ff27 100644 --- a/src/components/ModelingMachineProvider.tsx +++ b/src/components/ModelingMachineProvider.tsx @@ -43,12 +43,12 @@ import { } from './Toolbar/SetAngleBetween' import { applyConstraintAngleLength } from './Toolbar/setAngleLength' import { - Selections, canSweepSelection, handleSelectionBatch, isSelectionLastLine, isRangeBetweenCharacters, isSketchPipe, + Selections, updateSelections, } from 'lib/selections' import { applyConstraintIntersect } from './Toolbar/Intersect' @@ -318,7 +318,7 @@ export const ModelingMachineProvider = ({ }) } let selections: Selections = { - codeBasedSelections: [], + graphSelections: [], otherSelections: [], } if (setSelections.selectionType === 'singleCodeCursor') { @@ -328,7 +328,7 @@ export const ModelingMachineProvider = ({ !editorManager.isShiftDown ) { selections = { - codeBasedSelections: [], + graphSelections: [], otherSelections: [], } } else if ( @@ -336,13 +336,13 @@ export const ModelingMachineProvider = ({ !editorManager.isShiftDown ) { selections = { - codeBasedSelections: [setSelections.selection], + graphSelections: [setSelections.selection], otherSelections: [], } } else if (setSelections.selection && editorManager.isShiftDown) { selections = { - codeBasedSelections: [ - ...selectionRanges.codeBasedSelections, + graphSelections: [ + ...selectionRanges.graphSelections, setSelections.selection, ], otherSelections: selectionRanges.otherSelections, @@ -378,18 +378,18 @@ export const ModelingMachineProvider = ({ if (setSelections.selectionType === 'otherSelection') { if (editorManager.isShiftDown) { selections = { - codeBasedSelections: selectionRanges.codeBasedSelections, + graphSelections: selectionRanges.graphSelections, otherSelections: [setSelections.selection], } } else { selections = { - codeBasedSelections: [], + graphSelections: [], otherSelections: [setSelections.selection], } } const { engineEvents, updateSceneObjectColors } = handleSelectionBatch({ - selections, + selections: selections, }) engineEvents && engineEvents.forEach((event) => { @@ -558,7 +558,7 @@ export const ModelingMachineProvider = ({ // A user can begin extruding if they either have 1+ faces selected or nothing selected // TODO: I believe this guard only allows for extruding a single face at a time const hasNoSelection = - selectionRanges.codeBasedSelections.length === 0 || + selectionRanges.graphSelections.length === 0 || isRangeBetweenCharacters(selectionRanges) || isSelectionLastLine(selectionRanges, codeManager.code) @@ -570,21 +570,24 @@ export const ModelingMachineProvider = ({ } if (!isSketchPipe(selectionRanges)) return false - return canSweepSelection(selectionRanges) + const canSweep = canSweepSelection(selectionRanges) + if (err(canSweep)) return false + return canSweep }, 'has valid selection for deletion': ({ context: { selectionRanges }, }) => { if (!commandBarState.matches('Closed')) return false - if (selectionRanges.codeBasedSelections.length <= 0) return false + if (selectionRanges.graphSelections.length <= 0) return false return true }, - 'has valid fillet selection': ({ context: { selectionRanges } }) => - hasValidFilletSelection({ + 'has valid fillet selection': ({ context: { selectionRanges } }) => { + return hasValidFilletSelection({ selectionRanges, ast: kclManager.ast, code: codeManager.code, - }), + }) + }, 'Selection is on face': ({ context: { selectionRanges }, event }) => { if (event.type !== 'Enter sketch') return false if (event.data?.forceNewSketch) return false @@ -691,7 +694,8 @@ export const ModelingMachineProvider = ({ }), 'animate-to-sketch': fromPromise( async ({ input: { selectionRanges } }) => { - const sourceRange = selectionRanges.codeBasedSelections[0].range + const sourceRange = + selectionRanges.graphSelections[0]?.codeRef?.range const sketchPathToNode = getNodePathFromSourceRange( kclManager.ast, sourceRange @@ -713,6 +717,7 @@ export const ModelingMachineProvider = ({ } } ), + 'Get horizontal info': fromPromise( async ({ input: { selectionRanges, sketchDetails } }) => { const { modifiedAst, pathToNodeMap } = @@ -848,7 +853,9 @@ export const ModelingMachineProvider = ({ 'Get length info': fromPromise( async ({ input: { selectionRanges, sketchDetails } }) => { const { modifiedAst, pathToNodeMap } = - await applyConstraintAngleLength({ selectionRanges }) + await applyConstraintAngleLength({ + selectionRanges, + }) const _modifiedAst = parse(recast(modifiedAst)) if (!sketchDetails) return Promise.reject(new Error('No sketch details')) diff --git a/src/components/Toolbar/EqualAngle.tsx b/src/components/Toolbar/EqualAngle.tsx index 892088bc4..d3df54bed 100644 --- a/src/components/Toolbar/EqualAngle.tsx +++ b/src/components/Toolbar/EqualAngle.tsx @@ -25,8 +25,8 @@ export function equalAngleInfo({ enabled: boolean } | Error { - const paths = selectionRanges.codeBasedSelections.map(({ range }) => - getNodePathFromSourceRange(kclManager.ast, range) + const paths = selectionRanges.graphSelections.map(({ codeRef }) => + getNodePathFromSourceRange(kclManager.ast, codeRef.range) ) const _nodes = paths.map((pathToNode) => { const tmp = getNodeFromPath(kclManager.ast, pathToNode) @@ -64,7 +64,7 @@ export function equalAngleInfo({ const transforms = getTransformInfos( { ...selectionRanges, - codeBasedSelections: selectionRanges.codeBasedSelections.slice(1), + graphSelections: selectionRanges.graphSelections.slice(1), }, kclManager.ast, 'equalAngle' diff --git a/src/components/Toolbar/EqualLength.tsx b/src/components/Toolbar/EqualLength.tsx index 63c96d448..84578ca55 100644 --- a/src/components/Toolbar/EqualLength.tsx +++ b/src/components/Toolbar/EqualLength.tsx @@ -1,10 +1,7 @@ import { toolTips } from 'lang/langHelpers' import { Selections } from 'lib/selections' import { Program, Expr, VariableDeclarator } from '../../lang/wasm' -import { - getNodePathFromSourceRange, - getNodeFromPath, -} from '../../lang/queryAst' +import { getNodeFromPath } from '../../lang/queryAst' import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints' import { transformSecondarySketchLinesTagFirst, @@ -26,11 +23,8 @@ export function setEqualLengthInfo({ enabled: boolean } | Error { - const paths = selectionRanges.codeBasedSelections.map(({ range }) => - getNodePathFromSourceRange(kclManager.ast, range) - ) - const _nodes = paths.map((pathToNode) => { - const tmp = getNodeFromPath(kclManager.ast, pathToNode) + const _nodes = selectionRanges.graphSelections.map(({ codeRef }) => { + const tmp = getNodeFromPath(kclManager.ast, codeRef.pathToNode) if (err(tmp)) return tmp return tmp.node }) @@ -38,10 +32,10 @@ export function setEqualLengthInfo({ if (err(_err1)) return _err1 const nodes = _nodes as Expr[] - const _varDecs = paths.map((pathToNode) => { + const _varDecs = selectionRanges.graphSelections.map(({ codeRef }) => { const tmp = getNodeFromPath( kclManager.ast, - pathToNode, + codeRef.pathToNode, 'VariableDeclarator' ) if (err(tmp)) return tmp @@ -65,7 +59,7 @@ export function setEqualLengthInfo({ const transforms = getTransformInfos( { ...selectionRanges, - codeBasedSelections: selectionRanges.codeBasedSelections.slice(1), + graphSelections: selectionRanges.graphSelections.slice(1), }, kclManager.ast, 'equalLength' diff --git a/src/components/Toolbar/HorzVert.tsx b/src/components/Toolbar/HorzVert.tsx index 967b545ac..d330ff73a 100644 --- a/src/components/Toolbar/HorzVert.tsx +++ b/src/components/Toolbar/HorzVert.tsx @@ -1,10 +1,7 @@ import { toolTips } from 'lang/langHelpers' import { Selections } from 'lib/selections' import { Program, ProgramMemory, Expr } from '../../lang/wasm' -import { - getNodePathFromSourceRange, - getNodeFromPath, -} from '../../lang/queryAst' +import { getNodeFromPath } from '../../lang/queryAst' import { PathToNodeMap, getTransformInfos, @@ -24,11 +21,8 @@ export function horzVertInfo( enabled: boolean } | Error { - const paths = selectionRanges.codeBasedSelections.map(({ range }) => - getNodePathFromSourceRange(kclManager.ast, range) - ) - const _nodes = paths.map((pathToNode) => { - const tmp = getNodeFromPath(kclManager.ast, pathToNode) + const _nodes = selectionRanges.graphSelections.map(({ codeRef }) => { + const tmp = getNodeFromPath(kclManager.ast, codeRef.pathToNode) if (err(tmp)) return tmp return tmp.node }) diff --git a/src/components/Toolbar/Intersect.tsx b/src/components/Toolbar/Intersect.tsx index 6d31cb009..06025fa2f 100644 --- a/src/components/Toolbar/Intersect.tsx +++ b/src/components/Toolbar/Intersect.tsx @@ -1,8 +1,7 @@ import { toolTips } from 'lang/langHelpers' -import { Selections } from 'lib/selections' import { Program, Expr, VariableDeclarator } from '../../lang/wasm' +import { Selections } from 'lib/selections' import { - getNodePathFromSourceRange, getNodeFromPath, isLinesParallelAndConstrained, } from '../../lang/queryAst' @@ -17,7 +16,7 @@ import { TransformInfo } from 'lang/std/stdTypes' import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal' import { createVariableDeclaration } from '../../lang/modifyAst' import { removeDoubleNegatives } from '../AvailableVarsHelpers' -import { kclManager } from 'lib/singletons' +import { engineCommandManager, kclManager } from 'lib/singletons' import { err } from 'lib/trap' import { Node } from 'wasm-lib/kcl/bindings/Node' @@ -34,7 +33,7 @@ export function intersectInfo({ forcedSelectionRanges: Selections } | Error { - if (selectionRanges.codeBasedSelections.length < 2) { + if (selectionRanges.graphSelections.length < 2) { return { enabled: false, transforms: [], @@ -43,38 +42,35 @@ export function intersectInfo({ } const previousSegment = - selectionRanges.codeBasedSelections.length > 1 && + selectionRanges.graphSelections.length > 1 && isLinesParallelAndConstrained( kclManager.ast, + engineCommandManager.artifactGraph, kclManager.programMemory, - selectionRanges.codeBasedSelections[0], - selectionRanges.codeBasedSelections[1] + selectionRanges.graphSelections[0], + selectionRanges.graphSelections[1] ) + if (err(previousSegment)) return previousSegment + const artifact = selectionRanges.graphSelections[1]?.artifact const shouldUsePreviousSegment = - selectionRanges.codeBasedSelections?.[1]?.type !== 'line-end' && + (!artifact || artifact.type === 'segment') && previousSegment && previousSegment.isParallelAndConstrained const _forcedSelectionRanges: typeof selectionRanges = { ...selectionRanges, - codeBasedSelections: [ - selectionRanges.codeBasedSelections?.[0], - shouldUsePreviousSegment - ? { - range: previousSegment.sourceRange, - type: 'line-end', - } - : selectionRanges.codeBasedSelections?.[1], + graphSelections: [ + selectionRanges.graphSelections?.[0], + shouldUsePreviousSegment && previousSegment.selection + ? previousSegment.selection + : selectionRanges.graphSelections?.[1], ], } - const paths = _forcedSelectionRanges.codeBasedSelections.map(({ range }) => - getNodePathFromSourceRange(kclManager.ast, range) - ) - const _nodes = paths.map((pathToNode) => { - const tmp = getNodeFromPath(kclManager.ast, pathToNode) + const _nodes = _forcedSelectionRanges.graphSelections.map(({ codeRef }) => { + const tmp = getNodeFromPath(kclManager.ast, codeRef.pathToNode) if (err(tmp)) return tmp return tmp.node }) @@ -82,10 +78,10 @@ export function intersectInfo({ if (err(_err1)) return _err1 const nodes = _nodes as Expr[] - const _varDecs = paths.map((pathToNode) => { + const _varDecs = _forcedSelectionRanges.graphSelections.map(({ codeRef }) => { const tmp = getNodeFromPath( kclManager.ast, - pathToNode, + codeRef.pathToNode, 'VariableDeclarator' ) if (err(tmp)) return tmp @@ -112,18 +108,19 @@ export function intersectInfo({ const theTransforms = getTransformInfos( { ...selectionRanges, - codeBasedSelections: _forcedSelectionRanges.codeBasedSelections.slice(1), + graphSelections: _forcedSelectionRanges.graphSelections.slice(1), }, kclManager.ast, 'intersect' ) + const forcedArtifact = _forcedSelectionRanges?.graphSelections?.[1]?.artifact const _enableEqual = secondaryVarDecs.length === 1 && isAllTooltips && isOthersLinkedToPrimary && theTransforms.every(Boolean) && - _forcedSelectionRanges?.codeBasedSelections?.[1]?.type === 'line-end' + (!forcedArtifact || forcedArtifact.type === 'segment') return { enabled: _enableEqual, diff --git a/src/components/Toolbar/RemoveConstrainingValues.tsx b/src/components/Toolbar/RemoveConstrainingValues.tsx index 2f9e6c5da..d5d1efde8 100644 --- a/src/components/Toolbar/RemoveConstrainingValues.tsx +++ b/src/components/Toolbar/RemoveConstrainingValues.tsx @@ -1,10 +1,7 @@ import { toolTips } from 'lang/langHelpers' import { Selection, Selections } from 'lib/selections' import { PathToNode, Program, Expr } from '../../lang/wasm' -import { - getNodePathFromSourceRange, - getNodeFromPath, -} from '../../lang/queryAst' +import { getNodeFromPath } from '../../lang/queryAst' import { PathToNodeMap, getRemoveConstraintsTransforms, @@ -14,6 +11,7 @@ import { TransformInfo } from 'lang/std/stdTypes' import { kclManager } from 'lib/singletons' import { err } from 'lib/trap' import { Node } from 'wasm-lib/kcl/bindings/Node' +import { codeRefFromRange } from 'lang/std/artifactGraph' export function removeConstrainingValuesInfo({ selectionRanges, @@ -28,13 +26,8 @@ export function removeConstrainingValuesInfo({ updatedSelectionRanges: Selections } | Error { - const paths = - pathToNodes || - selectionRanges.codeBasedSelections.map(({ range }) => - getNodePathFromSourceRange(kclManager.ast, range) - ) - const _nodes = paths.map((pathToNode) => { - const tmp = getNodeFromPath(kclManager.ast, pathToNode) + const _nodes = selectionRanges.graphSelections.map(({ codeRef }) => { + const tmp = getNodeFromPath(kclManager.ast, codeRef.pathToNode) if (err(tmp)) return tmp return tmp.node }) @@ -45,10 +38,9 @@ export function removeConstrainingValuesInfo({ const updatedSelectionRanges = pathToNodes ? { otherSelections: [], - codeBasedSelections: nodes.map( + graphSelections: nodes.map( (node): Selection => ({ - range: [node.start, node.end], - type: 'default', + codeRef: codeRefFromRange([node.start, node.end], kclManager.ast), }) ), } diff --git a/src/components/Toolbar/SetAbsDistance.tsx b/src/components/Toolbar/SetAbsDistance.tsx index 4da63e473..f1b9652d6 100644 --- a/src/components/Toolbar/SetAbsDistance.tsx +++ b/src/components/Toolbar/SetAbsDistance.tsx @@ -1,10 +1,7 @@ import { toolTips } from 'lang/langHelpers' -import { Selections } from 'lib/selections' import { Program, Expr } from '../../lang/wasm' -import { - getNodePathFromSourceRange, - getNodeFromPath, -} from '../../lang/queryAst' +import { Selections } from 'lib/selections' +import { getNodeFromPath } from '../../lang/queryAst' import { getTransformInfos, transformAstSketchLines, @@ -47,13 +44,10 @@ export function absDistanceInfo({ : constraint === 'snapToYAxis' ? 'xAbs' : 'yAbs' - const paths = selectionRanges.codeBasedSelections.map(({ range }) => - getNodePathFromSourceRange(kclManager.ast, range) - ) - const _nodes = paths.map((pathToNode) => { + const _nodes = selectionRanges.graphSelections.map(({ codeRef }) => { const tmp = getNodeFromPath( kclManager.ast, - pathToNode, + codeRef.pathToNode, 'CallExpression' ) if (err(tmp)) return tmp @@ -84,7 +78,7 @@ export function absDistanceInfo({ const enabled = isAllTooltips && transforms.every(Boolean) && - selectionRanges.codeBasedSelections.length === 1 && + selectionRanges.graphSelections.length === 1 && (enableX || enableY) return { enabled, transforms } @@ -109,7 +103,7 @@ export async function applyConstraintAbsDistance({ const transform1 = transformAstSketchLines({ ast: structuredClone(kclManager.ast), - selectionRanges: selectionRanges, + selectionRanges, transformInfos, programMemory: kclManager.programMemory, referenceSegName: '', @@ -129,7 +123,7 @@ export async function applyConstraintAbsDistance({ const transform2 = transformAstSketchLines({ ast: structuredClone(kclManager.ast), - selectionRanges: selectionRanges, + selectionRanges, transformInfos, programMemory: kclManager.programMemory, referenceSegName: '', @@ -177,7 +171,7 @@ export function applyConstraintAxisAlign({ return transformAstSketchLines({ ast: structuredClone(kclManager.ast), - selectionRanges: selectionRanges, + selectionRanges, transformInfos, programMemory: kclManager.programMemory, referenceSegName: '', diff --git a/src/components/Toolbar/SetAngleBetween.tsx b/src/components/Toolbar/SetAngleBetween.tsx index 17296fcf0..14a0fe72a 100644 --- a/src/components/Toolbar/SetAngleBetween.tsx +++ b/src/components/Toolbar/SetAngleBetween.tsx @@ -1,10 +1,7 @@ import { toolTips } from 'lang/langHelpers' -import { Selections } from 'lib/selections' import { Program, Expr, VariableDeclarator } from '../../lang/wasm' -import { - getNodePathFromSourceRange, - getNodeFromPath, -} from '../../lang/queryAst' +import { Selections } from 'lib/selections' +import { getNodeFromPath } from '../../lang/queryAst' import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints' import { transformSecondarySketchLinesTagFirst, @@ -31,12 +28,8 @@ export function angleBetweenInfo({ enabled: boolean } | Error { - const paths = selectionRanges.codeBasedSelections.map(({ range }) => - getNodePathFromSourceRange(kclManager.ast, range) - ) - - const _nodes = paths.map((pathToNode) => { - const tmp = getNodeFromPath(kclManager.ast, pathToNode) + const _nodes = selectionRanges.graphSelections.map(({ codeRef }) => { + const tmp = getNodeFromPath(kclManager.ast, codeRef.pathToNode) if (err(tmp)) return tmp return tmp.node }) @@ -44,10 +37,10 @@ export function angleBetweenInfo({ if (err(_err1)) return _err1 const nodes = _nodes as Expr[] - const _varDecs = paths.map((pathToNode) => { + const _varDecs = selectionRanges.graphSelections.map(({ codeRef }) => { const tmp = getNodeFromPath( kclManager.ast, - pathToNode, + codeRef.pathToNode, 'VariableDeclarator' ) if (err(tmp)) return tmp @@ -71,7 +64,7 @@ export function angleBetweenInfo({ const theTransforms = getTransformInfos( { ...selectionRanges, - codeBasedSelections: selectionRanges.codeBasedSelections.slice(1), + graphSelections: selectionRanges.graphSelections.slice(1), }, kclManager.ast, 'setAngleBetween' @@ -88,10 +81,8 @@ export function angleBetweenInfo({ export async function applyConstraintAngleBetween({ selectionRanges, -}: // constraint, -{ +}: { selectionRanges: Selections - // constraint: 'setHorzDistance' | 'setVertDistance' }): Promise<{ modifiedAst: Program pathToNodeMap: PathToNodeMap diff --git a/src/components/Toolbar/SetHorzVertDistance.tsx b/src/components/Toolbar/SetHorzVertDistance.tsx index a874fa14c..172ebfa79 100644 --- a/src/components/Toolbar/SetHorzVertDistance.tsx +++ b/src/components/Toolbar/SetHorzVertDistance.tsx @@ -1,9 +1,6 @@ import { toolTips } from 'lang/langHelpers' import { Program, Expr, VariableDeclarator } from '../../lang/wasm' -import { - getNodePathFromSourceRange, - getNodeFromPath, -} from '../../lang/queryAst' +import { getNodeFromPath } from '../../lang/queryAst' import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints' import { transformSecondarySketchLinesTagFirst, @@ -34,11 +31,8 @@ export function horzVertDistanceInfo({ enabled: boolean } | Error { - const paths = selectionRanges.codeBasedSelections.map(({ range }) => - getNodePathFromSourceRange(kclManager.ast, range) - ) - const _nodes = paths.map((pathToNode) => { - const tmp = getNodeFromPath(kclManager.ast, pathToNode) + const _nodes = selectionRanges.graphSelections.map(({ codeRef }) => { + const tmp = getNodeFromPath(kclManager.ast, codeRef.pathToNode) if (err(tmp)) return tmp return tmp.node }) @@ -47,10 +41,10 @@ export function horzVertDistanceInfo({ if (hasErr) return nodesWErrs[0] const nodes = _nodes as Expr[] - const _varDecs = paths.map((pathToNode) => { + const _varDecs = selectionRanges.graphSelections.map(({ codeRef }) => { const tmp = getNodeFromPath( kclManager.ast, - pathToNode, + codeRef.pathToNode, 'VariableDeclarator' ) if (err(tmp)) return tmp @@ -77,7 +71,7 @@ export function horzVertDistanceInfo({ const theTransforms = getTransformInfos( { ...selectionRanges, - codeBasedSelections: selectionRanges.codeBasedSelections.slice(1), + graphSelections: selectionRanges.graphSelections.slice(1), }, kclManager.ast, constraint @@ -104,7 +98,7 @@ export async function applyConstraintHorzVertDistance({ pathToNodeMap: PathToNodeMap }> { const info = horzVertDistanceInfo({ - selectionRanges, + selectionRanges: selectionRanges, constraint, }) if (err(info)) return Promise.reject(info) diff --git a/src/components/Toolbar/setAngleLength.tsx b/src/components/Toolbar/setAngleLength.tsx index 70c68c913..a0744735d 100644 --- a/src/components/Toolbar/setAngleLength.tsx +++ b/src/components/Toolbar/setAngleLength.tsx @@ -1,10 +1,7 @@ import { toolTips } from 'lang/langHelpers' -import { Selections } from 'lib/selections' import { Program, Expr } from '../../lang/wasm' -import { - getNodePathFromSourceRange, - getNodeFromPath, -} from '../../lang/queryAst' +import { Selections } from 'lib/selections' +import { getNodeFromPath } from '../../lang/queryAst' import { PathToNodeMap, getTransformInfos, @@ -40,15 +37,11 @@ export function angleLengthInfo({ enabled: boolean } | Error { - const paths = selectionRanges.codeBasedSelections.map(({ range }) => - getNodePathFromSourceRange(kclManager.ast, range) - ) - - const nodes = paths.map((pathToNode) => - getNodeFromPath(kclManager.ast, pathToNode, 'CallExpression') + const nodes = selectionRanges.graphSelections.map(({ codeRef }) => + getNodeFromPath(kclManager.ast, codeRef.pathToNode, 'CallExpression') ) const _err1 = nodes.find(err) - if (err(_err1)) return _err1 + if (_err1 instanceof Error) return _err1 const isAllTooltips = nodes.every((meta) => { if (err(meta)) return false @@ -64,7 +57,7 @@ export function angleLengthInfo({ angleOrLength ) const enabled = - selectionRanges.codeBasedSelections.length <= 1 && + selectionRanges.graphSelections.length <= 1 && isAllTooltips && transforms.every(Boolean) return { enabled, transforms } diff --git a/src/editor/manager.ts b/src/editor/manager.ts index 5eda79470..0c786f44d 100644 --- a/src/editor/manager.ts +++ b/src/editor/manager.ts @@ -1,9 +1,9 @@ import { EditorView, ViewUpdate } from '@codemirror/view' import { syntaxTree } from '@codemirror/language' import { EditorSelection, Annotation, Transaction } from '@codemirror/state' -import { engineCommandManager } from 'lib/singletons' +import { engineCommandManager, kclManager } from 'lib/singletons' import { modelingMachine, ModelingMachineEvent } from 'machines/modelingMachine' -import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections' +import { Selections, Selection, processCodeMirrorRanges } from 'lib/selections' import { undo, redo } from '@codemirror/commands' import { CommandBarMachineEvent } from 'machines/commandBarMachine' import { addLineHighlight, addLineHighlightEvent } from './highlightextension' @@ -31,7 +31,7 @@ export default class EditorManager { private _isShiftDown: boolean = false private _selectionRanges: Selections = { otherSelections: [], - codeBasedSelections: [], + graphSelections: [], } private _lastEvent: { event: string; time: number } | null = null @@ -138,10 +138,10 @@ export default class EditorManager { return this._highlightRange } - setHighlightRange(selections: Array): void { - this._highlightRange = selections + setHighlightRange(range: Array): void { + this._highlightRange = range - const selectionsWithSafeEnds = selections.map((s): [number, number] => { + const selectionsWithSafeEnds = range.map((s): [number, number] => { const safeEnd = Math.min(s[1], this._editorView?.state.doc.length || s[1]) return [s[0], safeEnd] }) @@ -254,21 +254,23 @@ export default class EditorManager { } selectRange(selections: Selections) { - if (selections.codeBasedSelections.length === 0) { + if (selections?.graphSelections?.length === 0) { return } let codeBasedSelections = [] - for (const selection of selections.codeBasedSelections) { + for (const selection of selections.graphSelections) { codeBasedSelections.push( - EditorSelection.range(selection.range[0], selection.range[1]) + EditorSelection.range( + selection.codeRef.range[0], + selection.codeRef.range[1] + ) ) } codeBasedSelections.push( EditorSelection.cursor( - selections.codeBasedSelections[ - selections.codeBasedSelections.length - 1 - ].range[1] + selections.graphSelections[selections.graphSelections.length - 1] + .codeRef.range[1] ) ) @@ -311,6 +313,7 @@ export default class EditorManager { codeMirrorRanges: viewUpdate.state.selection.ranges, selectionRanges: this._selectionRanges, isShiftDown: this._isShiftDown, + ast: kclManager.ast, }) if (!eventInfo) { diff --git a/src/hooks/useEngineConnectionSubscriptions.ts b/src/hooks/useEngineConnectionSubscriptions.ts index 0640023ad..f7d3ff8e8 100644 --- a/src/hooks/useEngineConnectionSubscriptions.ts +++ b/src/hooks/useEngineConnectionSubscriptions.ts @@ -1,4 +1,4 @@ -import { useEffect } from 'react' +import { useEffect, useRef } from 'react' import { editorManager, engineCommandManager, @@ -9,11 +9,9 @@ import { useModelingContext } from './useModelingContext' import { getEventForSelectWithPoint } from 'lib/selections' import { getCapCodeRef, - getSweepEdgeCodeRef, getSweepFromSuspectedSweepSurface, - getEdgeCuteConsumedCodeRef, - getSolid2dCodeRef, getWallCodeRef, + getCodeRefsByArtifactId, getArtifactOfTypes, SegmentArtifact, } from 'lang/std/artifactGraph' @@ -25,6 +23,8 @@ import { EdgeCutInfo, ExtrudeFacePlane } from 'machines/modelingMachine' export function useEngineConnectionSubscriptions() { const { send, context, state } = useModelingContext() + const stateRef = useRef(state) + stateRef.current = state useEffect(() => { if (!engineCommandManager) return @@ -34,66 +34,12 @@ export function useEngineConnectionSubscriptions() { event: 'highlight_set_entity', callback: ({ data }) => { if (data?.entity_id) { - const artifact = engineCommandManager.artifactGraph.get( - data.entity_id + const codeRefs = getCodeRefsByArtifactId( + data.entity_id, + engineCommandManager.artifactGraph ) - if (artifact?.type === 'solid2D') { - const codeRef = getSolid2dCodeRef( - artifact, - engineCommandManager.artifactGraph - ) - if (err(codeRef)) return - editorManager.setHighlightRange([codeRef.range]) - } else if (artifact?.type === 'cap') { - const codeRef = getCapCodeRef( - artifact, - engineCommandManager.artifactGraph - ) - if (err(codeRef)) return - editorManager.setHighlightRange([codeRef.range]) - } else if (artifact?.type === 'wall') { - const extrusion = getSweepFromSuspectedSweepSurface( - data.entity_id, - engineCommandManager.artifactGraph - ) - const codeRef = getWallCodeRef( - artifact, - engineCommandManager.artifactGraph - ) - if (err(codeRef)) return - editorManager.setHighlightRange( - err(extrusion) - ? [codeRef.range] - : [codeRef.range, extrusion.codeRef.range] - ) - } else if (artifact?.type === 'sweepEdge') { - const codeRef = getSweepEdgeCodeRef( - artifact, - engineCommandManager.artifactGraph - ) - if (err(codeRef)) return - editorManager.setHighlightRange([codeRef.range]) - } else if (artifact?.type === 'segment') { - editorManager.setHighlightRange([ - artifact?.codeRef?.range || [0, 0], - ]) - } else if (artifact?.type === 'edgeCut') { - const codeRef = artifact.codeRef - const consumedCodeRef = getEdgeCuteConsumedCodeRef( - artifact, - engineCommandManager.artifactGraph - ) - editorManager.setHighlightRange( - err(consumedCodeRef) - ? [codeRef.range] - : [codeRef.range, consumedCodeRef.range] - ) - } else if (artifact?.type === 'plane') { - const codeRef = artifact.codeRef - if (err(codeRef)) return - editorManager.setHighlightRange([codeRef.range]) - } else { - editorManager.setHighlightRange([[0, 0]]) + if (codeRefs) { + editorManager.setHighlightRange(codeRefs.map(({ range }) => range)) } } else if ( !editorManager.highlightRange || @@ -108,6 +54,7 @@ export function useEngineConnectionSubscriptions() { event: 'select_with_point', callback: (engineEvent) => { ;(async () => { + if (stateRef.current.matches('Sketch no face')) return const event = await getEventForSelectWithPoint(engineEvent) event && send(event) })().catch(reportRejection) diff --git a/src/hooks/useToolbarGuards.ts b/src/hooks/useToolbarGuards.ts index 8d8ae9678..497a32e27 100644 --- a/src/hooks/useToolbarGuards.ts +++ b/src/hooks/useToolbarGuards.ts @@ -28,14 +28,16 @@ export function useConvertToVariable(range?: SourceRange) { const meta = isNodeSafeToReplace( parsed, - range || context.selectionRanges.codeBasedSelections?.[0]?.range || [] + range || + context.selectionRanges.graphSelections?.[0]?.codeRef?.range || + [] ) if (trap(meta)) return const { isSafe, value } = meta const canReplace = isSafe && value.type !== 'Identifier' const isOnlyOneSelection = - !!range || context.selectionRanges.codeBasedSelections.length === 1 + !!range || context.selectionRanges.graphSelections.length === 1 setEnabled(canReplace && isOnlyOneSelection) }, [context.selectionRanges]) @@ -52,7 +54,7 @@ export function useConvertToVariable(range?: SourceRange) { moveValueIntoNewVariable( ast, kclManager.programMemory, - range || context.selectionRanges.codeBasedSelections[0].range, + range || context.selectionRanges.graphSelections[0]?.codeRef?.range, variableName ) diff --git a/src/lang/KclSingleton.ts b/src/lang/KclSingleton.ts index 8f3363eff..1a0d3b061 100644 --- a/src/lang/KclSingleton.ts +++ b/src/lang/KclSingleton.ts @@ -463,7 +463,7 @@ export class KclManager { if (optionalParams?.focusPath) { returnVal = { - codeBasedSelections: [], + graphSelections: [], otherSelections: [], } @@ -485,9 +485,11 @@ export class KclManager { } if (start && end) { - returnVal.codeBasedSelections.push({ - type: 'default', - range: [start, end], + returnVal.graphSelections.push({ + codeRef: { + range: [start, end], + pathToNode: path, + }, }) } } diff --git a/src/lang/langHelpers.ts b/src/lang/langHelpers.ts index f6eddeaf5..9a07aae98 100644 --- a/src/lang/langHelpers.ts +++ b/src/lang/langHelpers.ts @@ -81,7 +81,7 @@ export async function executeAst({ false )) - await engineCommandManager.waitForAllCommands() + await engineCommandManager.waitForAllCommands(useFakeExecutor) return { logs: [], errors: [], diff --git a/src/lang/modifyAst.test.ts b/src/lang/modifyAst.test.ts index 7dd459261..dd9f0863f 100644 --- a/src/lang/modifyAst.test.ts +++ b/src/lang/modifyAst.test.ts @@ -22,6 +22,7 @@ import { findUsesOfTagInPipe, getNodePathFromSourceRange } from './queryAst' import { err } from 'lib/trap' import { SimplifiedArgDetails } from './std/stdTypes' import { Node } from 'wasm-lib/kcl/bindings/Node' +import { Artifact, codeRefFromRange } from './std/artifactGraph' beforeAll(async () => { await initPromise @@ -735,7 +736,7 @@ sketch003 = startSketchOn('XZ') |> close(%)`, codeAfter: `myVar = 5\n`, lineOfInterest: 'line([-2.94, 2.7], %)', - type: 'default', + type: 'segment', }, ], [ @@ -761,7 +762,7 @@ const extrude001 = extrude(10, sketch001)`, |> line([-17.67, 0.85], %) |> close(%)\n`, lineOfInterest: 'line([2.66, 1.17], %)', - type: 'extrude-wall', + type: 'wall', }, ], [ @@ -817,7 +818,7 @@ sketch002 = startSketchOn({ |> close(%) `, lineOfInterest: 'line([-11.18, -2.15], %)', - type: 'extrude-wall', + type: 'wall', }, ], [ @@ -873,7 +874,7 @@ sketch002 = startSketchOn({ |> close(%) `, lineOfInterest: 'startProfileAt([4.46, 5.12], %, $tag)', - type: 'end-cap', + type: 'cap', }, ], ] as const @@ -890,11 +891,12 @@ sketch002 = startSketchOn({ codeBefore.indexOf(lineOfInterest), codeBefore.indexOf(lineOfInterest) + lineOfInterest.length, ] + const artifact = { type } as Artifact const newAst = await deleteFromSelection( ast, { - range, - type, + codeRef: codeRefFromRange(range, ast), + artifact, }, execState.memory, async () => { diff --git a/src/lang/modifyAst.ts b/src/lang/modifyAst.ts index 8ef1aef1e..7eb1b47b4 100644 --- a/src/lang/modifyAst.ts +++ b/src/lang/modifyAst.ts @@ -1,5 +1,5 @@ -import { Selection } from 'lib/selections' import { err, reportRejection, trap } from 'lib/trap' +import { Selection } from 'lib/selections' import { Program, CallExpression, @@ -836,7 +836,7 @@ export function createBinaryExpressionWithUnary([left, right]: [ export function giveSketchFnCallTag( ast: Node, - range: Selection['range'], + range: SourceRange, tag?: string ): | { @@ -910,7 +910,7 @@ export function moveValueIntoNewVariablePath( export function moveValueIntoNewVariable( ast: Node, programMemory: ProgramMemory, - sourceRange: Selection['range'], + sourceRange: SourceRange, variableName: string ): { modifiedAst: Node @@ -1035,18 +1035,15 @@ export async function deleteFromSelection( ({} as any) ): Promise | Error> { const astClone = structuredClone(ast) - const range = selection.range - const path = getNodePathFromSourceRange(ast, range) const varDec = getNodeFromPath( ast, - path, + selection?.codeRef?.pathToNode, 'VariableDeclarator' ) if (err(varDec)) return varDec if ( - (selection.type === 'extrude-wall' || - selection.type === 'end-cap' || - selection.type === 'start-cap') && + (selection?.artifact?.type === 'wall' || + selection?.artifact?.type === 'cap') && varDec.node.init.type === 'PipeExpression' ) { const varDecName = varDec.node.id.name @@ -1126,7 +1123,6 @@ export async function deleteFromSelection( sketchName ) if (err(sketchToPreserve)) return sketchToPreserve - console.log('sketchName', sketchName) // Can't kick off multiple requests at once as getFaceDetails // is three engine calls in one and they conflict const faceDetails = await getFaceDetails(sketchToPreserve.on.id) diff --git a/src/lang/modifyAst/addFillet.test.ts b/src/lang/modifyAst/addFillet.test.ts index f0648d643..e15ddf8aa 100644 --- a/src/lang/modifyAst/addFillet.test.ts +++ b/src/lang/modifyAst/addFillet.test.ts @@ -13,7 +13,7 @@ import { getPathToExtrudeForSegmentSelection, hasValidFilletSelection, isTagUsedInFillet, - modifyAstCloneWithFilletAndTag, + modifyAstWithFilletAndTag, } from './addFillet' import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst' import { createLiteral } from 'lang/modifyAst' @@ -22,6 +22,8 @@ import { Selections } from 'lib/selections' import { engineCommandManager, kclManager } from 'lib/singletons' import { VITE_KC_DEV_TOKEN } from 'env' import { KclCommandValue } from 'lib/commandTypes' +import { isOverlap } from 'lib/utils' +import { codeRefFromRange } from 'lang/std/artifactGraph' beforeAll(async () => { await initPromise @@ -114,10 +116,9 @@ const runGetPathToExtrudeForSegmentSelectionTest = async ( code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length, ] const selection: Selections = { - codeBasedSelections: [ + graphSelections: [ { - range: segmentRange, - type: 'default', + codeRef: codeRefFromRange(segmentRange, ast), }, ], otherSelections: [], @@ -272,13 +273,6 @@ const runModifyAstCloneWithFilletAndTag = async ( code.indexOf(selectionSnippet) + selectionSnippet.length, ] ) - const selection: Selections = { - codeBasedSelections: segmentRanges.map((segmentRange) => ({ - range: segmentRange, - type: 'default', - })), - otherSelections: [], - } // radius const radius: KclCommandValue = { @@ -289,9 +283,24 @@ const runModifyAstCloneWithFilletAndTag = async ( // executeAst await kclManager.executeAst({ ast }) + const artifactGraph = engineCommandManager.artifactGraph + + const selection: Selections = { + graphSelections: segmentRanges.map((segmentRange) => { + const maybeArtifact = [...artifactGraph].find(([, a]) => { + if (!('codeRef' in a)) return false + return isOverlap(a.codeRef.range, segmentRange) + }) + return { + codeRef: codeRefFromRange(segmentRange, ast), + artifact: maybeArtifact ? maybeArtifact[1] : undefined, + } + }), + otherSelections: [], + } // apply fillet to selection - const result = modifyAstCloneWithFilletAndTag(ast, selection, radius) + const result = modifyAstWithFilletAndTag(ast, selection, radius) if (err(result)) { return result } @@ -612,7 +621,6 @@ describe('Testing button states', () => { } const ast = astOrError - // selectionRanges const range: [number, number] = segmentSnippet ? [ code.indexOf(segmentSnippet), @@ -621,10 +629,9 @@ describe('Testing button states', () => { : [ast.end, ast.end] // empty line in the end of the code const selectionRanges: Selections = { - codeBasedSelections: [ + graphSelections: [ { - range, - type: 'default', + codeRef: codeRefFromRange(range, ast), }, ], otherSelections: [], diff --git a/src/lang/modifyAst/addFillet.ts b/src/lang/modifyAst/addFillet.ts index e2a05616c..398634cf4 100644 --- a/src/lang/modifyAst/addFillet.ts +++ b/src/lang/modifyAst/addFillet.ts @@ -32,6 +32,7 @@ import { err, trap } from 'lib/trap' import { Selections } from 'lib/selections' import { KclCommandValue } from 'lib/commandTypes' import { + Artifact, ArtifactGraph, getSweepFromSuspectedPath, } from 'lang/std/artifactGraph' @@ -51,7 +52,7 @@ export function applyFilletToSelection( radius: KclCommandValue ): void | Error { // 1. clone and modify with fillet and tag - const result = modifyAstCloneWithFilletAndTag(ast, selection, radius) + const result = modifyAstWithFilletAndTag(ast, selection, radius) if (err(result)) return result const { modifiedAst, pathToFilletNode } = result @@ -60,9 +61,9 @@ export function applyFilletToSelection( updateAstAndFocus(modifiedAst, pathToFilletNode) } -export function modifyAstCloneWithFilletAndTag( +export function modifyAstWithFilletAndTag( ast: Node, - selection: Selections, + selections: Selections, radius: KclCommandValue ): { modifiedAst: Node; pathToFilletNode: Array } | Error { let clonedAst = structuredClone(ast) @@ -76,16 +77,15 @@ export function modifyAstCloneWithFilletAndTag( // Step 1: modify ast with tags and group them by extrude nodes (bodies) const extrudeToTagsMap: Map< PathToNode, - Array<{ tag: string; selectionType: string }> + Array<{ tag: string; artifact: Artifact }> > = new Map() const lookupMap: Map = new Map() // work around for Map key comparison - for (const selectionRange of selection.codeBasedSelections) { + for (const selection of selections.graphSelections) { const singleSelection = { - codeBasedSelections: [selectionRange], + graphSelections: [selection], otherSelections: [], } - const selectionType = singleSelection.codeBasedSelections[0].type const result = getPathToExtrudeForSegmentSelection( clonedAstForGetExtrude, @@ -101,18 +101,21 @@ export function modifyAstCloneWithFilletAndTag( ) if (err(tagResult)) return tagResult const { tag } = tagResult - const tagInfo = { tag, selectionType } // Group tags by their corresponding extrude node const extrudeKey = JSON.stringify(pathToExtrudeNode) - if (lookupMap.has(extrudeKey)) { + if (lookupMap.has(extrudeKey) && selection.artifact) { const existingPath = lookupMap.get(extrudeKey) if (!existingPath) return new Error('Path to extrude node not found.') - extrudeToTagsMap.get(existingPath)?.push(tagInfo) - } else { + extrudeToTagsMap + .get(existingPath) + ?.push({ tag, artifact: selection.artifact } as const) + } else if (selection.artifact) { lookupMap.set(extrudeKey, pathToExtrudeNode) - extrudeToTagsMap.set(pathToExtrudeNode, [tagInfo]) + extrudeToTagsMap.set(pathToExtrudeNode, [ + { tag, artifact: selection.artifact } as const, + ]) } } @@ -123,8 +126,8 @@ export function modifyAstCloneWithFilletAndTag( const radiusValue = 'variableName' in radius ? radius.variableIdentifierAst : radius.valueAst - const tagCalls = tagInfos.map(({ tag, selectionType }) => { - return getEdgeTagCall(tag, selectionType) + const tagCalls = tagInfos.map(({ tag, artifact }) => { + return getEdgeTagCall(tag, artifact) }) const firstTag = tagCalls[0] // can be Identifier or CallExpression (for opposite and adjacent edges) @@ -214,7 +217,7 @@ export function getPathToExtrudeForSegmentSelection( ): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error { const pathToSegmentNode = getNodePathFromSourceRange( ast, - selection.codeBasedSelections[0].range + selection.graphSelections[0]?.codeRef?.range ) const varDecNode = getNodeFromPath( @@ -292,14 +295,14 @@ function mutateAstWithTagForSketchSegment( function getEdgeTagCall( tag: string, - selectionType: string + artifact: Artifact ): Node { let tagCall: Expr = createIdentifier(tag) // Modify the tag based on selectionType - if (selectionType === 'edge') { + if (artifact.type === 'sweepEdge' && artifact.subType === 'opposite') { tagCall = createCallExpressionStdLib('getOppositeEdge', [tagCall]) - } else if (selectionType === 'adjacent-edge') { + } else if (artifact.type === 'sweepEdge' && artifact.subType === 'adjacent') { tagCall = createCallExpressionStdLib('getNextAdjacentEdge', [tagCall]) } return tagCall @@ -442,22 +445,21 @@ export const hasValidFilletSelection = ({ if (!extrudeExists) return false // check if nothing is selected - if (selectionRanges.codeBasedSelections.length === 0) { + if (selectionRanges.graphSelections.length === 0) { return true } // check if selection is last string in code - if (selectionRanges.codeBasedSelections[0].range[0] === code.length) { + if (selectionRanges.graphSelections[0]?.codeRef?.range[0] === code.length) { return true } // selection exists: - for (const selection of selectionRanges.codeBasedSelections) { + for (const selection of selectionRanges.graphSelections) { // check if all selections are in sketchLineHelperMap - const path = getNodePathFromSourceRange(ast, selection.range) const segmentNode = getNodeFromPath>( ast, - path, + selection.codeRef.pathToNode, 'CallExpression' ) if (err(segmentNode)) return false @@ -493,9 +495,9 @@ export const hasValidFilletSelection = ({ }) // check if tag is used in fillet - if (tagExists) { + if (tagExists && selection.artifact) { // create tag call - let tagCall: Expr = getEdgeTagCall(tag, selection.type) + let tagCall: Expr = getEdgeTagCall(tag, selection.artifact) // check if tag is used in fillet let inFillet = false diff --git a/src/lang/queryAst.test.ts b/src/lang/queryAst.test.ts index 58491a4d2..679cc6b0e 100644 --- a/src/lang/queryAst.test.ts +++ b/src/lang/queryAst.test.ts @@ -20,6 +20,7 @@ import { createPipeSubstitution, } from './modifyAst' import { err } from 'lib/trap' +import { codeRefFromRange } from './std/artifactGraph' beforeAll(async () => { await initPromise @@ -365,7 +366,9 @@ part001 = startSketchAt([-1.41, 3.46]) const result = doesPipeHaveCallExp({ calleeName: 'close', ast, - selection: { type: 'default', range: [100, 101] }, + selection: { + codeRef: codeRefFromRange([100, 101], ast), + }, }) expect(result).toEqual(true) }) @@ -385,7 +388,9 @@ part001 = startSketchAt([-1.41, 3.46]) const result = doesPipeHaveCallExp({ calleeName: 'extrude', ast, - selection: { type: 'default', range: [100, 101] }, + selection: { + codeRef: codeRefFromRange([100, 101], ast), + }, }) expect(result).toEqual(true) }) @@ -403,7 +408,9 @@ part001 = startSketchAt([-1.41, 3.46]) const result = doesPipeHaveCallExp({ calleeName: 'close', ast, - selection: { type: 'default', range: [100, 101] }, + selection: { + codeRef: codeRefFromRange([100, 101], ast), + }, }) expect(result).toEqual(false) }) @@ -415,7 +422,9 @@ part001 = startSketchAt([-1.41, 3.46]) const result = doesPipeHaveCallExp({ calleeName: 'close', ast, - selection: { type: 'default', range: [9, 10] }, + selection: { + codeRef: codeRefFromRange([9, 10], ast), + }, }) expect(result).toEqual(false) }) @@ -435,7 +444,9 @@ part001 = startSketchAt([-1.41, 3.46]) const execState = await enginelessExecutor(ast) const result = hasExtrudeSketch({ ast, - selection: { type: 'default', range: [100, 101] }, + selection: { + codeRef: codeRefFromRange([100, 101], ast), + }, programMemory: execState.memory, }) expect(result).toEqual(true) @@ -454,7 +465,9 @@ part001 = startSketchAt([-1.41, 3.46]) const execState = await enginelessExecutor(ast) const result = hasExtrudeSketch({ ast, - selection: { type: 'default', range: [100, 101] }, + selection: { + codeRef: codeRefFromRange([100, 101], ast), + }, programMemory: execState.memory, }) expect(result).toEqual(true) @@ -467,7 +480,9 @@ part001 = startSketchAt([-1.41, 3.46]) const execState = await enginelessExecutor(ast) const result = hasExtrudeSketch({ ast, - selection: { type: 'default', range: [10, 11] }, + selection: { + codeRef: codeRefFromRange([10, 11], ast), + }, programMemory: execState.memory, }) expect(result).toEqual(false) @@ -556,8 +571,7 @@ sketch003 = startSketchOn(extrude001, 'END') exampleCode.indexOf(lineOfInterest) + lineOfInterest.length const extruded = hasSketchPipeBeenExtruded( { - range: [characterIndex, characterIndex], - type: 'default', + codeRef: codeRefFromRange([characterIndex, characterIndex], ast), }, ast ) @@ -571,8 +585,7 @@ sketch003 = startSketchOn(extrude001, 'END') exampleCode.indexOf(lineOfInterest) + lineOfInterest.length const extruded = hasSketchPipeBeenExtruded( { - range: [characterIndex, characterIndex], - type: 'default', + codeRef: codeRefFromRange([characterIndex, characterIndex], ast), }, ast ) @@ -586,8 +599,7 @@ sketch003 = startSketchOn(extrude001, 'END') exampleCode.indexOf(lineOfInterest) + lineOfInterest.length const extruded = hasSketchPipeBeenExtruded( { - range: [characterIndex, characterIndex], - type: 'default', + codeRef: codeRefFromRange([characterIndex, characterIndex], ast), }, ast ) diff --git a/src/lang/queryAst.ts b/src/lang/queryAst.ts index 887fef761..85360c4f1 100644 --- a/src/lang/queryAst.ts +++ b/src/lang/queryAst.ts @@ -31,6 +31,7 @@ import { import { err, Reason } from 'lib/trap' import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement' import { Node } from 'wasm-lib/kcl/bindings/Node' +import { ArtifactGraph, codeRefFromRange } from './std/artifactGraph' /** * Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type. @@ -130,7 +131,7 @@ function moreNodePathFromSourceRange( | VariableDeclaration | ReturnStatement >, - sourceRange: Selection['range'], + sourceRange: SourceRange, previousPath: PathToNode = [['body', '']] ): PathToNode { const [start, end] = sourceRange @@ -381,7 +382,7 @@ function moreNodePathFromSourceRange( export function getNodePathFromSourceRange( node: Program, - sourceRange: Selection['range'], + sourceRange: SourceRange, previousPath: PathToNode = [['body', '']] ): PathToNode { const [start, end] = sourceRange || [] @@ -560,7 +561,7 @@ export function findAllPreviousVariablesPath( export function findAllPreviousVariables( ast: Program, programMemory: ProgramMemory, - sourceRange: Selection['range'], + sourceRange: SourceRange, type: 'number' | 'string' = 'number' ): { variables: PrevVariable[] @@ -705,19 +706,26 @@ export function isValueZero(val?: Expr): boolean { export function isLinesParallelAndConstrained( ast: Program, + artifactGraph: ArtifactGraph, programMemory: ProgramMemory, primaryLine: Selection, secondaryLine: Selection ): | { isParallelAndConstrained: boolean - sourceRange: SourceRange + selection: Selection | null } | Error { try { const EPSILON = 0.005 - const primaryPath = getNodePathFromSourceRange(ast, primaryLine.range) - const secondaryPath = getNodePathFromSourceRange(ast, secondaryLine.range) + const primaryPath = getNodePathFromSourceRange( + ast, + primaryLine?.codeRef?.range + ) + const secondaryPath = getNodePathFromSourceRange( + ast, + secondaryLine?.codeRef?.range + ) const _secondaryNode = getNodeFromPath( ast, secondaryPath, @@ -733,12 +741,15 @@ export function isLinesParallelAndConstrained( if (err(sg)) return sg const _primarySegment = getSketchSegmentFromSourceRange( sg, - primaryLine.range + primaryLine?.codeRef?.range ) if (err(_primarySegment)) return _primarySegment const primarySegment = _primarySegment.segment - const _segment = getSketchSegmentFromSourceRange(sg, secondaryLine.range) + const _segment = getSketchSegmentFromSourceRange( + sg, + secondaryLine?.codeRef?.range + ) if (err(_segment)) return _segment const { segment: secondarySegment, index: secondaryIndex } = _segment const primaryAngle = getAngle(primarySegment.from, primarySegment.to) @@ -751,7 +762,7 @@ export function isLinesParallelAndConstrained( Math.abs(primaryAngle - secondaryAngle) < EPSILON || Math.abs(primaryAngle - secondaryAngleAlt) < EPSILON - // is secordary line fully constrain, or has constrain type of 'angle' + // is secondary line fully constrain, or has constrain type of 'angle' const secondaryFirstArg = getFirstArg(secondaryNode) if (err(secondaryFirstArg)) return secondaryFirstArg @@ -761,14 +772,14 @@ export function isLinesParallelAndConstrained( ) const constraintLevelMeta = getConstraintLevelFromSourceRange( - secondaryLine.range, + secondaryLine?.codeRef.range, ast ) if (err(constraintLevelMeta)) { console.error(constraintLevelMeta) return { isParallelAndConstrained: false, - sourceRange: [0, 0], + selection: null, } } const constraintLevel = constraintLevelMeta.level @@ -785,12 +796,15 @@ export function isLinesParallelAndConstrained( return { isParallelAndConstrained, - sourceRange: prevSourceRange, + selection: { + codeRef: codeRefFromRange(prevSourceRange, ast), + artifact: artifactGraph.get(prevSegment.__geoMeta.id), + }, } } catch (e) { return { isParallelAndConstrained: false, - sourceRange: [0, 0], + selection: null, } } } @@ -804,10 +818,9 @@ export function doesPipeHaveCallExp({ ast: Program selection: Selection }): boolean { - const pathToNode = getNodePathFromSourceRange(ast, selection.range) const pipeExpressionMeta = getNodeFromPath( ast, - pathToNode, + selection?.codeRef?.pathToNode, 'PipeExpression' ) if (err(pipeExpressionMeta)) { @@ -832,10 +845,9 @@ export function hasExtrudeSketch({ selection: Selection programMemory: ProgramMemory }): boolean { - const pathToNode = getNodePathFromSourceRange(ast, selection.range) const varDecMeta = getNodeFromPath( ast, - pathToNode, + selection?.codeRef?.pathToNode, 'VariableDeclaration' ) if (err(varDecMeta)) { @@ -856,9 +868,9 @@ export function isSingleCursorInPipe( selectionRanges: Selections, ast: Program ) { - if (selectionRanges.codeBasedSelections.length !== 1) return false - const selection = selectionRanges.codeBasedSelections[0] - const pathToNode = getNodePathFromSourceRange(ast, selection.range) + if (selectionRanges.graphSelections.length !== 1) return false + const selection = selectionRanges.graphSelections[0] + const pathToNode = getNodePathFromSourceRange(ast, selection?.codeRef?.range) const nodeTypes = pathToNode.map(([, type]) => type) if (nodeTypes.includes('FunctionExpression')) return false if (!nodeTypes.includes('VariableDeclaration')) return false @@ -928,10 +940,9 @@ export function findUsesOfTagInPipe( } export function hasSketchPipeBeenExtruded(selection: Selection, ast: Program) { - const path = getNodePathFromSourceRange(ast, selection.range) const _node = getNodeFromPath>( ast, - path, + selection.codeRef.pathToNode, 'PipeExpression' ) if (err(_node)) return false @@ -939,7 +950,7 @@ export function hasSketchPipeBeenExtruded(selection: Selection, ast: Program) { if (pipeExpression.type !== 'PipeExpression') return false const _varDec = getNodeFromPath( ast, - path, + selection.codeRef.pathToNode, 'VariableDeclarator' ) if (err(_varDec)) return false diff --git a/src/lang/std/__snapshots__/artifactGraph.test.ts.snap b/src/lang/std/__snapshots__/artifactGraph.test.ts.snap index af03d4c30..4a5beb7f2 100644 --- a/src/lang/std/__snapshots__/artifactGraph.test.ts.snap +++ b/src/lang/std/__snapshots__/artifactGraph.test.ts.snap @@ -16,6 +16,7 @@ Map { 0, ], }, + "id": "UUID", "pathIds": [ "UUID", ], @@ -35,6 +36,7 @@ Map { 0, ], }, + "id": "UUID", "planeId": "UUID", "segIds": [ "UUID", @@ -65,6 +67,7 @@ Map { "UUID", "UUID", ], + "id": "UUID", "pathId": "UUID", "surfaceId": "UUID", "type": "segment", @@ -88,6 +91,7 @@ Map { "UUID", "UUID", ], + "id": "UUID", "pathId": "UUID", "surfaceId": "UUID", "type": "segment", @@ -110,6 +114,7 @@ Map { "UUID", "UUID", ], + "id": "UUID", "pathId": "UUID", "surfaceId": "UUID", "type": "segment", @@ -132,6 +137,7 @@ Map { "UUID", "UUID", ], + "id": "UUID", "pathId": "UUID", "surfaceId": "UUID", "type": "segment", @@ -151,10 +157,12 @@ Map { ], }, "edgeIds": [], + "id": "UUID", "pathId": "UUID", "type": "segment", }, "UUID-7" => { + "id": "UUID", "pathId": "UUID", "type": "solid2D", }, @@ -182,6 +190,7 @@ Map { "UUID", "UUID", ], + "id": "UUID", "pathId": "UUID", "subType": "extrusion", "surfaceIds": [ @@ -196,6 +205,7 @@ Map { }, "UUID-9" => { "edgeCutEdgeIds": [], + "id": "UUID", "pathIds": [], "segId": "UUID", "sweepId": "UUID", @@ -203,6 +213,7 @@ Map { }, "UUID-10" => { "edgeCutEdgeIds": [], + "id": "UUID", "pathIds": [ "UUID", ], @@ -212,6 +223,7 @@ Map { }, "UUID-11" => { "edgeCutEdgeIds": [], + "id": "UUID", "pathIds": [], "segId": "UUID", "sweepId": "UUID", @@ -219,6 +231,7 @@ Map { }, "UUID-12" => { "edgeCutEdgeIds": [], + "id": "UUID", "pathIds": [], "segId": "UUID", "sweepId": "UUID", @@ -226,6 +239,7 @@ Map { }, "UUID-13" => { "edgeCutEdgeIds": [], + "id": "UUID", "pathIds": [], "subType": "start", "sweepId": "UUID", @@ -233,54 +247,63 @@ Map { }, "UUID-14" => { "edgeCutEdgeIds": [], + "id": "UUID", "pathIds": [], "subType": "end", "sweepId": "UUID", "type": "cap", }, "UUID-15" => { + "id": "UUID", "segId": "UUID", "subType": "opposite", "sweepId": "UUID", "type": "sweepEdge", }, "UUID-16" => { + "id": "UUID", "segId": "UUID", "subType": "adjacent", "sweepId": "UUID", "type": "sweepEdge", }, "UUID-17" => { + "id": "UUID", "segId": "UUID", "subType": "opposite", "sweepId": "UUID", "type": "sweepEdge", }, "UUID-18" => { + "id": "UUID", "segId": "UUID", "subType": "adjacent", "sweepId": "UUID", "type": "sweepEdge", }, "UUID-19" => { + "id": "UUID", "segId": "UUID", "subType": "opposite", "sweepId": "UUID", "type": "sweepEdge", }, "UUID-20" => { + "id": "UUID", "segId": "UUID", "subType": "adjacent", "sweepId": "UUID", "type": "sweepEdge", }, "UUID-21" => { + "id": "UUID", "segId": "UUID", "subType": "opposite", "sweepId": "UUID", "type": "sweepEdge", }, "UUID-22" => { + "id": "UUID", "segId": "UUID", "subType": "adjacent", "sweepId": "UUID", @@ -302,6 +325,7 @@ Map { }, "consumedEdgeId": "UUID", "edgeIds": [], + "id": "UUID", "subType": "fillet", "type": "edgeCut", }, @@ -319,6 +343,7 @@ Map { 0, ], }, + "id": "UUID", "planeId": "UUID", "segIds": [ "UUID", @@ -348,6 +373,7 @@ Map { "UUID", "UUID", ], + "id": "UUID", "pathId": "UUID", "surfaceId": "UUID", "type": "segment", @@ -370,6 +396,7 @@ Map { "UUID", "UUID", ], + "id": "UUID", "pathId": "UUID", "surfaceId": "UUID", "type": "segment", @@ -392,6 +419,7 @@ Map { "UUID", "UUID", ], + "id": "UUID", "pathId": "UUID", "surfaceId": "UUID", "type": "segment", @@ -411,10 +439,12 @@ Map { ], }, "edgeIds": [], + "id": "UUID", "pathId": "UUID", "type": "segment", }, "UUID-29" => { + "id": "UUID", "pathId": "UUID", "type": "solid2D", }, @@ -440,6 +470,7 @@ Map { "UUID", "UUID", ], + "id": "UUID", "pathId": "UUID", "subType": "extrusion", "surfaceIds": [ @@ -453,6 +484,7 @@ Map { }, "UUID-31" => { "edgeCutEdgeIds": [], + "id": "UUID", "pathIds": [], "segId": "UUID", "sweepId": "UUID", @@ -460,6 +492,7 @@ Map { }, "UUID-32" => { "edgeCutEdgeIds": [], + "id": "UUID", "pathIds": [], "segId": "UUID", "sweepId": "UUID", @@ -467,6 +500,7 @@ Map { }, "UUID-33" => { "edgeCutEdgeIds": [], + "id": "UUID", "pathIds": [], "segId": "UUID", "sweepId": "UUID", @@ -474,6 +508,7 @@ Map { }, "UUID-34" => { "edgeCutEdgeIds": [], + "id": "UUID", "pathIds": [], "subType": "start", "sweepId": "UUID", @@ -481,42 +516,49 @@ Map { }, "UUID-35" => { "edgeCutEdgeIds": [], + "id": "UUID", "pathIds": [], "subType": "end", "sweepId": "UUID", "type": "cap", }, "UUID-36" => { + "id": "UUID", "segId": "UUID", "subType": "opposite", "sweepId": "UUID", "type": "sweepEdge", }, "UUID-37" => { + "id": "UUID", "segId": "UUID", "subType": "adjacent", "sweepId": "UUID", "type": "sweepEdge", }, "UUID-38" => { + "id": "UUID", "segId": "UUID", "subType": "opposite", "sweepId": "UUID", "type": "sweepEdge", }, "UUID-39" => { + "id": "UUID", "segId": "UUID", "subType": "adjacent", "sweepId": "UUID", "type": "sweepEdge", }, "UUID-40" => { + "id": "UUID", "segId": "UUID", "subType": "opposite", "sweepId": "UUID", "type": "sweepEdge", }, "UUID-41" => { + "id": "UUID", "segId": "UUID", "subType": "adjacent", "sweepId": "UUID", diff --git a/src/lang/std/artifactGraph.test.ts b/src/lang/std/artifactGraph.test.ts index 4234bc058..0ca488745 100644 --- a/src/lang/std/artifactGraph.test.ts +++ b/src/lang/std/artifactGraph.test.ts @@ -438,7 +438,8 @@ async function GraphTheGraph( if ( propName === 'type' || propName === 'codeRef' || - propName === 'subType' + propName === 'subType' || + propName === 'id' ) return if (Array.isArray(value)) @@ -662,6 +663,7 @@ describe('testing getArtifactsToUpdate', () => { { type: 'path', segIds: [], + id: expect.any(String), planeId: 'UUID-1', sweepId: '', codeRef: { @@ -675,6 +677,7 @@ describe('testing getArtifactsToUpdate', () => { type: 'sweep', subType: 'extrusion', pathId: expect.any(String), + id: expect.any(String), surfaceIds: [], edgeIds: [], codeRef: { @@ -684,6 +687,7 @@ describe('testing getArtifactsToUpdate', () => { }, { type: 'path', + id: expect.any(String), segIds: expect.any(Array), planeId: expect.any(String), sweepId: expect.any(String), @@ -697,6 +701,7 @@ describe('testing getArtifactsToUpdate', () => { expect(getUpdateObjects('extend_path')).toEqual([ { type: 'segment', + id: expect.any(String), pathId: expect.any(String), surfaceId: '', edgeIds: [], @@ -707,6 +712,7 @@ describe('testing getArtifactsToUpdate', () => { }, { type: 'path', + id: expect.any(String), segIds: expect.any(Array), planeId: expect.any(String), sweepId: expect.any(String), @@ -721,6 +727,7 @@ describe('testing getArtifactsToUpdate', () => { { type: 'edgeCut', subType: 'fillet', + id: expect.any(String), consumedEdgeId: expect.any(String), edgeIds: [], surfaceId: '', @@ -731,6 +738,7 @@ describe('testing getArtifactsToUpdate', () => { }, { type: 'segment', + id: expect.any(String), pathId: expect.any(String), surfaceId: expect.any(String), edgeIds: expect.any(Array), @@ -744,6 +752,7 @@ describe('testing getArtifactsToUpdate', () => { expect(getUpdateObjects('solid3d_get_extrusion_face_info')).toEqual([ { type: 'wall', + id: expect.any(String), segId: expect.any(String), edgeCutEdgeIds: [], sweepId: expect.any(String), @@ -751,6 +760,7 @@ describe('testing getArtifactsToUpdate', () => { }, { type: 'segment', + id: expect.any(String), pathId: expect.any(String), surfaceId: expect.any(String), edgeIds: expect.any(Array), @@ -762,6 +772,7 @@ describe('testing getArtifactsToUpdate', () => { { type: 'sweep', subType: 'extrusion', + id: expect.any(String), pathId: expect.any(String), surfaceIds: expect.any(Array), edgeIds: expect.any(Array), @@ -772,6 +783,7 @@ describe('testing getArtifactsToUpdate', () => { }, { type: 'wall', + id: expect.any(String), segId: expect.any(String), edgeCutEdgeIds: [], sweepId: expect.any(String), @@ -779,6 +791,7 @@ describe('testing getArtifactsToUpdate', () => { }, { type: 'segment', + id: expect.any(String), pathId: expect.any(String), surfaceId: expect.any(String), edgeIds: expect.any(Array), @@ -790,6 +803,7 @@ describe('testing getArtifactsToUpdate', () => { { type: 'sweep', subType: 'extrusion', + id: expect.any(String), pathId: expect.any(String), surfaceIds: expect.any(Array), edgeIds: expect.any(Array), @@ -800,6 +814,7 @@ describe('testing getArtifactsToUpdate', () => { }, { type: 'wall', + id: expect.any(String), segId: expect.any(String), edgeCutEdgeIds: [], sweepId: expect.any(String), @@ -807,6 +822,7 @@ describe('testing getArtifactsToUpdate', () => { }, { type: 'segment', + id: expect.any(String), pathId: expect.any(String), surfaceId: expect.any(String), edgeIds: expect.any(Array), @@ -819,6 +835,7 @@ describe('testing getArtifactsToUpdate', () => { { type: 'sweep', subType: 'extrusion', + id: expect.any(String), pathId: expect.any(String), surfaceIds: expect.any(Array), edgeIds: expect.any(Array), @@ -829,6 +846,7 @@ describe('testing getArtifactsToUpdate', () => { }, { type: 'wall', + id: expect.any(String), segId: expect.any(String), edgeCutEdgeIds: [], sweepId: expect.any(String), @@ -836,6 +854,7 @@ describe('testing getArtifactsToUpdate', () => { }, { type: 'segment', + id: expect.any(String), pathId: expect.any(String), surfaceId: expect.any(String), edgeIds: expect.any(Array), @@ -847,6 +866,7 @@ describe('testing getArtifactsToUpdate', () => { { type: 'sweep', subType: 'extrusion', + id: expect.any(String), pathId: expect.any(String), surfaceIds: expect.any(Array), edgeIds: expect.any(Array), @@ -858,6 +878,7 @@ describe('testing getArtifactsToUpdate', () => { { type: 'cap', subType: 'start', + id: expect.any(String), edgeCutEdgeIds: [], sweepId: expect.any(String), pathIds: [], @@ -865,6 +886,7 @@ describe('testing getArtifactsToUpdate', () => { { type: 'sweep', subType: 'extrusion', + id: expect.any(String), pathId: expect.any(String), surfaceIds: expect.any(Array), edgeIds: expect.any(Array), @@ -876,6 +898,7 @@ describe('testing getArtifactsToUpdate', () => { { type: 'cap', subType: 'end', + id: expect.any(String), edgeCutEdgeIds: [], sweepId: expect.any(String), pathIds: [], @@ -883,6 +906,7 @@ describe('testing getArtifactsToUpdate', () => { { type: 'sweep', subType: 'extrusion', + id: expect.any(String), pathId: expect.any(String), surfaceIds: expect.any(Array), edgeIds: expect.any(Array), diff --git a/src/lang/std/artifactGraph.ts b/src/lang/std/artifactGraph.ts index 8fddddc01..d5a9e5098 100644 --- a/src/lang/std/artifactGraph.ts +++ b/src/lang/std/artifactGraph.ts @@ -5,36 +5,40 @@ import { err } from 'lib/trap' export type ArtifactId = string -interface CommonCommandProperties { +interface BaseArtifact { + id: ArtifactId +} + +export interface CodeRef { range: SourceRange pathToNode: PathToNode } -export interface PlaneArtifact { +export interface PlaneArtifact extends BaseArtifact { type: 'plane' pathIds: Array - codeRef: CommonCommandProperties + codeRef: CodeRef } -export interface PlaneArtifactRich { +export interface PlaneArtifactRich extends BaseArtifact { type: 'plane' paths: Array - codeRef: CommonCommandProperties + codeRef: CodeRef } -export interface PathArtifact { +export interface PathArtifact extends BaseArtifact { type: 'path' planeId: ArtifactId segIds: Array sweepId: ArtifactId solid2dId?: ArtifactId - codeRef: CommonCommandProperties + codeRef: CodeRef } -interface solid2D { +interface solid2D extends BaseArtifact { type: 'solid2D' pathId: ArtifactId } -export interface PathArtifactRich { +export interface PathArtifactRich extends BaseArtifact { type: 'path' /** A path must always lie on a plane */ plane: PlaneArtifact | WallArtifact @@ -42,52 +46,52 @@ export interface PathArtifactRich { segments: Array /** A path may not result in a sweep artifact */ sweep?: SweepArtifact - codeRef: CommonCommandProperties + codeRef: CodeRef } -export interface SegmentArtifact { +export interface SegmentArtifact extends BaseArtifact { type: 'segment' pathId: ArtifactId surfaceId: ArtifactId edgeIds: Array edgeCutId?: ArtifactId - codeRef: CommonCommandProperties + codeRef: CodeRef } -interface SegmentArtifactRich { +interface SegmentArtifactRich extends BaseArtifact { type: 'segment' path: PathArtifact surf: WallArtifact edges: Array edgeCut?: EdgeCut - codeRef: CommonCommandProperties + codeRef: CodeRef } /** A Sweep is a more generic term for extrude, revolve, loft and sweep*/ -interface SweepArtifact { +interface SweepArtifact extends BaseArtifact { type: 'sweep' subType: 'extrusion' | 'revolve' pathId: string surfaceIds: Array edgeIds: Array - codeRef: CommonCommandProperties + codeRef: CodeRef } -interface SweepArtifactRich { +interface SweepArtifactRich extends BaseArtifact { type: 'sweep' subType: 'extrusion' | 'revolve' path: PathArtifact surfaces: Array edges: Array - codeRef: CommonCommandProperties + codeRef: CodeRef } -interface WallArtifact { +interface WallArtifact extends BaseArtifact { type: 'wall' segId: ArtifactId edgeCutEdgeIds: Array sweepId: ArtifactId pathIds: Array } -interface CapArtifact { +interface CapArtifact extends BaseArtifact { type: 'cap' subType: 'start' | 'end' edgeCutEdgeIds: Array @@ -95,7 +99,7 @@ interface CapArtifact { pathIds: Array } -interface SweepEdge { +interface SweepEdge extends BaseArtifact { type: 'sweepEdge' segId: ArtifactId sweepId: ArtifactId @@ -103,16 +107,16 @@ interface SweepEdge { } /** A edgeCut is a more generic term for both fillet or chamfer */ -interface EdgeCut { +interface EdgeCut extends BaseArtifact { type: 'edgeCut' subType: 'fillet' | 'chamfer' consumedEdgeId: ArtifactId edgeIds: Array surfaceId: ArtifactId - codeRef: CommonCommandProperties + codeRef: CodeRef } -interface EdgeCutEdge { +interface EdgeCutEdge extends BaseArtifact { type: 'edgeCutEdge' edgeCutId: ArtifactId surfaceId: ArtifactId @@ -257,6 +261,7 @@ export function getArtifactsToUpdate({ id, artifact: { type: 'plane', + id, pathIds: [], codeRef: { range, pathToNode }, }, @@ -274,6 +279,7 @@ export function getArtifactsToUpdate({ id: currentPlaneId, artifact: { type: 'wall', + id: currentPlaneId, segId: existingPlane.segId, edgeCutEdgeIds: existingPlane.edgeCutEdgeIds, sweepId: existingPlane.sweepId, @@ -283,7 +289,10 @@ export function getArtifactsToUpdate({ ] } else { return [ - { id: currentPlaneId, artifact: { type: 'plane', pathIds, codeRef } }, + { + id: currentPlaneId, + artifact: { type: 'plane', id: currentPlaneId, pathIds, codeRef }, + }, ] } } else if (cmd.type === 'start_path') { @@ -291,6 +300,7 @@ export function getArtifactsToUpdate({ id, artifact: { type: 'path', + id, segIds: [], planeId: currentPlaneId, sweepId: '', @@ -303,7 +313,7 @@ export function getArtifactsToUpdate({ if (plane?.type === 'plane') { returnArr.push({ id: currentPlaneId, - artifact: { type: 'plane', pathIds: [id], codeRef }, + artifact: { type: 'plane', id: currentPlaneId, pathIds: [id], codeRef }, }) } if (plane?.type === 'wall') { @@ -311,6 +321,7 @@ export function getArtifactsToUpdate({ id: currentPlaneId, artifact: { type: 'wall', + id: currentPlaneId, segId: plane.segId, edgeCutEdgeIds: plane.edgeCutEdgeIds, sweepId: plane.sweepId, @@ -325,6 +336,7 @@ export function getArtifactsToUpdate({ id, artifact: { type: 'segment', + id, pathId, surfaceId: '', edgeIds: [], @@ -343,7 +355,11 @@ export function getArtifactsToUpdate({ ) { returnArr.push({ id: response.data.modeling_response.data.face_id, - artifact: { type: 'solid2D', pathId }, + artifact: { + type: 'solid2D', + id: response.data.modeling_response.data.face_id, + pathId, + }, }) const path = getArtifact(pathId) if (path?.type === 'path') @@ -363,6 +379,7 @@ export function getArtifactsToUpdate({ artifact: { type: 'sweep', subType: subType, + id, pathId: cmd.target, surfaceIds: [], edgeIds: [], @@ -394,6 +411,7 @@ export function getArtifactsToUpdate({ id: face_id, artifact: { type: 'wall', + id: face_id, segId: curve_id, edgeCutEdgeIds: [], sweepId: path.sweepId, @@ -426,6 +444,7 @@ export function getArtifactsToUpdate({ id: face_id, artifact: { type: 'cap', + id: face_id, subType: cap === 'bottom' ? 'start' : 'end', edgeCutEdgeIds: [], sweepId: path.sweepId, @@ -472,6 +491,7 @@ export function getArtifactsToUpdate({ id: response.data.modeling_response.data.edge, artifact: { type: 'sweepEdge', + id: response.data.modeling_response.data.edge, subType: cmd.type === 'solid3d_get_next_adjacent_edge' ? 'adjacent' @@ -500,6 +520,7 @@ export function getArtifactsToUpdate({ id, artifact: { type: 'edgeCut', + id, subType: cmd.cut_type, consumedEdgeId: cmd.edge_id, edgeIds: [], @@ -590,6 +611,7 @@ export function expandPlane( ) return { type: 'plane', + id: plane.id, paths: Array.from(paths.values()), codeRef: plane.codeRef, } @@ -620,6 +642,7 @@ export function expandPath( if (err(plane)) return plane return { type: 'path', + id: path.id, segments: Array.from(segs.values()), sweep, plane, @@ -647,6 +670,7 @@ export function expandSweep( return { type: 'sweep', subType: sweep.subType, + id: sweep.id, surfaces: Array.from(surfs.values()), edges: Array.from(edges.values()), path, @@ -682,6 +706,7 @@ export function expandSegment( return { type: 'segment', + id: segment.id, path, surf, edges: Array.from(edges.values()), @@ -693,7 +718,7 @@ export function expandSegment( export function getCapCodeRef( cap: CapArtifact, artifactGraph: ArtifactGraph -): CommonCommandProperties | Error { +): CodeRef | Error { const sweep = getArtifactOfTypes( { key: cap.sweepId, types: ['sweep'] }, artifactGraph @@ -710,7 +735,7 @@ export function getCapCodeRef( export function getSolid2dCodeRef( solid2D: solid2D, artifactGraph: ArtifactGraph -): CommonCommandProperties | Error { +): CodeRef | Error { const path = getArtifactOfTypes( { key: solid2D.pathId, types: ['path'] }, artifactGraph @@ -722,7 +747,7 @@ export function getSolid2dCodeRef( export function getWallCodeRef( wall: WallArtifact, artifactGraph: ArtifactGraph -): CommonCommandProperties | Error { +): CodeRef | Error { const seg = getArtifactOfTypes( { key: wall.segId, types: ['segment'] }, artifactGraph @@ -734,7 +759,7 @@ export function getWallCodeRef( export function getSweepEdgeCodeRef( edge: SweepEdge, artifactGraph: ArtifactGraph -): CommonCommandProperties | Error { +): CodeRef | Error { const seg = getArtifactOfTypes( { key: edge.segId, types: ['segment'] }, artifactGraph @@ -742,10 +767,10 @@ export function getSweepEdgeCodeRef( if (err(seg)) return seg return seg.codeRef } -export function getEdgeCuteConsumedCodeRef( +export function getEdgeCutConsumedCodeRef( edge: EdgeCut, artifactGraph: ArtifactGraph -): CommonCommandProperties | Error { +): CodeRef | Error { const seg = getArtifactOfTypes( { key: edge.consumedEdgeId, types: ['segment', 'sweepEdge'] }, artifactGraph @@ -803,3 +828,46 @@ export function getSweepFromSuspectedPath( artifactGraph ) } + +export function getCodeRefsByArtifactId( + id: string, + artifactGraph: ArtifactGraph +): Array | null { + const artifact = artifactGraph.get(id) + if (artifact?.type === 'solid2D') { + const codeRef = getSolid2dCodeRef(artifact, artifactGraph) + if (err(codeRef)) return null + return [codeRef] + } else if (artifact?.type === 'cap') { + const codeRef = getCapCodeRef(artifact, artifactGraph) + if (err(codeRef)) return null + return [codeRef] + } else if (artifact?.type === 'wall') { + const extrusion = getSweepFromSuspectedSweepSurface(id, artifactGraph) + const codeRef = getWallCodeRef(artifact, artifactGraph) + if (err(codeRef)) return null + return err(extrusion) ? [codeRef] : [codeRef, extrusion.codeRef] + } else if (artifact?.type === 'sweepEdge') { + const codeRef = getSweepEdgeCodeRef(artifact, artifactGraph) + if (err(codeRef)) return null + return [codeRef] + } else if (artifact?.type === 'segment') { + return [artifact.codeRef] + } else if (artifact?.type === 'edgeCut') { + const codeRef = artifact.codeRef + const consumedCodeRef = getEdgeCutConsumedCodeRef(artifact, artifactGraph) + if (err(consumedCodeRef)) return [codeRef] + return [codeRef, consumedCodeRef] + } else if (artifact && 'codeRef' in artifact) { + return [artifact.codeRef] + } else { + return null + } +} + +export function codeRefFromRange(range: SourceRange, ast: Program): CodeRef { + return { + range, + pathToNode: getNodePathFromSourceRange(ast, range), + } +} diff --git a/src/lang/std/engineConnection.ts b/src/lang/std/engineConnection.ts index 1124bd165..5f17f7027 100644 --- a/src/lang/std/engineConnection.ts +++ b/src/lang/std/engineConnection.ts @@ -1,4 +1,4 @@ -import { Program, SourceRange } from 'lang/wasm' +import { SourceRange } from 'lang/wasm' import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env' import { Models } from '@kittycad/lib' import { exportSave } from 'lib/exportSave' @@ -1398,11 +1398,6 @@ export class EngineCommandManager extends EventTarget { this._camControlsCameraChange = cb } - private getAst: () => Program = () => - ({ start: 0, end: 0, body: [], nonCodeMeta: {} } as any) - set getAstCb(cb: () => Program) { - this.getAst = cb - } private makeDefaultPlanes: () => Promise | null = () => null private modifyGrid: (hidden: boolean) => Promise | null = () => null @@ -2125,18 +2120,30 @@ export class EngineCommandManager extends EventTarget { * When an execution takes place we want to wait until we've got replies for all of the commands * When this is done when we build the artifact map synchronously. */ - async waitForAllCommands() { + async waitForAllCommands(useFakeExecutor = false) { await Promise.all(Object.values(this.pendingCommands).map((a) => a.promise)) - this.artifactGraph = createArtifactGraph({ - orderedCommands: this.orderedCommands, - responseMap: this.responseMap, - ast: this.getAst(), + setTimeout(() => { + // the ast is wrong without this one tick timeout. + // an example is `Solids should be select and deletable` e2e test will fail + // because the out of date ast messes with selections + // TODO: race condition + if (!this?.kclManager) return + this.artifactGraph = createArtifactGraph({ + orderedCommands: this.orderedCommands, + responseMap: this.responseMap, + ast: this.kclManager.ast, + }) + if (useFakeExecutor) { + // mock executions don't produce an artifactGraph, so this will always be empty + // skipping the below logic to wait for the next real execution + return + } + if (this.artifactGraph.size) { + this.deferredArtifactEmptied(null) + } else { + this.deferredArtifactPopulated(null) + } }) - if (this.artifactGraph.size) { - this.deferredArtifactEmptied(null) - } else { - this.deferredArtifactPopulated(null) - } } /** diff --git a/src/lang/std/sketchConstraints.test.ts b/src/lang/std/sketchConstraints.test.ts index 08869ae91..c5ca49fcd 100644 --- a/src/lang/std/sketchConstraints.test.ts +++ b/src/lang/std/sketchConstraints.test.ts @@ -1,13 +1,20 @@ -import { parse, Sketch, recast, initPromise, sketchFromKclValue } from '../wasm' +import { + parse, + Sketch, + recast, + initPromise, + sketchFromKclValue, + SourceRange, +} from '../wasm' import { ConstraintType, getTransformInfos, transformAstSketchLines, } from './sketchcombos' import { getSketchSegmentFromSourceRange } from './sketchConstraints' -import { Selection } from 'lib/selections' import { enginelessExecutor } from '../../lib/testHelpers' import { err } from 'lib/trap' +import { codeRefFromRange } from './artifactGraph' beforeAll(async () => { await initPromise @@ -27,16 +34,17 @@ async function testingSwapSketchFnCall({ originalRange: [number, number] }> { const startIndex = inputCode.indexOf(callToSwap) - const range: Selection = { - type: 'default', - range: [startIndex, startIndex + callToSwap.length], - } + const range: SourceRange = [startIndex, startIndex + callToSwap.length] const ast = parse(inputCode) if (err(ast)) return Promise.reject(ast) const execState = await enginelessExecutor(ast) const selections = { - codeBasedSelections: [range], + graphSelections: [ + { + codeRef: codeRefFromRange(range, ast), + }, + ], otherSelections: [], } const transformInfos = getTransformInfos(selections, ast, constraintType) @@ -57,7 +65,7 @@ async function testingSwapSketchFnCall({ return { newCode, - originalRange: range.range, + originalRange: range, } } diff --git a/src/lang/std/sketchcombos.test.ts b/src/lang/std/sketchcombos.test.ts index 48fc80edb..07c4bd9bc 100644 --- a/src/lang/std/sketchcombos.test.ts +++ b/src/lang/std/sketchcombos.test.ts @@ -1,4 +1,4 @@ -import { parse, Expr, recast, initPromise } from '../wasm' +import { parse, Expr, recast, initPromise, Program } from '../wasm' import { getConstraintType, getTransformInfos, @@ -9,9 +9,10 @@ import { getConstraintLevelFromSourceRange, } from './sketchcombos' import { ToolTip } from 'lang/langHelpers' -import { Selection, Selections } from 'lib/selections' +import { Selections, Selection } from 'lib/selections' import { err } from 'lib/trap' import { enginelessExecutor } from '../../lib/testHelpers' +import { codeRefFromRange } from './artifactGraph' beforeAll(async () => { await initPromise @@ -87,10 +88,10 @@ function getConstraintTypeFromSourceHelper2( } function makeSelections( - codeBaseSelections: Selections['codeBasedSelections'] + graphSelections: Selections['graphSelections'] ): Selections { return { - codeBasedSelections: codeBaseSelections, + graphSelections: graphSelections, otherSelections: [], } } @@ -112,7 +113,11 @@ describe('testing transformAstForSketchLines for equal length constraint', () => |> close(%) ` - const selectLine = (script: string, lineNumber: number): Selection => { + const selectLine = ( + script: string, + lineNumber: number, + ast: Program + ): Selection => { const lines = script.split('\n') const codeBeforeLine = lines.slice(0, lineNumber).join('\n').length const line = lines.find((_, i) => i === lineNumber) @@ -124,14 +129,13 @@ describe('testing transformAstForSketchLines for equal length constraint', () => const start = codeBeforeLine + line.indexOf('|> ' + 5) const range: [number, number] = [start, start] return { - type: 'default', - range, + codeRef: codeRefFromRange(range, ast), } } async function applyTransformation( inputCode: string, - selectionRanges: Selections['codeBasedSelections'] + selectionRanges: Selections['graphSelections'] ) { const ast = parse(inputCode) if (err(ast)) return Promise.reject(ast) @@ -157,9 +161,11 @@ describe('testing transformAstForSketchLines for equal length constraint', () => } it(`Should reorder when user selects first-to-last`, async () => { - const selectionRanges: Selections['codeBasedSelections'] = [ - selectLine(inputScript, 3), - selectLine(inputScript, 4), + const ast = parse(inputScript) + if (err(ast)) return Promise.reject(ast) + const selectionRanges: Selections['graphSelections'] = [ + selectLine(inputScript, 3, ast), + selectLine(inputScript, 4, ast), ] const newCode = await applyTransformation(inputScript, selectionRanges) @@ -167,9 +173,11 @@ describe('testing transformAstForSketchLines for equal length constraint', () => }) it(`Should reorder when user selects last-to-first`, async () => { - const selectionRanges: Selections['codeBasedSelections'] = [ - selectLine(inputScript, 4), - selectLine(inputScript, 3), + const ast = parse(inputScript) + if (err(ast)) return Promise.reject(ast) + const selectionRanges: Selections['graphSelections'] = [ + selectLine(inputScript, 4, ast), + selectLine(inputScript, 3, ast), ] const newCode = await applyTransformation(inputScript, selectionRanges) @@ -288,15 +296,14 @@ part001 = startSketchOn('XY') const ast = parse(inputScript) if (err(ast)) return Promise.reject(ast) - const selectionRanges: Selections['codeBasedSelections'] = inputScript + const selectionRanges: Selections['graphSelections'] = inputScript .split('\n') .filter((ln) => ln.includes('//')) .map((ln) => { const comment = ln.split('//')[1] const start = inputScript.indexOf('//' + comment) - 7 return { - type: 'default', - range: [start, start], + codeRef: codeRefFromRange([start, start], ast), } }) @@ -379,15 +386,14 @@ part001 = startSketchOn('XY') const ast = parse(inputScript) if (err(ast)) return Promise.reject(ast) - const selectionRanges: Selections['codeBasedSelections'] = inputScript + const selectionRanges: Selections['graphSelections'] = inputScript .split('\n') .filter((ln) => ln.includes('// select for horizontal constraint')) .map((ln) => { const comment = ln.split('//')[1] const start = inputScript.indexOf('//' + comment) - 7 return { - type: 'default', - range: [start, start], + codeRef: codeRefFromRange([start, start], ast), } }) @@ -441,15 +447,14 @@ part001 = startSketchOn('XY') const ast = parse(inputScript) if (err(ast)) return Promise.reject(ast) - const selectionRanges: Selections['codeBasedSelections'] = inputScript + const selectionRanges: Selections['graphSelections'] = inputScript .split('\n') .filter((ln) => ln.includes('// select for vertical constraint')) .map((ln) => { const comment = ln.split('//')[1] const start = inputScript.indexOf('//' + comment) - 7 return { - type: 'default', - range: [start, start], + codeRef: codeRefFromRange([start, start], ast), } }) @@ -536,7 +541,7 @@ async function helperThing( const ast = parse(inputScript) if (err(ast)) return Promise.reject(ast) - const selectionRanges: Selections['codeBasedSelections'] = inputScript + const selectionRanges: Selections['graphSelections'] = inputScript .split('\n') .filter((ln) => linesOfInterest.some((lineOfInterest) => ln.includes(lineOfInterest)) @@ -545,8 +550,7 @@ async function helperThing( const comment = ln.split('//')[1] const start = inputScript.indexOf('//' + comment) - 7 return { - type: 'default', - range: [start, start], + codeRef: codeRefFromRange([start, start], ast), } }) @@ -605,7 +609,7 @@ part001 = startSketchOn('XY') const ast = parse(code) const constraintLevels: ConstraintLevel[] = ['full', 'partial', 'free'] constraintLevels.forEach((constraintLevel) => { - const recursivelySeachCommentsAndCheckConstraintLevel = ( + const recursivelySearchCommentsAndCheckConstraintLevel = ( str: string, offset: number = 0 ): null => { @@ -622,12 +626,12 @@ part001 = startSketchOn('XY') throw expectedConstraintLevel } expect(expectedConstraintLevel.level).toBe(constraintLevel) - return recursivelySeachCommentsAndCheckConstraintLevel( + return recursivelySearchCommentsAndCheckConstraintLevel( str, index + constraintLevel.length ) } - recursivelySeachCommentsAndCheckConstraintLevel(code) + recursivelySearchCommentsAndCheckConstraintLevel(code) }) }) }) diff --git a/src/lang/std/sketchcombos.ts b/src/lang/std/sketchcombos.ts index 3486502a1..5e2e3f061 100644 --- a/src/lang/std/sketchcombos.ts +++ b/src/lang/std/sketchcombos.ts @@ -7,7 +7,7 @@ import { TransformInfo, } from './stdTypes' import { ToolTip, toolTips } from 'lang/langHelpers' -import { Selections, Selection } from 'lib/selections' +import { Selections } from 'lib/selections' import { cleanErrs, err } from 'lib/trap' import { CallExpression, @@ -19,6 +19,7 @@ import { ProgramMemory, sketchFromKclValue, Literal, + SourceRange, } from '../wasm' import { getNodeFromPath, @@ -1483,11 +1484,8 @@ export function getTransformInfos( ast: Program, constraintType: ConstraintType ): TransformInfo[] { - const paths = selectionRanges.codeBasedSelections.map(({ range }) => - getNodePathFromSourceRange(ast, range) - ) - const nodes = paths.map((pathToNode) => - getNodeFromPath(ast, pathToNode, 'CallExpression') + const nodes = selectionRanges.graphSelections.map(({ codeRef }) => + getNodeFromPath(ast, codeRef.pathToNode, 'CallExpression') ) try { @@ -1515,12 +1513,8 @@ export function getRemoveConstraintsTransforms( ast: Program, constraintType: ConstraintType ): TransformInfo[] | Error { - // return () - const paths = selectionRanges.codeBasedSelections.map((selectionRange) => - getNodePathFromSourceRange(ast, selectionRange.range) - ) - const nodes = paths.map((pathToNode) => - getNodeFromPath(ast, pathToNode) + const nodes = selectionRanges.graphSelections.map(({ codeRef }) => + getNodeFromPath(ast, codeRef.pathToNode) ) const theTransforms = nodes.map((nodeMeta) => { @@ -1571,11 +1565,10 @@ export function transformSecondarySketchLinesTagFirst({ // We need to sort the selections by their start position // so that we can process them in dependency order and not write invalid KCL. - const sortedCodeBasedSelections = - selectionRanges.codeBasedSelections.toSorted( - (a, b) => a.range[0] - b.range[0] - ) - const primarySelection = sortedCodeBasedSelections[0].range + const sortedCodeBasedSelections = selectionRanges.graphSelections.toSorted( + (a, b) => a?.codeRef?.range[0] - b?.codeRef?.range[0] + ) + const primarySelection = sortedCodeBasedSelections[0]?.codeRef?.range const secondarySelections = sortedCodeBasedSelections.slice(1) const _tag = giveSketchFnCallTag(ast, primarySelection, forceSegName) @@ -1586,7 +1579,7 @@ export function transformSecondarySketchLinesTagFirst({ ast: modifiedAst, selectionRanges: { ...selectionRanges, - codeBasedSelections: secondarySelections, + graphSelections: secondarySelections, }, referencedSegmentRange: primarySelection, transformInfos, @@ -1634,8 +1627,8 @@ export function transformAstSketchLines({ transformInfos: TransformInfo[] programMemory: ProgramMemory referenceSegName: string + referencedSegmentRange?: SourceRange forceValueUsedInTransform?: BinaryPart - referencedSegmentRange?: Selection['range'] }): | { modifiedAst: Node @@ -1786,11 +1779,11 @@ export function transformAstSketchLines({ } } - if ('codeBasedSelections' in selectionRanges) { + if ('graphSelections' in selectionRanges) { // If the processing of any of the selections failed, return the first error - const maybeProcessErrors = selectionRanges.codeBasedSelections - .map(({ range }, index) => - processSelection(getNodePathFromSourceRange(node, range), index) + const maybeProcessErrors = selectionRanges.graphSelections + .map(({ codeRef }, index) => + processSelection(getNodePathFromSourceRange(node, codeRef.range), index) ) .filter(err) @@ -1838,7 +1831,7 @@ function getArgLiteralVal(arg: Literal): number | Error { export type ConstraintLevel = 'free' | 'partial' | 'full' export function getConstraintLevelFromSourceRange( - cursorRange: Selection['range'], + cursorRange: SourceRange, ast: Program | Error ): Error | { range: [number, number]; level: ConstraintLevel } { if (err(ast)) return ast diff --git a/src/lang/util.ts b/src/lang/util.ts index 4fe17fcfa..f7147fd04 100644 --- a/src/lang/util.ts +++ b/src/lang/util.ts @@ -1,52 +1,13 @@ import { Selections } from 'lib/selections' import { - Program, PathToNode, CallExpression, Literal, ArrayExpression, BinaryExpression, } from './wasm' -import { getNodeFromPath } from './queryAst' import { ArtifactGraph, filterArtifacts } from 'lang/std/artifactGraph' import { isOverlap } from 'lib/utils' -import { err } from 'lib/trap' - -export function pathMapToSelections( - ast: Program, - prevSelections: Selections, - pathToNodeMap: { [key: number]: PathToNode } -): Selections { - const newSelections: Selections = { - ...prevSelections, - codeBasedSelections: [], - } - Object.entries(pathToNodeMap).forEach(([index, path]) => { - const nodeMeta = getNodeFromPath(ast, path) - if (err(nodeMeta)) return - const node = nodeMeta.node as any - const selection = prevSelections.codeBasedSelections[Number(index)] - if (node) { - if ( - selection.type === 'base-edgeCut' || - selection.type === 'adjacent-edgeCut' || - selection.type === 'opposite-edgeCut' - ) { - newSelections.codeBasedSelections.push({ - range: [node.start, node.end], - type: selection.type, - secondaryRange: selection.secondaryRange, - }) - } else { - newSelections.codeBasedSelections.push({ - range: [node.start, node.end], - type: selection.type, - }) - } - } - }) - return newSelections -} export function updatePathToNodeFromMap( oldPath: PathToNode, @@ -72,11 +33,11 @@ export function isCursorInSketchCommandRange( { types: ['segment', 'path'], predicate: (artifact) => { - return selectionRanges.codeBasedSelections.some( + return selectionRanges.graphSelections.some( (selection) => - Array.isArray(selection?.range) && + Array.isArray(selection?.codeRef?.range) && Array.isArray(artifact?.codeRef?.range) && - isOverlap(selection.range, artifact.codeRef.range) + isOverlap(selection?.codeRef?.range, artifact.codeRef.range) ) }, }, diff --git a/src/lib/commandBarConfigs/modelingCommandConfig.ts b/src/lib/commandBarConfigs/modelingCommandConfig.ts index 416a05056..46a5f6737 100644 --- a/src/lib/commandBarConfigs/modelingCommandConfig.ts +++ b/src/lib/commandBarConfigs/modelingCommandConfig.ts @@ -233,8 +233,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< args: { selection: { inputType: 'selection', - // TODO: These are products of an extrude - selectionTypes: ['extrude-wall', 'start-cap', 'end-cap'], + selectionTypes: ['solid2D', 'segment'], multiple: false, // TODO: multiple selection required: true, skip: true, @@ -265,7 +264,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< args: { selection: { inputType: 'selection', - selectionTypes: ['extrude-wall', 'start-cap', 'end-cap'], + selectionTypes: ['solid2D', 'segment'], multiple: false, // TODO: multiple selection required: true, skip: true, @@ -284,20 +283,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< args: { selection: { inputType: 'selection', - selectionTypes: [ - 'default', - 'line-end', - 'line-mid', - 'extrude-wall', - 'solid2D', - 'start-cap', - 'end-cap', - 'point', - 'edge', - 'line', - 'arc', - 'all', - ], + selectionTypes: ['segment', 'sweepEdge', 'edgeCutEdge'], multiple: true, required: true, skip: false, diff --git a/src/lib/commandTypes.ts b/src/lib/commandTypes.ts index 9371c7834..ccbfd6df1 100644 --- a/src/lib/commandTypes.ts +++ b/src/lib/commandTypes.ts @@ -1,12 +1,12 @@ import { CustomIconName } from 'components/CustomIcon' import { AllMachines } from 'hooks/useStateMachineCommands' import { Actor, AnyStateMachine, ContextFrom, EventFrom } from 'xstate' -import { Selection } from './selections' import { Identifier, Expr, VariableDeclaration } from 'lang/wasm' import { commandBarMachine } from 'machines/commandBarMachine' import { ReactNode } from 'react' import { MachineManager } from 'components/MachineManagerProvider' import { Node } from 'wasm-lib/kcl/bindings/Node' +import { Artifact } from 'lang/std/artifactGraph' type Icon = CustomIconName const PLATFORMS = ['both', 'web', 'desktop'] as const @@ -144,7 +144,7 @@ export type CommandArgumentConfig< } | { inputType: 'selection' - selectionTypes: Selection['type'][] + selectionTypes: Artifact['type'][] multiple: boolean } | { inputType: 'kcl'; defaultValue?: string } // KCL expression inputs have simple strings as default values @@ -218,7 +218,7 @@ export type CommandArgument< } | { inputType: 'selection' - selectionTypes: Selection['type'][] + selectionTypes: Artifact['type'][] multiple: boolean } | { inputType: 'kcl'; defaultValue?: string } // KCL expression inputs have simple strings as default value diff --git a/src/lib/selections.ts b/src/lib/selections.ts index ca165569a..62607a91b 100644 --- a/src/lib/selections.ts +++ b/src/lib/selections.ts @@ -5,7 +5,7 @@ import { kclManager, sceneEntitiesManager, } from 'lib/singletons' -import { CallExpression, SourceRange, Expr, parse } from 'lang/wasm' +import { CallExpression, SourceRange, Expr } from 'lang/wasm' import { ModelingMachineEvent } from 'machines/modelingMachine' import { isNonNullable, uuidv4 } from 'lib/utils' import { EditorSelection, SelectionRange } from '@codemirror/state' @@ -15,6 +15,7 @@ import { Program } from 'lang/wasm' import { doesPipeHaveCallExp, getNodeFromPath, + getNodePathFromSourceRange, hasSketchPipeBeenExtruded, isSingleCursorInPipe, } from 'lang/queryAst' @@ -28,12 +29,15 @@ import { AXIS_GROUP, X_AXIS } from 'clientSideScene/sceneInfra' import { PathToNodeMap } from 'lang/std/sketchcombos' import { err } from 'lib/trap' import { + Artifact, getArtifactOfTypes, getArtifactsOfTypes, getCapCodeRef, getSweepEdgeCodeRef, getSolid2dCodeRef, getWallCodeRef, + CodeRef, + getCodeRefsByArtifactId, ArtifactId, } from 'lang/std/artifactGraph' import { Node } from 'wasm-lib/kcl/bindings/Node' @@ -43,7 +47,8 @@ export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01' export type Axis = 'y-axis' | 'x-axis' | 'z-axis' -export type Selection = +/** @deprecated Use {@link Artifact} instead. */ +type Selection__old = | { type: | 'default' @@ -67,9 +72,88 @@ export type Selection = // TODO this is a temporary measure that well be made redundant with: https://github.com/KittyCAD/modeling-app/pull/3836 secondaryRange: SourceRange } -export type Selections = { +/** @deprecated Use {@link Selection} instead. */ +export type Selections__old = { otherSelections: Axis[] - codeBasedSelections: Selection[] + codeBasedSelections: Selection__old[] +} +export interface Selection { + artifact?: Artifact + codeRef: CodeRef +} +export type Selections = { + otherSelections: Array + graphSelections: Array +} + +/** @deprecated If you're writing a new function, it should use {@link Selection} and not {@link Selection__old} + * this function should only be used for backwards compatibility with old functions. + */ +function convertSelectionToOld(selection: Selection): Selection__old | null { + // return {} as Selection__old + // TODO implementation + const _artifact = selection.artifact + if (_artifact?.type === 'solid2D') { + const codeRef = getSolid2dCodeRef( + _artifact, + engineCommandManager.artifactGraph + ) + if (err(codeRef)) return null + return { range: codeRef.range, type: 'solid2D' } + } + if (_artifact?.type === 'cap') { + const codeRef = getCapCodeRef(_artifact, engineCommandManager.artifactGraph) + if (err(codeRef)) return null + return { + range: codeRef.range, + type: _artifact?.subType === 'end' ? 'end-cap' : 'start-cap', + } + } + if (_artifact?.type === 'wall') { + const codeRef = getWallCodeRef( + _artifact, + engineCommandManager.artifactGraph + ) + if (err(codeRef)) return null + return { range: codeRef.range, type: 'extrude-wall' } + } + if (_artifact?.type === 'segment' || _artifact?.type === 'path') { + return { range: _artifact.codeRef.range, type: 'default' } + } + if (_artifact?.type === 'sweepEdge') { + const codeRef = getSweepEdgeCodeRef( + _artifact, + engineCommandManager.artifactGraph + ) + if (err(codeRef)) return null + if (_artifact?.subType === 'adjacent') { + return { range: codeRef.range, type: 'adjacent-edge' } + } + return { range: codeRef.range, type: 'edge' } + } + if (_artifact?.type === 'edgeCut') { + const codeRef = _artifact.codeRef + return { range: codeRef.range, type: 'default' } + } + if (selection?.codeRef?.range) { + return { range: selection.codeRef.range, type: 'default' } + } + return null +} +/** @deprecated If you're writing a new function, it should use {@link Selection} and not {@link Selection__old} + * this function should only be used for backwards compatibility with old functions. + */ +export function convertSelectionsToOld(selection: Selections): Selections__old { + const selections: Selection__old[] = [] + for (const artifact of selection.graphSelections) { + const converted = convertSelectionToOld(artifact) + if (converted) selections.push(converted) + } + const selectionsOld: Selections__old = { + otherSelections: selection.otherSelections, + codeBasedSelections: selections, + } + return selectionsOld } export async function getEventForSelectWithPoint({ @@ -94,127 +178,18 @@ export async function getEventForSelectWithPoint({ } } let _artifact = engineCommandManager.artifactGraph.get(data.entity_id) - if (!_artifact) - return { - type: 'Set selection', - data: { selectionType: 'singleCodeCursor' }, - } - if (_artifact.type === 'solid2D') { - const codeRef = getSolid2dCodeRef( - _artifact, - engineCommandManager.artifactGraph - ) - if (err(codeRef)) return null - return { - type: 'Set selection', - data: { - selectionType: 'singleCodeCursor', - selection: { range: codeRef.range, type: 'solid2D' }, - }, - } - } - if (_artifact.type === 'cap') { - const codeRef = getCapCodeRef(_artifact, engineCommandManager.artifactGraph) - if (err(codeRef)) return null + const codeRefs = getCodeRefsByArtifactId( + data.entity_id, + engineCommandManager.artifactGraph + ) + if (_artifact && codeRefs) { return { type: 'Set selection', data: { selectionType: 'singleCodeCursor', selection: { - range: codeRef.range, - type: _artifact?.subType === 'end' ? 'end-cap' : 'start-cap', - }, - }, - } - } - if (_artifact.type === 'wall') { - const codeRef = getWallCodeRef( - _artifact, - engineCommandManager.artifactGraph - ) - if (err(codeRef)) return null - return { - type: 'Set selection', - data: { - selectionType: 'singleCodeCursor', - selection: { range: codeRef.range, type: 'extrude-wall' }, - }, - } - } - if (_artifact.type === 'segment' || _artifact.type === 'path') { - return { - type: 'Set selection', - data: { - selectionType: 'singleCodeCursor', - selection: { range: _artifact.codeRef.range, type: 'default' }, - }, - } - } - if (_artifact.type === 'sweepEdge') { - const codeRef = getSweepEdgeCodeRef( - _artifact, - engineCommandManager.artifactGraph - ) - if (err(codeRef)) return null - if (_artifact?.subType === 'adjacent') { - return { - type: 'Set selection', - data: { - selectionType: 'singleCodeCursor', - selection: { range: codeRef.range, type: 'adjacent-edge' }, - }, - } - } - return { - type: 'Set selection', - data: { - selectionType: 'singleCodeCursor', - selection: { range: codeRef.range, type: 'edge' }, - }, - } - } - if (_artifact.type === 'edgeCut') { - const consumedEdge = getArtifactOfTypes( - { key: _artifact.consumedEdgeId, types: ['segment', 'sweepEdge'] }, - engineCommandManager.artifactGraph - ) - if (err(consumedEdge)) - return { - type: 'Set selection', - data: { - selectionType: 'singleCodeCursor', - selection: { range: _artifact.codeRef.range, type: 'default' }, - }, - } - if (consumedEdge.type === 'segment') { - return { - type: 'Set selection', - data: { - selectionType: 'singleCodeCursor', - selection: { - range: _artifact.codeRef.range, - type: 'base-edgeCut', - secondaryRange: consumedEdge.codeRef.range, - }, - }, - } - } - const segment = getArtifactOfTypes( - { key: consumedEdge.segId, types: ['segment'] }, - engineCommandManager.artifactGraph - ) - if (err(segment)) return null - return { - type: 'Set selection', - data: { - selectionType: 'singleCodeCursor', - selection: { - range: _artifact.codeRef.range, - type: - consumedEdge.subType === 'adjacent' - ? 'adjacent-edgeCut' - : 'opposite-edgeCut', - secondaryRange: segment.codeRef.range, + artifact: _artifact, + codeRef: codeRefs[0], }, }, } @@ -237,28 +212,55 @@ export function getEventForSegmentSelection( }, } } - const pathToNode = group?.userData?.pathToNode - if (!pathToNode) return null - // previous drags don't update ast for efficiency reasons - // So we want to make sure we have and updated ast with - // accurate source ranges - const updatedAst = parse(codeManager.code) - if (err(updatedAst)) return null + // id does not match up with the artifact graph when in sketch mode, because mock executions + // do not update the artifact graph, therefore we match up the pathToNode instead + // we can reliably use `type === 'segment'` since it's in sketch mode and we're concerned with segments + const segWithMatchingPathToNode__Id = [ + ...engineCommandManager.artifactGraph, + ].find((entry) => { + return ( + entry[1].type === 'segment' && + JSON.stringify(entry[1].codeRef.pathToNode) === + JSON.stringify(group?.userData?.pathToNode) + ) + })?.[0] - const nodeMeta = getNodeFromPath>( - updatedAst, - pathToNode, - 'CallExpression' + const id = segWithMatchingPathToNode__Id + + if (!id && group) { + const node = getNodeFromPath( + kclManager.ast, + group.userData.pathToNode + ) + if (err(node)) return null + return { + type: 'Set selection', + data: { + selectionType: 'singleCodeCursor', + selection: { + codeRef: { + range: [node.node.start, node.node.end], + pathToNode: group.userData.pathToNode, + }, + }, + }, + } + } + if (!id || !group) return null + const artifact = engineCommandManager.artifactGraph.get(id) + const codeRefs = getCodeRefsByArtifactId( + id, + engineCommandManager.artifactGraph ) - if (err(nodeMeta)) return null - - const node = nodeMeta.node - const range: SourceRange = [node.start, node.end] + if (!artifact || !codeRefs) return null return { type: 'Set selection', data: { selectionType: 'singleCodeCursor', - selection: { range, type: 'default' }, + selection: { + artifact, + codeRef: codeRefs[0], + }, }, } } @@ -274,13 +276,24 @@ export function handleSelectionBatch({ updateSceneObjectColors: () => void } { const ranges: ReturnType[] = [] + const selectionToEngine: SelectionToEngine[] = [] + + selections.graphSelections.forEach(({ artifact }) => { + artifact?.id && + selectionToEngine.push({ + type: 'default', + id: artifact?.id, + range: getCodeRefsByArtifactId( + artifact.id, + engineCommandManager.artifactGraph + )?.[0].range || [0, 0], + }) + }) const engineEvents: Models['WebSocketRequest_type'][] = - resetAndSetEngineEntitySelectionCmds( - codeToIdSelections(selections.codeBasedSelections) - ) - selections.codeBasedSelections.forEach(({ range, type }) => { - if (range?.[1]) { - ranges.push(EditorSelection.cursor(range[1])) + resetAndSetEngineEntitySelectionCmds(selectionToEngine) + selections.graphSelections.forEach(({ codeRef }) => { + if (codeRef.range?.[1]) { + ranges.push(EditorSelection.cursor(codeRef.range[1])) } }) if (ranges.length) @@ -288,11 +301,11 @@ export function handleSelectionBatch({ engineEvents, codeMirrorSelection: EditorSelection.create( ranges, - selections.codeBasedSelections.length - 1 + selections.graphSelections.length - 1 ), otherSelections: selections.otherSelections, updateSceneObjectColors: () => - updateSceneObjectColors(selections.codeBasedSelections), + updateSceneObjectColors(selections.graphSelections), } return { @@ -303,43 +316,75 @@ export function handleSelectionBatch({ engineEvents, otherSelections: selections.otherSelections, updateSceneObjectColors: () => - updateSceneObjectColors(selections.codeBasedSelections), + updateSceneObjectColors(selections.graphSelections), } } -type SelectionToEngine = { type: Selection['type']; id: string } +type SelectionToEngine = { + type: Selection__old['type'] + id?: string + range: SourceRange +} export function processCodeMirrorRanges({ codeMirrorRanges, selectionRanges, isShiftDown, + ast, }: { codeMirrorRanges: readonly SelectionRange[] selectionRanges: Selections isShiftDown: boolean + ast: Program }): null | { modelingEvent: ModelingMachineEvent engineEvents: Models['WebSocketRequest_type'][] } { const isChange = - codeMirrorRanges.length !== selectionRanges.codeBasedSelections.length || + codeMirrorRanges.length !== selectionRanges?.graphSelections?.length || codeMirrorRanges.some(({ from, to }, i) => { return ( - from !== selectionRanges.codeBasedSelections[i].range[0] || - to !== selectionRanges.codeBasedSelections[i].range[1] + from !== selectionRanges.graphSelections[i]?.codeRef?.range[0] || + to !== selectionRanges.graphSelections[i]?.codeRef?.range[1] ) }) if (!isChange) return null - const codeBasedSelections: Selections['codeBasedSelections'] = + const codeBasedSelections: Selections['graphSelections'] = codeMirrorRanges.map(({ from, to }) => { + const pathToNode = getNodePathFromSourceRange(ast, [from, to]) return { - type: 'default', - range: [from, to], + codeRef: { + range: [from, to], + pathToNode, + }, } }) const idBasedSelections: SelectionToEngine[] = codeToIdSelections(codeBasedSelections) + const selections: Selection[] = [] + for (const { id, range } of idBasedSelections) { + if (!id) { + const pathToNode = getNodePathFromSourceRange(ast, range) + selections.push({ + codeRef: { + range, + pathToNode, + }, + }) + continue + } + const artifact = engineCommandManager.artifactGraph.get(id) + const codeRefs = getCodeRefsByArtifactId( + id, + engineCommandManager.artifactGraph + ) + if (artifact && codeRefs) { + selections.push({ artifact, codeRef: codeRefs[0] }) + } else if (codeRefs) { + selections.push({ codeRef: codeRefs[0] }) + } + } if (!selectionRanges) return null updateSceneObjectColors(codeBasedSelections) @@ -350,11 +395,13 @@ export function processCodeMirrorRanges({ selectionType: 'mirrorCodeMirrorSelections', selection: { otherSelections: isShiftDown ? selectionRanges.otherSelections : [], - codeBasedSelections, + graphSelections: selections, }, }, }, - engineEvents: resetAndSetEngineEntitySelectionCmds(idBasedSelections), + engineEvents: resetAndSetEngineEntitySelectionCmds( + idBasedSelections.filter(({ id }) => !!id) + ), } } @@ -371,7 +418,7 @@ function updateSceneObjectColors(codeBasedSelections: Selection[]) { if (err(nodeMeta)) return const node = nodeMeta.node const groupHasCursor = codeBasedSelections.some((selection) => { - return isOverlap(selection.range, [node.start, node.end]) + return isOverlap(selection?.codeRef?.range, [node.start, node.end]) }) const color = groupHasCursor @@ -406,7 +453,7 @@ function resetAndSetEngineEntitySelectionCmds( type: 'modeling_cmd_req', cmd: { type: 'select_add', - entities: selections.map(({ id }) => id), + entities: selections.map(({ id }) => id).filter(isNonNullable), }, cmd_id: uuidv4(), }, @@ -426,14 +473,14 @@ export function isSelectionLastLine( code: string, i = 0 ) { - return selectionRanges.codeBasedSelections[i].range[1] === code.length + return selectionRanges.graphSelections[i]?.codeRef?.range[1] === code.length } export function isRangeBetweenCharacters(selectionRanges: Selections) { return ( - selectionRanges.codeBasedSelections.length === 1 && - selectionRanges.codeBasedSelections[0].range[0] === 0 && - selectionRanges.codeBasedSelections[0].range[1] === 0 + selectionRanges.graphSelections.length === 1 && + selectionRanges.graphSelections[0]?.codeRef?.range[0] === 0 && + selectionRanges.graphSelections[0]?.codeRef?.range[1] === 0 ) } @@ -444,7 +491,7 @@ export type CommonASTNode = { function buildCommonNodeFromSelection(selectionRanges: Selections, i: number) { return { - selection: selectionRanges.codeBasedSelections[i], + selection: selectionRanges.graphSelections[i], ast: kclManager.ast, } } @@ -476,7 +523,7 @@ function nodeHasCircle(node: CommonASTNode) { } export function canSweepSelection(selection: Selections) { - const commonNodes = selection.codeBasedSelections.map((_, i) => + const commonNodes = selection.graphSelections.map((_, i) => buildCommonNodeFromSelection(selection, i) ) return ( @@ -488,33 +535,8 @@ export function canSweepSelection(selection: Selections) { ) } -export function canFilletSelection(selection: Selections) { - const commonNodes = selection.codeBasedSelections.map((_, i) => - buildCommonNodeFromSelection(selection, i) - ) // TODO FILLET DUMMY PLACEHOLDER - return ( - !!isSketchPipe(selection) && - commonNodes.every((n) => nodeHasClose(n)) && - commonNodes.every((n) => !nodeHasExtrude(n)) - ) -} - -function canExtrudeSelectionItem(selection: Selections, i: number) { - const isolatedSelection = { - ...selection, - codeBasedSelections: [selection.codeBasedSelections[i]], - } - const commonNode = buildCommonNodeFromSelection(selection, i) - - return ( - !!isSketchPipe(isolatedSelection) && - (nodeHasClose(commonNode) || nodeHasCircle(commonNode)) && - !nodeHasExtrude(commonNode) - ) -} - // This accounts for non-geometry selections under "other" -export type ResolvedSelectionType = [Selection['type'] | 'other', number] +export type ResolvedSelectionType = [Artifact['type'] | 'other', number] /** * In the future, I'd like this function to properly return the type of each selected entity based on @@ -527,17 +549,16 @@ export function getSelectionType( selection?: Selections ): ResolvedSelectionType[] { if (!selection) return [] - const extrudableCount = selection.codeBasedSelections.filter((_, i) => { - const singleSelection = { - ...selection, - codeBasedSelections: [selection.codeBasedSelections[i]], - } - return canExtrudeSelectionItem(singleSelection, 0) - }).length - - return extrudableCount === selection.codeBasedSelections.length - ? [['extrude-wall', extrudableCount]] - : [['other', selection.codeBasedSelections.length]] + const selectionsWithArtifacts = selection.graphSelections.filter( + (s) => !!s.artifact + ) + const firstSelection = selectionsWithArtifacts[0] + const firstSelectionType = firstSelection?.artifact?.type + if (!firstSelectionType) return [] + const selectionsWithSameType = selectionsWithArtifacts.filter( + (s) => s.artifact?.type === firstSelection.artifact?.type + ) + return [[firstSelectionType, selectionsWithSameType.length]] } export function getSelectionTypeDisplayText( @@ -549,9 +570,10 @@ export function getSelectionTypeDisplayText( .map( // Hack for showing "face" instead of "extrude-wall" in command bar text ([type, count]) => - `${count} ${type.replace('extrude-wall', 'face')}${ - count > 1 ? 's' : '' - }` + `${count} ${type + .replace('wall', 'face') + .replace('solid2D', 'face') + .replace('segment', 'face')}${count > 1 ? 's' : ''}` ) .join(', ') } @@ -572,10 +594,14 @@ export function canSubmitSelectionArg( ) } -function codeToIdSelections( - codeBasedSelections: Selection[] +export function codeToIdSelections( + selections: Selection[] ): SelectionToEngine[] { - return codeBasedSelections + const selectionsOld = convertSelectionsToOld({ + graphSelections: selections, + otherSelections: [], + }).codeBasedSelections + return selectionsOld .flatMap((selection): null | SelectionToEngine[] => { const { type } = selection // TODO #868: loops over all artifacts will become inefficient at a large scale @@ -607,19 +633,26 @@ function codeToIdSelections( | { id: ArtifactId artifact: unknown - selection: Selection + selection: Selection__old } | undefined overlappingEntries.forEach((entry) => { + // TODO probably need to remove much of the `type === 'xyz'` below if (type === 'default' && entry.artifact.type === 'segment') { bestCandidate = entry return } - if (type === 'solid2D' && entry.artifact.type === 'path') { - const solid = engineCommandManager.artifactGraph.get( + if (entry.artifact.type === 'path') { + const artifact = engineCommandManager.artifactGraph.get( entry.artifact.solid2dId || '' ) - if (solid?.type !== 'solid2D') return + if (artifact?.type !== 'solid2D') { + bestCandidate = { + artifact: entry.artifact, + selection, + id: entry.id, + } + } if (!entry.artifact.solid2dId) { console.error( 'Expected PathArtifact to have solid2dId, but none found' @@ -627,7 +660,7 @@ function codeToIdSelections( return } bestCandidate = { - artifact: solid, + artifact: artifact, selection, id: entry.artifact.solid2dId, } @@ -752,10 +785,11 @@ function codeToIdSelections( { type, id: bestCandidate.id, + range: bestCandidate.selection.range, }, ] } - return null + return [selection] }) .filter(isNonNullable) } @@ -798,32 +832,58 @@ export function updateSelections( const newSelections = Object.entries(pathToNodeMap) .map(([index, pathToNode]): Selection | undefined => { + const previousSelection = + prevSelectionRanges.graphSelections[Number(index)] const nodeMeta = getNodeFromPath(ast, pathToNode) if (err(nodeMeta)) return undefined const node = nodeMeta.node - const selection = prevSelectionRanges.codeBasedSelections[Number(index)] - if ( - selection?.type === 'base-edgeCut' || - selection?.type === 'adjacent-edgeCut' || - selection?.type === 'opposite-edgeCut' - ) - return { - range: [node.start, node.end], - type: selection?.type, - secondaryRange: selection?.secondaryRange, + let artifact: Artifact | null = null + for (const [id, a] of engineCommandManager.artifactGraph) { + if (previousSelection?.artifact?.type === a.type) { + const codeRefs = getCodeRefsByArtifactId( + id, + engineCommandManager.artifactGraph + ) + if (!codeRefs) continue + if ( + JSON.stringify(codeRefs[0].pathToNode) === + JSON.stringify(pathToNode) + ) { + artifact = a + console.log('found artifact', a) + break + } } + } + if (!artifact) return undefined return { - range: [node.start, node.end], - type: selection?.type, + artifact: artifact, + codeRef: { + range: [node.start, node.end], + pathToNode: pathToNode, + }, } }) .filter((x?: Selection) => x !== undefined) as Selection[] + // for when there is no artifact (sketch mode since mock execute does not update artifactGraph) + const pathToNodeBasedSelections: Selections['graphSelections'] = [] + for (const pathToNode of Object.values(pathToNodeMap)) { + const node = getNodeFromPath(ast, pathToNode) + if (err(node)) return node + pathToNodeBasedSelections.push({ + codeRef: { + range: [node.node.start, node.node.end], + pathToNode: pathToNode, + }, + }) + } + return { - codeBasedSelections: - newSelections.length > 0 + graphSelections: + newSelections.length >= pathToNodeBasedSelections.length ? newSelections - : prevSelectionRanges.codeBasedSelections, + : pathToNodeBasedSelections, otherSelections: prevSelectionRanges.otherSelections, } } diff --git a/src/lib/singletons.ts b/src/lib/singletons.ts index a4d1e8d1b..55a9de5ae 100644 --- a/src/lib/singletons.ts +++ b/src/lib/singletons.ts @@ -18,8 +18,6 @@ window.tearDown = engineCommandManager.tearDown export const kclManager = new KclManager(engineCommandManager) engineCommandManager.kclManager = kclManager -engineCommandManager.getAstCb = () => kclManager.ast - export const sceneInfra = new SceneInfra(engineCommandManager) engineCommandManager.camControlsCameraChange = sceneInfra.onCameraChange diff --git a/src/lib/useCalculateKclExpression.ts b/src/lib/useCalculateKclExpression.ts index bc3b74bc5..60d1ecfe3 100644 --- a/src/lib/useCalculateKclExpression.ts +++ b/src/lib/useCalculateKclExpression.ts @@ -35,8 +35,8 @@ export function useCalculateKclExpression({ const { programMemory, code } = useKclContext() const { context } = useModelingContext() const selectionRange: - | (typeof context.selectionRanges.codeBasedSelections)[number]['range'] - | undefined = context.selectionRanges.codeBasedSelections[0]?.range + | (typeof context)['selectionRanges']['graphSelections'][number]['codeRef']['range'] + | undefined = context.selectionRanges.graphSelections[0]?.codeRef?.range const inputRef = useRef(null) const [availableVarInfo, setAvailableVarInfo] = useState< ReturnType @@ -102,6 +102,7 @@ export function useCalculateKclExpression({ engineCommandManager, useFakeExecutor: true, programMemoryOverride: kclManager.programMemory.clone(), + idGenerator: kclManager.execState.idGenerator, }) const resultDeclaration = ast.body.find( (a) => diff --git a/src/lib/usePreviousVariables.ts b/src/lib/usePreviousVariables.ts index 19db4ee5a..4082f6800 100644 --- a/src/lib/usePreviousVariables.ts +++ b/src/lib/usePreviousVariables.ts @@ -7,7 +7,7 @@ import { useEffect, useState } from 'react' export function usePreviousVariables() { const { programMemory, code } = useKclContext() const { context } = useModelingContext() - const selectionRange = context.selectionRanges.codeBasedSelections[0] + const selectionRange = context.selectionRanges.graphSelections[0]?.codeRef ?.range || [code.length, code.length] const [previousVariablesInfo, setPreviousVariablesInfo] = useState< ReturnType diff --git a/src/machines/commandBarMachine.ts b/src/machines/commandBarMachine.ts index fe74361ac..8e1211e3d 100644 --- a/src/machines/commandBarMachine.ts +++ b/src/machines/commandBarMachine.ts @@ -5,7 +5,7 @@ import { CommandArgumentWithName, KclCommandValue, } from 'lib/commandTypes' -import { Selections } from 'lib/selections' +import { Selections__old } from 'lib/selections' import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils' import { MachineManager } from 'components/MachineManagerProvider' @@ -13,7 +13,7 @@ export type CommandBarContext = { commands: Command[] selectedCommand?: Command currentArgument?: CommandArgument & { name: string } - selectionRanges: Selections + selectionRanges: Selections__old argumentsToSubmit: { [x: string]: unknown } machineManager: MachineManager } diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index 9f9810432..4585d755c 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -5,7 +5,7 @@ import { parse, recast, } from 'lang/wasm' -import { Axis, Selection, Selections, updateSelections } from 'lib/selections' +import { Axis, Selections, Selection, updateSelections } from 'lib/selections' import { assign, fromPromise, setup } from 'xstate' import { SidebarType } from 'components/ModelingSidebar/ModelingPanes' import { @@ -343,7 +343,7 @@ export const modelingMachineDefaultContext: ModelingMachineContext = { selection: [], selectionRanges: { otherSelections: [], - codeBasedSelections: [], + graphSelections: [], }, sketchDetails: { sketchPathToNode: [], @@ -405,7 +405,7 @@ export const modelingMachine = setup({ }, 'Can constrain horizontal distance': ({ context: { selectionRanges } }) => { const info = horzVertDistanceInfo({ - selectionRanges, + selectionRanges: selectionRanges, constraint: 'setHorzDistance', }) if (trap(info)) return false @@ -413,24 +413,32 @@ export const modelingMachine = setup({ }, 'Can constrain vertical distance': ({ context: { selectionRanges } }) => { const info = horzVertDistanceInfo({ - selectionRanges, + selectionRanges: selectionRanges, constraint: 'setVertDistance', }) if (trap(info)) return false return info.enabled }, 'Can constrain ABS X': ({ context: { selectionRanges } }) => { - const info = absDistanceInfo({ selectionRanges, constraint: 'xAbs' }) + const info = absDistanceInfo({ + selectionRanges, + constraint: 'xAbs', + }) if (trap(info)) return false return info.enabled }, 'Can constrain ABS Y': ({ context: { selectionRanges } }) => { - const info = absDistanceInfo({ selectionRanges, constraint: 'yAbs' }) + const info = absDistanceInfo({ + selectionRanges, + constraint: 'yAbs', + }) if (trap(info)) return false return info.enabled }, 'Can constrain angle': ({ context: { selectionRanges } }) => { - const angleBetween = angleBetweenInfo({ selectionRanges }) + const angleBetween = angleBetweenInfo({ + selectionRanges, + }) if (trap(angleBetween)) return false const angleLength = angleLengthInfo({ selectionRanges, @@ -440,7 +448,9 @@ export const modelingMachine = setup({ return angleBetween.enabled || angleLength.enabled }, 'Can constrain length': ({ context: { selectionRanges } }) => { - const angleLength = angleLengthInfo({ selectionRanges }) + const angleLength = angleLengthInfo({ + selectionRanges, + }) if (trap(angleLength)) return false return angleLength.enabled }, @@ -453,7 +463,7 @@ export const modelingMachine = setup({ }, 'Can constrain horizontally align': ({ context: { selectionRanges } }) => { const info = horzVertDistanceInfo({ - selectionRanges, + selectionRanges: selectionRanges, constraint: 'setHorzDistance', }) if (trap(info)) return false @@ -461,7 +471,7 @@ export const modelingMachine = setup({ }, 'Can constrain vertically align': ({ context: { selectionRanges } }) => { const info = horzVertDistanceInfo({ - selectionRanges, + selectionRanges: selectionRanges, constraint: 'setHorzDistance', }) if (trap(info)) return false @@ -484,12 +494,16 @@ export const modelingMachine = setup({ return info.enabled }, 'Can constrain equal length': ({ context: { selectionRanges } }) => { - const info = setEqualLengthInfo({ selectionRanges }) + const info = setEqualLengthInfo({ + selectionRanges, + }) if (trap(info)) return false return info.enabled }, 'Can canstrain parallel': ({ context: { selectionRanges } }) => { - const info = equalAngleInfo({ selectionRanges }) + const info = equalAngleInfo({ + selectionRanges, + }) if (err(info)) return false return info.enabled }, @@ -603,7 +617,7 @@ export const modelingMachine = setup({ } const pathToNode = getNodePathFromSourceRange( ast, - selection.codeBasedSelections[0].range + selection.graphSelections[0]?.codeRef.range ) const extrudeSketchRes = extrudeSketch( ast, @@ -620,7 +634,7 @@ export const modelingMachine = setup({ focusPath: [pathToExtrudeArg], zoomToFit: true, zoomOnRangeAndType: { - range: selection.codeBasedSelections[0].range, + range: selection.graphSelections[0]?.codeRef.range, type: 'path', }, }) @@ -649,7 +663,7 @@ export const modelingMachine = setup({ } const pathToNode = getNodePathFromSourceRange( ast, - selection.codeBasedSelections[0].range + selection.graphSelections[0]?.codeRef.range ) const revolveSketchRes = revolveSketch( ast, @@ -664,7 +678,7 @@ export const modelingMachine = setup({ focusPath: [pathToRevolveArg], zoomToFit: true, zoomOnRangeAndType: { - range: selection.codeBasedSelections[0].range, + range: selection.graphSelections[0]?.codeRef.range, type: 'path', }, }) @@ -682,7 +696,7 @@ export const modelingMachine = setup({ const modifiedAst = await deleteFromSelection( ast, - selectionRanges.codeBasedSelections[0], + selectionRanges.graphSelections[0], kclManager.programMemory, getFaceDetails ) @@ -1161,7 +1175,7 @@ export const modelingMachine = setup({ input: Pick }) => { const constraint = applyConstraintHorzVertAlign({ - selectionRanges, + selectionRanges: selectionRanges, constraint: 'setVertDistance', }) if (trap(constraint)) return @@ -1195,7 +1209,7 @@ export const modelingMachine = setup({ input: Pick }) => { const constraint = applyConstraintHorzVertAlign({ - selectionRanges, + selectionRanges: selectionRanges, constraint: 'setHorzDistance', }) if (trap(constraint)) return @@ -1456,7 +1470,7 @@ export const modelingMachine = setup({ }, // end services }).createMachine({ - /** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0ANhoBWAHQAOAMwB2KQEY5AFgCcGqWqkAaEAE9Ew0RLEqa64TIBMKmTUXCAvk71oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBEEpYSkJOUUaOWsxeylrWzk9QwQClQkrVOsZNWExFItnVxB3LDxCXwCAW1QAVyDA9hJ2MAjeGI4ueNBE5KkaCWspZfMM+XE1YsQZMQWxNQtxao0ZeRc3dDavTv9ybhG+djGoibjeWZSZCRUxJa1flTCNTWLYIYS2CT2RSKdRyGhZawWc4tS6eDp+fy+KBdMC4AIAeQAbmAAE6YEj6WDPZisSbcd5CYHCSFqOQ1NSKWRiMRA0HqSTKblSX48+pKZGtNESbAQTBgAgAUTxpMCAGs-OQABbU6K0t4JIQ5RTfNQyYSZTm2YFiUHZNTpKQKR0KYQqRQ5CWo9rS2XypUjElqjXaxSRGmxKYMpJlB3WKqKVnLOoggxGc2mbnQ11iOTaDmejzemVyxWPEm9DA616Rg1JHPWdLg7PwtRu06gwVpHaibTzApZMQFq5QH0lgBKYEJqEwxKreprMyEOeNv3y8xUUhUChkHeFaV2pv2Mk3NhkQ6lxflADFsJg5U96ON5-Ta4Ic19W8saMY2fCCh2VHMdIciyGR3WPapzyLX1SxYEkHzDXUIxfRc61USFFHfONjnsFMSkcepFhoLRc0RGwNEUKD8FHeVkBIdU52Q6Z+CXORJB+DdWV+URuR3VMEEwwEKlUKxvx+eRVCokdLwIAARYIRkCYJQimRi6WY2Z32kTIsmMMDOQsDs1EIlRrFNORwRbMyz2aSVoJLAAVMBHkEdhUEEIgAEFZLU-VULfdCFG0RQakAmRTI7cKFi0a16h5QC3SkmiJE1GUwAABXJXA4AITz4OwAAzEhQn8KASRIJhNX8FgmF6ckRggXyFxYpIeIqDcsi3Cx8myDsxMhVIFBhXJzVsJLLwkWBNVQAB3TKyByvLOCKkqyoqqqwC6JhOEgJqUJat9tHSHNTLkDI4zhPDEHdTkKgsYzgQutkmguQtqO8dV2C1YgyEoTAPuDPaNMQYzymMN0FAyeoMk2fj3WqCpEWenYCjqGzXuHCQAa+zU5IUsAlOxXEEKfJioyUL4CjOizzFMjk+JKA4JAcLcrVp-JXSS7HvruDB-EgDh-AgXoSXaIMcaBqNoRCiRakdMDxEyV1QVZBtRo0GhjxhfJKNsr13s+rUscNzUAEkYLo9VCZxPF-GJfLyBITBJdrTJ9nSGoc1kBNdhzADuWkBxjPEb2DikLmTeN4NzZLS2CaCInbem0WAC97idl3UMyOwJAI04TvyQFIrMZmtybVIYSbCPgyjnGY-lIhuFgdhyrwfxk+wNO8SdoXsGb37RkfF5n2BgSaAKUwNdpuEajY0FbGPDNVByA9fk16ucdrrV6+IJuW5INv7c4R3MF7-uKEHxDq32xJMnkUwaGM-JrAsnkGcQapHGZuNMm0F-hDZBvI23MzYwUbrgZurdcD+E8gAIW8P4AAGpnFqbsPytlsNyH8GR54aDkN8aG4hdLHiyEAzUW9QElnAZAg+0C4EIIAJooNvuIL4uZlimkwtCHYoIwJgUWC-MudQORiBCmQihO9qH7zbmQKAcpmHXWlrLcw3JgQhTqGdeeZh7QWSIUsBEpC9ZvRHCAiRYC95QP8HKfA7BtRD3DOpKWbtZYFFOKyABuZjzz2MMaF+IU6bGVsCocRIDJEWNodVUkTBcQQGwOQOqJBAyxPPpQBRCA8HMx5BYew8g3EqHnsKfBBRLQv1UMKQcRjMamNCeYiB0joEdy7kMO8+h-BO2wFAXAaS77GjdL8TC2hiLCg7EKdIrIzqlNzHCOQISTZhLqZYo+cSnaYFae0zp3SLD4LzOaBwoj5aw3wsYNIj9lDVDZHkmZlSpTVLmbUmhbdYC4Aqv4NySDNlKGkIiXItQQoZAUPPYi5RshsQ5HUcE4drneludHe59TAjPKYK81A-gmH2KQo412xFjRWHsBueQOQ1zzwTAsIhdRH4AMdMoWZsKqHhLbmAAAjr0Hu1ioC2M2eCZmUM76rBsPkuGEzZY2AKKI787iXoomMRQsxdKFkRKYIklZ1B0XX1Hr-SQ9RTgbhSFuA8RkmbS1bI-Y8lhglQoNjXGpcqHnQJJJtVAxJbj0rxFSVVI8pYOHwY4DkKQOS1HkLuNkphOSbjMLIdQx4aV1xghOQQzkQi9BGN0qK3x7qckwlkb8wg+qbmZp7XY4UQpYOjdvGCvMCYCwCMLUW+BxZam6dCcomttBwnHrsN0V0x5mgqDYZGdh-mSrspaze1qG7cCPsiu2iTsAkAAEbyPdWTV2N0BGFIOLUesNp+L8vtByPx6h5i4SHfrExkcx0EC1LIgmblpxpIKBCK0m4tw-C1R2TkkgrDWDvgibIVhxGeRmgfAIjT06n2SUMC+-g8AFVQAQCA3AwDSlwFOdUEgYDsEEKB7umBBAwdQPe0CzNNYwk1mxH4Ob+K2C5TsF9OtaicwtWemugHgPt1QKnMDZ9IOUGg7gWDBBSQkg4xIJg9VYMki6OhvwWGOOdzA3h-jBGl2YtQkjL4ACm0JnmBZTc89TLlDMDkHI5gwKpHNRjG5kdWOCyWSfbjA8+MCYQ9lZDqGkMYcEHZp2inYOEfmBUVsZgdIXUwvPGwzIJLKAyFyXMFTLPQus0B2zpJj49wg45-DgmSTCZJKJ8THGpOee87h-D-njRmlsFoJQIUEzzyWGDABvxVDHiBDmADyWAj0KQU5uDLmkN4Hc9JzD87YCCD4L55TV8PW1kRMKDC8s-gHDirw+EYNTJFvxZrLcHW2PdcQb17LuX8vDAk0VmTo3xuTf85IDW5pTJxnHnUXBhEGhe0wqI+Ku3BbdYYYd-rbnUBoc85d-Q12VN+RanN8o+6Nzgo3LkXh0JSXwkdG-cK93vtdfgaiw7QmRNidO4V4bghQfg+m8utTxFNUngAQcS0mteGImZMuaGUIzqmix20-Acp-uIcB8Di7POwDk9JqpqH1PpBezqP8OmugqP7HtHYWw2Fvwv3i1KqpSW2OyN51l-HeXCfsDOyT3XIuysQ+aokObRTRB7jYlYOE26Sgr3tOabRuQWzhS52y2xfPXODaBx5mTvvNSi+HpTiXshZZaHdB1OWYWqMgtMMmAzN0fY+9xOyqq+ucsE4K5Jknofw8OMh9boFFQcxDKPcZRQ3j5uOEAsNH+sgufRJJNE3AsT4nkiSX3HjBMssA8D4LzD7fO-d4SSSQQGWL4l4xWXj+JGQ2+xFEoc079SgpHwfLGojeTgvzb1EmJcSp8Oag7n47RuTeefHyfnviSZ-94HvPtVUYyIrkzdyQljgQoFITNIBcmCuoKaIfkxjKgADJ4A3qoDTiXqajXrIp3qW43wfx9qyzGDgimjmDAi1AqxcrEQ-zZKAg2CQoJYjpGxQHZRIGYASCmy4AcAECEbiAsgAiOhmRsjy4lDZBsLmC6pDIJjaDiJUEwHTh0EMHsBMGhhi6L6lAOBfA1BkR07Vb-j8SyB7CnBeqPw5iFrCHQE0ESAAByKK6UqAeA7AsAuUEAEAgwiSAQLA5h96boxoPBw0oUHalG3B34zMygMuZgQIogFmmuVmNcIhBhxh-gph5hlhpAF8zsKB6q5o+C9QHI7ov4pwc8-ELWsswUnIZofahi5BzGm89BjBaSCgkgFgC8Bm34ywdecM6uGBxkoiyOmmGuw6xRRspRkhVA0hEe4uiQ+QCwgEFkKMACG4kU4Uiw7BGQvh9O4iDk16eIs6p8iS5AyK8BiBt68RFOAx2wp0jY9Qj8A46wm+pEREYEZ0hBfi4ivQXeKK-c8EIC+IuA8G-OI+SGnk3gDkggdxCGggjx7AzxXSCRUsZoK4zo36PwwoGgAq3Boiuc2YCOCgW4rY4iE4oQZuNBmx+AohOxMhVuH8OspgbEOYxkACNQVgfUVgucqgwIa4ZknIVyRRMqGJkGcieJEgNmnAdaQQdwXezqJI2UJIBAN4DBU0-g9qmJwuaSYEgE3Kx4PIoKFk9RjMLBdR2SipiU4BpibJWJ2xXJnWYscmUAeAVhNhUp7JvOJpeAsp4EtJZo4yxwygtoOc5cSwrIcuZBwRiWNcepwuBhYpfcmokA-g-pHJTBoJrspkLhjgqQaOpSXaGSeq34jg8wwoPw4i5AcoZAgQ9U8oja0ejgBQsePInCXBiiAcuwR6+Kdg2gJ60qpi3JzqKGqWU6hIM686JYw+KGQeJO-JR8rk7kHZosXZ5uSmhGbpACZQbodQ5gXaEWOK2guQ4kxkNgXOA5bZbyI5s6C68oBuJ2xuxOnmm58EQ5XmnZe5r+M2VO7s22D2K87uzu+x48kIZkmpKQloG5LqkpDqTq-JtqFhbxAevZaG-xAF9Sgg9qPQxIggEFUCJM-RshiIpkssuwxZPwZknau4ACiwFJMM-Kh6358qbc2GzSqywFA2oFSG4FLqsmnGOGqyhGcIucI0BwmsHCXa-i5QzOqO2Q+wZ0xFtqdsqWyyLSlFAuNF7k8FtCXmolJ8TFUZVOZ00gnE8wWyOwPCcMTeLiG4MIgi-hQl8KZFKyayOAnSElHxEgtFJFuA9F8mjFYO6yIJuxyFqZ3Koq4Ie+sIIyCJgE4KOQGg-Y1gRliy8lplbS5lrxPZQ2NltqclDsplpOUVk5XwDgxErYlQ0IrIHYrozIKQvwBwHBWgdQoVESTyLybyiCll1F1l0ldFFVTA55fAhGQIucwICU2sZQgKKl4IA6tRrIYiOp2ugsMljyiKU6DCNVsV9VtlAJiK55+gzFlRSwogMIx4rYxKqQXyMZbEp4bI7Rp6MqzZY10CTKLKp8oe01fZcVkF51PmoerVwxSgG436pwZogEHYbEaQyYvE8UQIO2w1LGnWLZwliq5Ud4WA11YFs18V4Nyq+JSFhJchKlJBoiYE+6fscMgyHsnC1G+wcY4iRACBuJ-gDksBmAkZrlyN1GDYdgboDg5oyg7o76OQiwh4gy2Qxm6MPpFB5CxNiB5NcBvR1NqBpQlQiJSpOQTo48rNkg1k+wOmoqkEQNm8AtpNQtlNVA1goto8SwLFXsmFNQ9Gct7NOwnNoqnIRNJNMAZNFNTBMgutUYbi9oWYog4IcUbIptCtFt3NZC-guAKKK08ovgcoJUGARUdU9hWUl8BJYtggPsGYG+tQgI-ScJH8xgeFB13I-SL8ZoSUZA2AXQwwYsbyhO2U0NSGhdxdIw55wdaScOssv8ygsJPsm+F0jYbI50uQSsQRHREg1dJddabyzk-eYssAJsldA9DBNdIubkAJJsDdKQTd2hSgeyxgeB-ExgDYhwoiiIxm8wh10qg9PJUAU65d+5eehuBeUmJ9c97k9dSlqCbNcYW4CgHFWFFkvCrIvagE36f850TQzQgdGA8AUQHRcdo8ggTuzMpoxtiIxx9Q88IaW4CYjgr8a2Qh4Bl4kDUYIgOYsDbiQxiDFZCAWgCwQ0qJPi1WzJvN0kvoKUaU802UYDpeyNIgj8hD8DLYIc76ABjoTIfqSwIUag40DDU0s0zDcAuDr4KMDosJHF9guYhy107a0xmaHBWQ7BZCMjqEXdpc1RtgtRSg+mLh4qr175Z0fdR1MKMacoujqCzNUupkj8wI8I9Qnh101QzIroYEWCm4yO3p-dTZINJl4Gz+F+SmDjt8EteCcI2BhcKjpQ9NqlXNy4tQLoXOJW5+vG+G0T105K6Q6DTeg6OVVGZkDY3ESsoibWm4XO+2vW+TAkPivay5GgXEuYnjpQrIH40MrReVPI9TOOf2eTSNYtcZxo1OGFlJkMSOWQwEZSr9wI+dqtRszZWJozbD4z4Muc2+qg22qOWiCJNgXs34RxYEmeNiOeUTYziRhEtQ48Kd4a4k3iuYZt+6donwtDwTI19hx+Xep+veOTg+NzWziRbNmlxwhwf9BSj636KJzOZEeh1B2xTTTJOKUIv+4UGORQW9yggBKdhB4ICYPNPzoR+hBp3RaL-qvaDgWL9NACtoUxxycYcsQWgEyLnJgdkmTsTTqMzIBzAlmE6lXT2Rmg0IsgX60ynL4RJhZhrqfLxS0gxtkMx4UMoI64DoOS+QJCJaqz5CVLtz5MSwRE36-hnBuL+EriiMoEVgvwcYuQCxSxnAPcaxyKTT4UPw3wGimgYKnIoIFJAi3jJLn8tQtx9xthTxJsLxaL0JsDxS4IawTaoIYoGBpqDzBFusLJupIQVpeJfL247NBQmVZSgkAb8zdoZosgGZ4gDZWufpub+pFNhpwG49IQ3ANhdwQppIfL3sRbWF4xSgZgGrXrf9naJCdLoj+rEg4ZvOBp3JxposppzEb+s2nwmSdQmQriHIlriA2R7pJEXp6JjbAZBpQZU0oZs7YAvbAcqsezCIOSKsQEpk6NOwT2e+WZOZ0CsA+ZaLxZsDqskMzohkcMSs3rmCDg-YnpxFk625l59jRrrsHtkIy4HCACjzu7CAEWDY5SzoGgogYB2bvzoN8K0FjqBMp1FhaLo0BC9gZgdgWmz5Ak5SJolJNWOq1jjZxHp17GDF5FJQYLUsNH4k4gYUTaXFpwdNJz8IQIIUFz07J1P5JWqy1H7otHonDH+lAEQEZmkasIDHZVpFcmTSEVzlqndNnu7F+ymicMPywkdHIU4qmEhn0CynZlHSK7N5qCwnln7jTJX9cMdrsDygHUbE68CnINPHjVU6iC1HELVemsuQcYKMgKdQUu1OponpxELnCKlVKKDCcXkgOwbakLdrXa8L0UUUzR+6YbEXbGPH91l1Weti5n6nVn-nX1NQwqJZZGO7ZkOX8NkNmAcXbC6Vm4mspwywmHCY21m6n46hWgpLNjkc6tttmtfLIxGB292Bj8ZkXTEr8tHNStft+rAdQdxU17iH-kxmFQc5joD2Lam+NQXwBVNgRwgh1oBdM9Q9Z9ZdMdTTjoiMlW61qQlW7daXkaHV+HJ0dbUoJ9pdKKo9zc49JsTTbzuYr6xypBs5Aby9-Y4Eqw3CLgLgQAA */ + /** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0ANhoBWAHQAOAMwB2KQEY5AFgCcGqWqkAaEAE9Ew0RLEqa64TIBMKmTUXCAvk71oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBEEpYSkJOUUaOWsxeylrWzk9QwQClQkrVOsZNWExFItnVxB3LDxCXwCAW1QAVyDA9hJ2MAjeGI4ueNBE5KkaCWspZfMM+XE1YsQZMQWxNQtxao0ZeRc3dDavTv9ybhG+djGoibjeWZSZCRUxJa1flTCNTWLYIYS2CT2RSKdRyGhZawWc4tS6eDp+fy+KBdMC4AIAeQAbmAAE6YEj6WDPZisSbcd5CYHCSFqOQ1NSKWRiMRA0HqSTKblSX48+pKZGtNHEXEjEm3Eg4kkkfzcQLBUJTanRWlvKIlQQ88piPSJGQpRSLeHKFRaRyciWo9oSbAQTBgAgAUTxpMCAGs-OQABZa15TBlJHIW9QyYSZTm2YHGgyIbJqdJSBQZhTCFSKHIOjxOl1uz3euWwf3sINURSRGmxMMJIRldPWKqKVnLOog5Ng2OmbnQnNiOTaDkFq5QZ2u90ex4k3oYEM6xszIQj6zpcHD+FqXOnUGCtI7UTaeYFLJiCdo6clgBKYEJqEwxOXDfpTaSI4tv3y8xtKgKDIh7CmkuxqDsEFSHY1TXkWM4EAAYtgmBuk89DjCuH5rl+bISHuyw0MYbLwgUh4qOY6Q5FkMh5masHNJK8ElnOLAkuhdbau+0z8OuqiQooI42OCNT2D2JSOPUlpaKOiI2Boihwfgt7usgJD+m+dI8bMI6SD8Nqsr8ojcsBvaCYCFSqFYRE-PIqhKVOxbugAIsEIxqm6GrhBhLxYdpfFfEo8JyMYtGchYh5qFJKjWBBIWIjQwIQQ5KkEAAKmAjyCOwqCCEQACCzmabqOEGvxCjaIoNQUTIMWHrVCxaIm9SGhRimMY6ylORIgYumAAAK5K4HABD5ex2AAGYkKE-hQEqTCBv4LBML05IjBAxWrrxSTGRUNpZIBFj5Nkh42ZCqQKDCuSxrYKXdbAgaoAA7oNZAjWNnBTTNc0kAt-hgF0TCcJAm3YdtBraOkI4xXIGRtnC4mIHmnIVBYUXAvDbJNBchbKd4lZBsQZCUJg+MBsGPn1lp4aZFkkK0eIOZiSkuhmTQ+TSPtPxNTkbYpWTVaBgQrloWAarYjKoP+Qg4ESFkWhHOCbbWLDoL8SsdTzKkshRXI-ME0LdwYP9EAcP4EC9CS7R+uTUs044zJWDy7MhWYo6grIFrQTQsUaEsgJ7vr5MSALQYAJIIWp-rizieL+MS43kCQmB25+0J5t8NnO6cwJZPVgly7FbYOFCChB4LIcGxHJZR2LQQS3Hj1WwAXvcyepzhmRRdINjWFVgJtrGh5918KvRkr0HqGo5dBpX5PV+6RDcLA7BKng-hN9grd4sn5vYCvxOjJTXHU2nl6LHUHJKNBShtqCBSsumgE7IzuxyFeHW41OoeBnPgsL8QZeq8SDrwTpwJOmA94HwoEfTioYwaJC7gsZQw4NAHE7PfQUixci5jioCbGKIv5-1nj-ABS9cArzXrgfw+UABC3h-AAA0O7bWhERCosY6JLGhCke+wpHYNBhCJGMUgZ6-1IQhchlCQHULoQwgAmiwxBmRmSAhHHCFI3IkwlF9gsfS2ZESxjNAQpieMDbEMDGQoBVD-BkCgG6JRSNHDlFEKoGMwJ5jZCKL2Ww7N8IZlvjsXBFExEWKsRQ4B683T4HYBTOBfl7ZSXqMCa0+RETLHvr8PR2huxmAKLRFQoSJElikZE6hTBSQVNwKbcgq0SBylNtAygjiECZHUFRIul5BQUXvhkVR6gqrVD7tBawRSq6SOsTIjeqAW5t1QvoWxOAoC4BaZkaCct8iJXcbkciKMfa7HyDYfkojP6TgsWEiZESbFgOwBAzACzk7YGWas2mFRDQBOMEsTYZlfhyAEgcU4SwaD0TGfPS50j16wFwL9fwOUmEvNqt8DMuY2TcJsKzCSe4vhRXUA4OS8NQX-3BWUwI0KmCwtQP4RRx94HS2hIi32TJ2ZAlZN8nRaNFiw1qOIGEwpp6nJvD-C5JTJnrzAAAR16LvaJUBYkvJhOkM0Zp5iDkAioQ8VhNyCSirVOoNQrD8pxmcoVxTF6ivKfU5OboU40oSWnXl2CfZthHByS+IF2k52ojaMwXJCXh2JTYkkANUDEluOa9gVJbXcRpkRNMshRA5isOYX4h5UXpEBIJXY9h9onKNYK8xpqCAPkEJlEIvQRirNEnLDIo56h1GRt4koWKL6pH2MsWQMVDWEONQW8ZJYjZi0gGbC2Vt8A20Fi84w6QiKqFhtVHkiMED-GkKICwySshoz9ZYy5YCKXx3qdgEgAAjBxUbT6d2UIFHFPIgraEbdsLuEgYSkR+CrWKW6AFBjsWLHKz4WkFBCvhRw2RbD7A0GyU6FFpD7FzG2DsawTGdW-uY-KT0QEBE3tvIYkDGlDBgf4PAE1UAEAgNwMAzpcBPn9BIGA7BBCYbmYIQjqB-15kkMIOEDh5haAzIoe+NgfybPUcYethSBVOiFah9D0zZk7xw-vPDlACO4CIwQUkJIZkSCYGtIjJIug0b8PRmZW9GPMdY22em8wOxAjEJyWq98lhfBCtoWqUUHDcj1uJsxwcpNmxuRAqBimxbMZI2RijVHyO0cEP55OTGVMsbPSVbaI8vgxhhJyGtzLtGIGGexiwNoOP2B5J5vNEmUNob86ScBu9cOH2U6p9TmntPDF0-pqLMXMBxaI6xxFPsIIcZFGydVPiYRXptO4-hshQm+YCHIph9XiOkeGuF1A1GovHtgIIPgXWEvxOjZ+EefztA8hUcC5+wg+FWBXbUWzwLSKjK88hnzFXZv0PmyFxrJItM6ZmW1wzG2ts7Z62kOoqgOR7ny44PheyAlZh9qNkr3b83Pek3N+RC3QvLbwBFgzdGAf6CB4lraiRDumFHDGGGTJ8j31qGmAo19bP-htIj0xT2K4zZoW99HH2SQaa+819grXceCHx4Tvb57ktVTSJGUDsYRI7N7PYOo6Qzy1BviFYw02Xu2PwG6DHS3yPY9W5F-7uuwBi8wvtnCI8wKFDsKeaC2XSjczeayHY79hIMVK959n2u7F6553z77LXfvC-9+bszROEE5aqmmYEc6cxwgUNye+uY0gjP-NBWSVUtfSZlbE-XYWjdrcM-nwMFvfJW8lzGN5q7ebmBu5klIKuci2fRiolnSHzkc7Lxjz7wfBeh6i2XivVMksk6qn8mw+r63xouz4sw5QajKF8WyZQwLc9mwqSSKpNS6kNIU3VkLBuVsl7o9v3ftz9+CFqzA0fJ9x85fZuUTkHZ5LZBHFYXpKsn3BQcNZ4ETfAIC-XEPfckA-JpYLeLNTXnJrH7PTYXEA6pK-cAm-Q-O-SPcXR-UoZ-J9JQCCWKD-EKUyHRDjNMWmOEaEWnB7b3NnWeAAGTwB-VQGfAIC-XwGYL-Sj2lmqHmAqFODfnEBwUBA9j7khESnB0qnBB9lCUYOGgpWfAkDDlwA4AIH-XEDSCIj7jbAolzAVxKHqE3DV0vSdgyxoKRzK2DjkM4MwCUJUPYDUNrEtwlxJyImZA0I5B+Cqn1QxSMC0C3GBUHhhjslkKYIUNsIADlKV+pUA8AI1RoIAIBBh6lgDYi8R-1AQl8DgfYGZbRahQRRRFgKI1ctk9wy5HtzlrDwiJAoj-AYi4jYAiYYEbUsDickYOMFhW8NB8tAJfg+NexRBR4chKdMgchxQKihVlDVCWlZA-k4QIIqDBwfZ58JIqoBQ1cE1P8xRQkpiHCaxWjo8l08JoQvDYpr49wVinEIJTArpPFeDlhzDWdzk0pv08RD1IF6lyAKU2DAxv1wiWlao+CYJUgOMZJhRQR61vhs0UhwRExoJQlehqlKUD52If58RcBMdDdKNjcJB8pvA0pBBETSNBAUT2A0SVluDwxxtFVfhe5klqgqoPYlB0hRwN0c4s9EMiEhUHxQhw9wifi-jf0WjnDsClh+JbBQNshr56VQQMZM58kFBdFOSe1g4eS8N7EbDcSXtrYgg7hqkw0SRhoSQkI8B95Fog1eSzcATrN00aIKJVBBQ1Zlc1kdhYpp83dQk1S+ShStT0NrZjMoA8AEikiLT1S9cAy8BrT35OZxB347BZBaIISfZoNRJhRtCUhHiu9uSQgwzNTkIVCHpIB-AvSzc1DKS041VIQuRjBAQMhNEPYeRp1qg4YVZoIOxQlyA3QyBAg1p3RVlqhJB8hXULp+5UhToFU2RhCzQpd9ggCw1KMqs91CQD1j0SwT9i8Tc6M9SwFspcplyrZVyI94t1Dah6Zcx4d7AcVfCEBaJER8IxiKIst2yJjytpNtzFy4V9zD0T13R+8Bchcot3z2JdzosVyfz79aVww+5oRp08w0FOMitQRhIfweRXSIJaIgQxNaDu9tc9SIVqEg0ehQ08Kyl4j1zsTqNiSSKqFBBCKQ1zdqKZEOIRS2icDu5Yw4RzBgMdh6hU0LN1BAQlBAIsVUg5zGL14GM5N7lMTT9yMqLzUjNZNsN7lzNNwhMOMPclcnc8xgUoTTh4xUwcxMyuTXyzZxLqEOtpLyKcd5KrkZFosqtbkrV9Aes9hBJhRYYOKxyzIoQn0+jeC+5cgbAxLzUZMTMpKHklkMTrKcTbL8LFLwrlKCdHlnlyzrd6VFUOQc5ch3ldkFhCsMw+4B5LwQq7LQFHK7lIqnloqi8KK5LcpzKHLE5nKRcoqTz2NYZYYBzxBaoSCkZdh3DExNAKJlgzRSr8LSUYU4VGEZKNyJA4qykSSyUQK+BzMvZ9gBMMyDVIoa9hRHBzwYxO8TKUczLQqoUprKV5FZq6r5qGqFLzqmAQKXK0rJcf8bNhLBIBCndEQlg5ZbNfksh+txBxqSUJUpVIEy9rqbK7qyrcAS1JVYsy9WMqon1xs21AIuVhsJIOLpBRxPh04qgQabEmBLVUIsAobYqYb4qSalQybhTK8XCY97An17AahEpGYTozIdZ8IRMLw4Kqp2psKhUiBfiOD-A0oWDMAyyDieD-rf9Epyc4MVZZT1kskDgQoOxjhlTkcK4Ra-iJbWD9iWLDi2xlge5W8+sBN70EBtAjRER1bYxgR9VQk9axaDapaqBrAZaoKnaiigr-82xTgsbEBbbTB7ashHbjhBaLCfdZ5XaYBxbJa1CZBvbPxapbN0xASahUwMwVa7b2aNanbagXbRaE73a1CVBU7rd+FC59xEQTIKJLjWkLBNw1aI7NbnaXzg4iBsASROybCBSxahSAS31pBTblAQpBLra5TzB6dch4QPMsKY66Df4e6+69cfSZt-SrZAyMT8pEjbhe7+6VQd7IyXrEhzBZBvhYYWYRMRxryYNM5HzrpoIgQXaj6N7JbfTJgx07wSBTZ+gTSCzFpyAP7YFjbpYG8LRL1X8HZ761ZkztAchWbUgA537168zTTCyki17+7paIGaYRQ-LWRHBEROR7SPZH4YwF7GdbMcwl6nif5-BcBKUvp3RfBPIAgMAppVpgChpwGGbsDBAOxJBuQjFag1Eir74p14YeLuRtCsYUoyBsAuhhhrY4VmthoKbqNlHVGRgQK2GWkCsgNEplANBcF9CcsOMtw2Q4ZcgVEGGu9dG1Gx0NH+GYCg9-zQ9nH9GcpBBDHz6kYcgL4Mb5h6Iyim7s7+CKI+4kG4ZtanQfH1HKVMoFMdSDZtHyMfHzc-GKxbZAmEBjGOREp6g4weQgQm7Pk5Zk0dLeYVUXBmgWGMB4AohWcCHPxBA4RJAtkahNl2beLexhGjCzARN35Y1AIrA7oZx2nSpQc5Z0KkHdwNDKGFhLoPMsYUFo6njupeoMBXphoWmx9WKRBEp5n2SEoDgBnViOxcaqcgQ0Uu1tmZwJAHpnp9m4AZnwZXT0xzHgVZBAa2UkZ9lFglBdhCD5ZsgxFPnEgKpp0FiRHbs75ex+IzBcheZhkcxxwu6K5TVoWgmIQ+iXYk1n5+iJISncbGhaSu5c1l6cLpNJLsNAsj94s8XWlUg0waz6g4RtwcwHM9xq1bIjIRMEnY7f4OcOsmX8NmNWX19Ut41hlzQb5MEM6PdcwE150jqVTfdUc3tGEFsZWMgl8Z07IqCLi+FTg8DRBDF+5cg5y0d9XBHWLlAUb35EQuKzAtlrbYop0cVs9ah8kP4hbTKAg+TpXHXDjnXmRhQYxbNVARk+rndMgBxLyfZxAuM5ze8w2jmI2MgLRwQa01WmUE3kHbc8wCgVZW0tmszg2lpKlQCUD6lJWlMs2H8nXQTC5-8BtvUvXUgvhijPX7HLnQj5ChSZXgRyhvWTh1dYxryMb01+QYptZ4ph3NTdix3shFhacoJmcZ2Cjrj-gXVqh6H7RsWGCwifSWG9Nk5WWGS0hOM8ExJBJKndKCg8lRxRjSmV3qjaj6i8RDnW2Tax5TAZ1DR7HvWIScw5ZMhaob7dg7Adj7DWXCJrtAjlV-Ym61k1KuZklEw+ZT3f4XiOC3jd5PiKVWXXM0hBkNKE1AiCibBvhLBDXycsgESkTkjUSDZ0SZW29q1cgDgs8skE2UVqmgqVVvZkp8OJASyNTwib2uR8J9gV8zQnaIpexBLIQ+Ox5Z0AJPSczvSv6t6x1dTuAki7hDTSQ5Or729shlPqhVOShOKOFWRYm2w0EZBdPLSZPN7tSx0IyeJIKDsORJAbOzRycEzSWjBkzo2oJ-KMyPPczqj8yzSizpO3Qb3+P+DWR9I+5E01YNA3lbA7A0ZtxA3aWhV+7uzYBeyZWiqtxjAhCFjhRg6bbld2X35aggRn9NWdbZ4OcgKAhPywK0vw26V22fZzARMhKRqkKy2NlaIMwzAoQQlJO+vQq6LiLw1-2AvO5jABQOwCCroxHtLDWqI8xPKZ2MgiapkGXnKZXdun19uBMfgjvDwOw8221RilV4wrvyrmr5k7vnTHuiqtE6hh58vFuwmejyGfvqEbv5lFlqqAe9uNBqPYS+56pIZsjqHovxuYf44KrnKEflkkeHuUfzui4IvWkBL-lYylYHBIWVvcKzqyU91GE7uFBC5lU8xDowXIpgn5Z9q9w7Aoo8eHq915F2fND7jueumfZ747s5ZYwan6IptGe3zQqwbpVcRZVAwSfTXUeKfDwwNiGcO4MrW8eaarUsB2fmRDrahrQFBgUE34xKOCD6g7JlgS79bJab3dh8qBCN0G6RC2YoM27C6tb0Hj7R2Rvww7JJB07GY4GFA+R9h8IW6NKBC7ORWV6JBcHP7FDDOoAT6nk8ByPDlIQfhE-dvk-exH7zBn6RE0HJO8-NTC-iz-7sB+gy-qgK-dgTtq-rypS0+G-UG37m+wHEusHAwiyW-u-UtK-+-QdrzPgn7k1G+QSxFmHWHpowBWXBBeYOEfVVALxRIkL929V67Ym-ZSuniknXHKVNHd+Y-PwMwKgj2iqREYxqhpG9IIJx2UfoY2fCQHfyL5wpUmK8dJuTFZajgVcz3YwLIH9hQ4BizePmtUFWD0oGmTgIAA */ id: 'Modeling', context: ({ input }) => ({