Merge branch 'main' into franknoirot/4088/create-file-url

This commit is contained in:
Frank Noirot
2025-01-17 18:41:44 -05:00
346 changed files with 47602 additions and 2783 deletions

View File

@ -1,3 +1,3 @@
[codespell] [codespell]
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall,ser
skip: **/target,node_modules,build,dist,./out,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./packages/codemirror-lang-kcl/test/all.test.ts,tsconfig.tsbuildinfo skip: **/target,node_modules,build,dist,./out,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./packages/codemirror-lang-kcl/test/all.test.ts,tsconfig.tsbuildinfo

View File

@ -1,44 +0,0 @@
on:
push:
branches:
- main
paths:
- '**.rs'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- .github/workflows/cargo-bench.yml
pull_request:
paths:
- '**.rs'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- .github/workflows/cargo-bench.yml
workflow_dispatch:
permissions: read-all
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
name: cargo bench
jobs:
cargo-bench:
name: Benchmark with iai
runs-on: ubuntu-latest-8-cores
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Install dependencies
run: |
cargo install cargo-criterion
sudo apt update
sudo apt install -y valgrind
- name: Rust Cache
uses: Swatinem/rust-cache@v2.6.1
- name: Benchmark kcl library
shell: bash
run: |-
cd src/wasm-lib/kcl; cargo bench --all-features -- iai
env:
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}

File diff suppressed because it is too large Load Diff

View File

@ -280,7 +280,7 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('router-template-slate')).toBeVisible() await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible() await expect(page.getByText('Create project')).toBeVisible()
}) })
await test.step('Opening the router-template project should load', async () => { await test.step('Opening the router-template project should load', async () => {

View File

@ -136,6 +136,22 @@ export class CmdBarFixture {
} }
} }
get cmdSearchInput() {
return this.page.getByTestId('cmd-bar-search')
}
get argumentInput() {
return this.page.getByTestId('cmd-bar-arg-value')
}
get cmdOptions() {
return this.page.getByTestId('cmd-bar-option')
}
chooseCommand = async (commandName: string) => {
await this.cmdOptions.getByText(commandName).click()
}
/** /**
* Select an option from the command bar * Select an option from the command bar
*/ */

View File

@ -103,7 +103,7 @@ export class HomePageFixture {
.toEqual(expectedState) .toEqual(expectedState)
} }
createAndGoToProject = async (projectTitle: string) => { createAndGoToProject = async (projectTitle = 'project-$nnn') => {
await expect(this.projectSection).not.toHaveText('Loading your Projects...') await expect(this.projectSection).not.toHaveText('Loading your Projects...')
await this.projectButtonNew.click() await this.projectButtonNew.click()
await this.projectTextName.click() await this.projectTextName.click()

View File

@ -63,6 +63,10 @@ export class ToolbarFixture {
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done') this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
} }
get logoLink() {
return this.page.getByTestId('app-logo')
}
startSketchPlaneSelection = async () => startSketchPlaneSelection = async () =>
doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500) doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500)

View File

@ -172,7 +172,7 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('broken-code')).toBeVisible() await expect(page.getByText('broken-code')).toBeVisible()
await expect(page.getByText('bracket')).toBeVisible() await expect(page.getByText('bracket')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible() await expect(page.getByText('Create project')).toBeVisible()
}) })
await test.step('opening broken code project should clear the scene and show the error', async () => { await test.step('opening broken code project should clear the scene and show the error', async () => {
// Go back home. // Go back home.
@ -253,7 +253,7 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('empty')).toBeVisible() await expect(page.getByText('empty')).toBeVisible()
await expect(page.getByText('bracket')).toBeVisible() await expect(page.getByText('bracket')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible() await expect(page.getByText('Create project')).toBeVisible()
}) })
await test.step('opening empty code project should clear the scene', async () => { await test.step('opening empty code project should clear the scene', async () => {
// Go back home. // Go back home.
@ -985,6 +985,126 @@ test.describe(`Project management commands`, () => {
}) })
} }
) )
test(`Create a new project with a colliding name`, async ({
context,
homePage,
toolbar,
cmdBar,
}) => {
const projectName = 'test-project'
await test.step(`Setup`, async () => {
await context.folderSetupFn(async (dir) => {
const projectDir = path.join(dir, projectName)
await Promise.all([fsp.mkdir(projectDir, { recursive: true })])
await Promise.all([
fsp.copyFile(
executorInputPath('router-template-slate.kcl'),
path.join(projectDir, 'main.kcl')
),
])
})
await homePage.expectState({
projectCards: [
{
title: projectName,
fileCount: 1,
},
],
sortBy: 'last-modified-desc',
})
})
await test.step('Create a new project with the same name', async () => {
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('create project')
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Create project',
currentArgKey: 'name',
currentArgValue: '',
headerArguments: {
Name: '',
},
highlightedHeaderArg: 'name',
})
await cmdBar.argumentInput.fill(projectName)
await cmdBar.progressCmdBar()
})
await test.step(`Check the project was created with a non-colliding name`, async () => {
await toolbar.logoLink.click()
await homePage.expectState({
projectCards: [
{
title: projectName + '-1',
fileCount: 1,
},
{
title: projectName,
fileCount: 1,
},
],
sortBy: 'last-modified-desc',
})
})
await test.step('Create another project with the same name', async () => {
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('create project')
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Create project',
currentArgKey: 'name',
currentArgValue: '',
headerArguments: {
Name: '',
},
highlightedHeaderArg: 'name',
})
await cmdBar.argumentInput.fill(projectName)
await cmdBar.progressCmdBar()
})
await test.step(`Check the second project was created with a non-colliding name`, async () => {
await toolbar.logoLink.click()
await homePage.expectState({
projectCards: [
{
title: projectName + '-2',
fileCount: 1,
},
{
title: projectName + '-1',
fileCount: 1,
},
{
title: projectName,
fileCount: 1,
},
],
sortBy: 'last-modified-desc',
})
})
})
})
test(`Create a few projects using the default project name`, async ({
homePage,
toolbar,
}) => {
for (let i = 0; i < 12; i++) {
await test.step(`Create project ${i}`, async () => {
await homePage.expectState({
projectCards: Array.from({ length: i }, (_, i) => ({
title: `project-${i.toString().padStart(3, '0')}`,
fileCount: 1,
})).toReversed(),
sortBy: 'last-modified-desc',
})
await homePage.createAndGoToProject()
await toolbar.logoLink.click()
})
}
}) })
test( test(
@ -1391,7 +1511,7 @@ extrude001 = extrude(200, sketch001)`)
await page.getByTestId('app-logo').click() await page.getByTestId('app-logo').click()
await expect( await expect(
page.getByRole('button', { name: 'New project' }) page.getByRole('button', { name: 'Create project' })
).toBeVisible() ).toBeVisible()
for (let i = 1; i <= 10; i++) { for (let i = 1; i <= 10; i++) {
@ -1465,7 +1585,7 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('router-template-slate')).toBeVisible() await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible() await expect(page.getByText('Create project')).toBeVisible()
}) })
await test.step('Opening the router-template project should load the stream', async () => { await test.step('Opening the router-template project should load the stream', async () => {
@ -1494,7 +1614,7 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('router-template-slate')).toBeVisible() await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible() await expect(page.getByText('Create project')).toBeVisible()
}) })
} }
) )

View File

@ -1078,7 +1078,7 @@ export async function createProject({
returnHome?: boolean returnHome?: boolean
}) { }) {
await test.step(`Create project and navigate to it`, async () => { await test.step(`Create project and navigate to it`, async () => {
await page.getByRole('button', { name: 'New project' }).click() await page.getByRole('button', { name: 'Create project' }).click()
await page.getByRole('textbox', { name: 'Name' }).fill(name) await page.getByRole('textbox', { name: 'Name' }).fill(name)
await page.getByRole('button', { name: 'Continue' }).click() await page.getByRole('button', { name: 'Continue' }).click()

View File

@ -154,7 +154,6 @@
"@playwright/test": "^1.49.0", "@playwright/test": "^1.49.0",
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^15.0.2", "@testing-library/react": "^15.0.2",
"@types/d3-force": "^3.0.10",
"@types/diff": "^6.0.0", "@types/diff": "^6.0.0",
"@types/electron": "^1.6.10", "@types/electron": "^1.6.10",
"@types/isomorphic-fetch": "^0.0.39", "@types/isomorphic-fetch": "^0.0.39",
@ -175,7 +174,6 @@
"@vitest/web-worker": "^1.5.0", "@vitest/web-worker": "^1.5.0",
"@xstate/cli": "^0.5.17", "@xstate/cli": "^0.5.17",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"d3-force": "^3.0.0",
"electron": "32.1.2", "electron": "32.1.2",
"electron-builder": "24.13.3", "electron-builder": "24.13.3",
"electron-notarize": "1.2.2", "electron-notarize": "1.2.2",

View File

@ -25,13 +25,13 @@ import {
CallExpression, CallExpression,
PathToNode, PathToNode,
Program, Program,
SourceRange,
Expr, Expr,
parse, parse,
recast, recast,
defaultSourceRange, defaultSourceRange,
resultIsOk, resultIsOk,
ProgramMemory, ProgramMemory,
topLevelRange,
} from 'lang/wasm' } from 'lang/wasm'
import { CustomIcon, CustomIconName } from 'components/CustomIcon' import { CustomIcon, CustomIconName } from 'components/CustomIcon'
import { ConstrainInfo } from 'lang/std/stdTypes' import { ConstrainInfo } from 'lang/std/stdTypes'
@ -600,8 +600,8 @@ const ConstraintSymbol = ({
if (err(_node)) return if (err(_node)) return
const node = _node.node const node = _node.node
const range: SourceRange = node const range = node
? [node.start, node.end, true] ? topLevelRange(node.start, node.end)
: defaultSourceRange() : defaultSourceRange()
if (_type === 'intersectionTag') return null if (_type === 'intersectionTag') return null

View File

@ -59,6 +59,7 @@ import {
sourceRangeFromRust, sourceRangeFromRust,
resultIsOk, resultIsOk,
SourceRange, SourceRange,
topLevelRange,
} from 'lang/wasm' } from 'lang/wasm'
import { calculate_circle_from_3_points } from '../wasm-lib/pkg/wasm_lib' import { calculate_circle_from_3_points } from '../wasm-lib/pkg/wasm_lib'
import { import {
@ -628,7 +629,7 @@ export class SceneEntities {
const startRange = _node1.node.start const startRange = _node1.node.start
const endRange = _node1.node.end const endRange = _node1.node.end
const sourceRange: SourceRange = [startRange, endRange, true] const sourceRange = topLevelRange(startRange, endRange)
const selection: Selections = computeSelectionFromSourceRangeAndAST( const selection: Selections = computeSelectionFromSourceRangeAndAST(
sourceRange, sourceRange,
maybeModdedAst maybeModdedAst
@ -2012,7 +2013,7 @@ export class SceneEntities {
kclManager.programMemory, kclManager.programMemory,
{ {
type: 'sourceRange', type: 'sourceRange',
sourceRange: [node.start, node.end, true], sourceRange: topLevelRange(node.start, node.end),
}, },
getChangeSketchInput() getChangeSketchInput()
) )
@ -2263,7 +2264,7 @@ export class SceneEntities {
) )
if (trap(_node, { suppress: true })) return if (trap(_node, { suppress: true })) return
const node = _node.node const node = _node.node
editorManager.setHighlightRange([[node.start, node.end, true]]) editorManager.setHighlightRange([topLevelRange(node.start, node.end)])
const yellow = 0xffff00 const yellow = 0xffff00
colorSegment(selected, yellow) colorSegment(selected, yellow)
const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE) const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE)

View File

@ -5,7 +5,7 @@ import { useEffect, useRef, useState } from 'react'
import { trap } from 'lib/trap' import { trap } from 'lib/trap'
import { codeToIdSelections } from 'lib/selections' import { codeToIdSelections } from 'lib/selections'
import { codeRefFromRange } from 'lang/std/artifactGraph' import { codeRefFromRange } from 'lang/std/artifactGraph'
import { defaultSourceRange } from 'lang/wasm' import { defaultSourceRange, SourceRange, topLevelRange } from 'lang/wasm'
export function AstExplorer() { export function AstExplorer() {
const { context } = useModelingContext() const { context } = useModelingContext()
@ -118,19 +118,19 @@ function DisplayObj({
hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : '' hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : ''
}`} }`}
onMouseEnter={(e) => { onMouseEnter={(e) => {
editorManager.setHighlightRange([[obj?.start || 0, obj.end, true]]) editorManager.setHighlightRange([
topLevelRange(obj?.start || 0, obj.end),
])
e.stopPropagation() e.stopPropagation()
}} }}
onMouseMove={(e) => { onMouseMove={(e) => {
e.stopPropagation() e.stopPropagation()
editorManager.setHighlightRange([[obj?.start || 0, obj.end, true]]) editorManager.setHighlightRange([
topLevelRange(obj?.start || 0, obj.end),
])
}} }}
onClick={(e) => { onClick={(e) => {
const range: [number, number, boolean] = [ const range = topLevelRange(obj?.start || 0, obj.end || 0)
obj?.start || 0,
obj.end || 0,
true,
]
const idInfo = codeToIdSelections([ const idInfo = codeToIdSelections([
{ codeRef: codeRefFromRange(range, kclManager.ast) }, { codeRef: codeRefFromRange(range, kclManager.ast) },
])[0] ])[0]

View File

@ -17,7 +17,7 @@ import { StateFrom } from 'xstate'
const semanticEntityNames: { const semanticEntityNames: {
[key: string]: Array<Artifact['type'] | 'defaultPlane'> [key: string]: Array<Artifact['type'] | 'defaultPlane'>
} = { } = {
face: ['wall', 'cap', 'solid2D'], face: ['wall', 'cap', 'solid2d'],
edge: ['segment', 'sweepEdge', 'edgeCutEdge'], edge: ['segment', 'sweepEdge', 'edgeCutEdge'],
point: [], point: [],
plane: ['defaultPlane'], plane: ['defaultPlane'],

View File

@ -52,6 +52,7 @@ function CommandComboBox({
className="w-5 h-5 bg-primary/10 dark:bg-primary text-primary dark:text-inherit" className="w-5 h-5 bg-primary/10 dark:bg-primary text-primary dark:text-inherit"
/> />
<Combobox.Input <Combobox.Input
data-testid="cmd-bar-search"
onChange={(event) => setQuery(event.target.value)} onChange={(event) => setQuery(event.target.value)}
className="w-full bg-transparent focus:outline-none selection:bg-primary/20 dark:selection:bg-primary/40 dark:focus:outline-none" className="w-full bg-transparent focus:outline-none selection:bg-primary/20 dark:selection:bg-primary/40 dark:focus:outline-none"
onKeyDown={(event) => { onKeyDown={(event) => {
@ -85,6 +86,7 @@ function CommandComboBox({
value={option} value={option}
className="flex items-center gap-4 px-4 py-1.5 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90 ui-disabled:!text-chalkboard-50" className="flex items-center gap-4 px-4 py-1.5 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90 ui-disabled:!text-chalkboard-50"
disabled={optionIsDisabled(option)} disabled={optionIsDisabled(option)}
data-testid={`cmd-bar-option`}
> >
{'icon' in option && option.icon && ( {'icon' in option && option.icon && (
<CustomIcon name={option.icon} className="w-5 h-5" /> <CustomIcon name={option.icon} className="w-5 h-5" />

View File

@ -1,10 +1,7 @@
import { useMemo } from 'react' import { useMemo } from 'react'
import { engineCommandManager } from 'lib/singletons' import { engineCommandManager } from 'lib/singletons'
import { import { expandPlane, PlaneArtifactRich } from 'lang/std/artifactGraph'
ArtifactGraph, import { ArtifactGraph } from 'lang/wasm'
expandPlane,
PlaneArtifactRich,
} from 'lang/std/artifactGraph'
import { DebugDisplayArray, GenericObj } from './DebugDisplayObj' import { DebugDisplayArray, GenericObj } from './DebugDisplayObj'
export function DebugFeatureTree() { export function DebugFeatureTree() {

View File

@ -18,6 +18,7 @@ import {
getNextProjectIndex, getNextProjectIndex,
interpolateProjectNameWithIndex, interpolateProjectNameWithIndex,
doesProjectNameNeedInterpolated, doesProjectNameNeedInterpolated,
getUniqueProjectName,
getNextFileName, getNextFileName,
} from 'lib/desktopFS' } from 'lib/desktopFS'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
@ -337,16 +338,12 @@ const ProjectsContextDesktop = ({
: settings.projects.defaultProjectName.current : settings.projects.defaultProjectName.current
).trim() ).trim()
if (doesProjectNameNeedInterpolated(name)) { const uniqueName = getUniqueProjectName(name, input.projects)
const nextIndex = getNextProjectIndex(name, input.projects) await createNewProjectDirectory(uniqueName)
name = interpolateProjectNameWithIndex(name, nextIndex)
}
await createNewProjectDirectory(name)
return { return {
message: `Successfully created "${name}"`, message: `Successfully created "${uniqueName}"`,
name, name: uniqueName,
} }
}), }),
renameProject: fromPromise(async ({ input }) => { renameProject: fromPromise(async ({ input }) => {

View File

@ -301,7 +301,7 @@ export const Stream = () => {
return return
} }
const path = getArtifactOfTypes( const path = getArtifactOfTypes(
{ key: entity_id, types: ['path', 'solid2D', 'segment'] }, { key: entity_id, types: ['path', 'solid2d', 'segment'] },
engineCommandManager.artifactGraph engineCommandManager.artifactGraph
) )
if (err(path)) { if (err(path)) {

View File

@ -1,6 +1,6 @@
import { toolTips } from 'lang/langHelpers' import { toolTips } from 'lang/langHelpers'
import { Selection, Selections } from 'lib/selections' import { Selection, Selections } from 'lib/selections'
import { PathToNode, Program, Expr } from '../../lang/wasm' import { PathToNode, Program, Expr, topLevelRange } from '../../lang/wasm'
import { getNodeFromPath } from '../../lang/queryAst' import { getNodeFromPath } from '../../lang/queryAst'
import { import {
PathToNodeMap, PathToNodeMap,
@ -41,7 +41,7 @@ export function removeConstrainingValuesInfo({
graphSelections: nodes.map( graphSelections: nodes.map(
(node): Selection => ({ (node): Selection => ({
codeRef: codeRefFromRange( codeRef: codeRefFromRange(
[node.start, node.end, true], topLevelRange(node.start, node.end),
kclManager.ast kclManager.ast
), ),
}) })

View File

@ -22,6 +22,7 @@ import {
ProgramMemory, ProgramMemory,
recast, recast,
SourceRange, SourceRange,
topLevelRange,
} from 'lang/wasm' } from 'lang/wasm'
import { getNodeFromPath } from './queryAst' import { getNodeFromPath } from './queryAst'
import { codeManager, editorManager, sceneInfra } from 'lib/singletons' import { codeManager, editorManager, sceneInfra } from 'lib/singletons'
@ -376,11 +377,7 @@ export class KclManager {
} }
this.ast = { ...ast } this.ast = { ...ast }
// updateArtifactGraph relies on updated executeState/programMemory // updateArtifactGraph relies on updated executeState/programMemory
await this.engineCommandManager.updateArtifactGraph( this.engineCommandManager.updateArtifactGraph(execState.artifactGraph)
this.ast,
execState.artifactCommands,
execState.artifacts
)
this._executeCallback() this._executeCallback()
if (!isInterrupted) { if (!isInterrupted) {
sceneInfra.modelingSend({ type: 'code edit during sketch' }) sceneInfra.modelingSend({ type: 'code edit during sketch' })
@ -473,7 +470,7 @@ export class KclManager {
...artifact, ...artifact,
codeRef: { codeRef: {
...artifact.codeRef, ...artifact.codeRef,
range: [node.start, node.end, true], range: topLevelRange(node.start, node.end),
}, },
}) })
} }
@ -594,7 +591,7 @@ export class KclManager {
if (start && end) { if (start && end) {
returnVal.graphSelections.push({ returnVal.graphSelections.push({
codeRef: { codeRef: {
range: [start, end, true], range: topLevelRange(start, end),
pathToNode: path, pathToNode: path,
}, },
}) })

View File

@ -1,4 +1,5 @@
import { kclErrorsToDiagnostics, KCLError } from './errors' import { kclErrorsToDiagnostics, KCLError } from './errors'
import { defaultArtifactGraph, topLevelRange } from 'lang/wasm'
describe('test kclErrToDiagnostic', () => { describe('test kclErrToDiagnostic', () => {
it('converts KCL errors to CodeMirror diagnostics', () => { it('converts KCL errors to CodeMirror diagnostics', () => {
@ -8,18 +9,20 @@ describe('test kclErrToDiagnostic', () => {
message: '', message: '',
kind: 'semantic', kind: 'semantic',
msg: 'Semantic error', msg: 'Semantic error',
sourceRange: [0, 1, true], sourceRange: topLevelRange(0, 1),
operations: [], operations: [],
artifactCommands: [], artifactCommands: [],
artifactGraph: defaultArtifactGraph(),
}, },
{ {
name: '', name: '',
message: '', message: '',
kind: 'type', kind: 'type',
msg: 'Type error', msg: 'Type error',
sourceRange: [4, 5, true], sourceRange: topLevelRange(4, 5),
operations: [], operations: [],
artifactCommands: [], artifactCommands: [],
artifactGraph: defaultArtifactGraph(),
}, },
] ]
const diagnostics = kclErrorsToDiagnostics(errors) const diagnostics = kclErrorsToDiagnostics(errors)

View File

@ -5,7 +5,13 @@ import { posToOffset } from '@kittycad/codemirror-lsp-client'
import { Diagnostic as LspDiagnostic } from 'vscode-languageserver-protocol' import { Diagnostic as LspDiagnostic } from 'vscode-languageserver-protocol'
import { Text } from '@codemirror/state' import { Text } from '@codemirror/state'
import { EditorView } from 'codemirror' import { EditorView } from 'codemirror'
import { ArtifactCommand, SourceRange } from 'lang/wasm' import {
ArtifactCommand,
ArtifactGraph,
defaultArtifactGraph,
isTopLevelModule,
SourceRange,
} from 'lang/wasm'
import { Operation } from 'wasm-lib/kcl/bindings/Operation' import { Operation } from 'wasm-lib/kcl/bindings/Operation'
type ExtractKind<T> = T extends { kind: infer K } ? K : never type ExtractKind<T> = T extends { kind: infer K } ? K : never
@ -15,13 +21,15 @@ export class KCLError extends Error {
msg: string msg: string
operations: Operation[] operations: Operation[]
artifactCommands: ArtifactCommand[] artifactCommands: ArtifactCommand[]
artifactGraph: ArtifactGraph
constructor( constructor(
kind: ExtractKind<RustKclError> | 'name', kind: ExtractKind<RustKclError> | 'name',
msg: string, msg: string,
sourceRange: SourceRange, sourceRange: SourceRange,
operations: Operation[], operations: Operation[],
artifactCommands: ArtifactCommand[] artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph
) { ) {
super() super()
this.kind = kind this.kind = kind
@ -29,6 +37,7 @@ export class KCLError extends Error {
this.sourceRange = sourceRange this.sourceRange = sourceRange
this.operations = operations this.operations = operations
this.artifactCommands = artifactCommands this.artifactCommands = artifactCommands
this.artifactGraph = artifactGraph
Object.setPrototypeOf(this, KCLError.prototype) Object.setPrototypeOf(this, KCLError.prototype)
} }
} }
@ -38,9 +47,17 @@ export class KCLLexicalError extends KCLError {
msg: string, msg: string,
sourceRange: SourceRange, sourceRange: SourceRange,
operations: Operation[], operations: Operation[],
artifactCommands: ArtifactCommand[] artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph
) { ) {
super('lexical', msg, sourceRange, operations, artifactCommands) super(
'lexical',
msg,
sourceRange,
operations,
artifactCommands,
artifactGraph
)
Object.setPrototypeOf(this, KCLSyntaxError.prototype) Object.setPrototypeOf(this, KCLSyntaxError.prototype)
} }
} }
@ -50,9 +67,17 @@ export class KCLInternalError extends KCLError {
msg: string, msg: string,
sourceRange: SourceRange, sourceRange: SourceRange,
operations: Operation[], operations: Operation[],
artifactCommands: ArtifactCommand[] artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph
) { ) {
super('internal', msg, sourceRange, operations, artifactCommands) super(
'internal',
msg,
sourceRange,
operations,
artifactCommands,
artifactGraph
)
Object.setPrototypeOf(this, KCLSyntaxError.prototype) Object.setPrototypeOf(this, KCLSyntaxError.prototype)
} }
} }
@ -62,9 +87,17 @@ export class KCLSyntaxError extends KCLError {
msg: string, msg: string,
sourceRange: SourceRange, sourceRange: SourceRange,
operations: Operation[], operations: Operation[],
artifactCommands: ArtifactCommand[] artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph
) { ) {
super('syntax', msg, sourceRange, operations, artifactCommands) super(
'syntax',
msg,
sourceRange,
operations,
artifactCommands,
artifactGraph
)
Object.setPrototypeOf(this, KCLSyntaxError.prototype) Object.setPrototypeOf(this, KCLSyntaxError.prototype)
} }
} }
@ -74,9 +107,17 @@ export class KCLSemanticError extends KCLError {
msg: string, msg: string,
sourceRange: SourceRange, sourceRange: SourceRange,
operations: Operation[], operations: Operation[],
artifactCommands: ArtifactCommand[] artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph
) { ) {
super('semantic', msg, sourceRange, operations, artifactCommands) super(
'semantic',
msg,
sourceRange,
operations,
artifactCommands,
artifactGraph
)
Object.setPrototypeOf(this, KCLSemanticError.prototype) Object.setPrototypeOf(this, KCLSemanticError.prototype)
} }
} }
@ -86,9 +127,10 @@ export class KCLTypeError extends KCLError {
msg: string, msg: string,
sourceRange: SourceRange, sourceRange: SourceRange,
operations: Operation[], operations: Operation[],
artifactCommands: ArtifactCommand[] artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph
) { ) {
super('type', msg, sourceRange, operations, artifactCommands) super('type', msg, sourceRange, operations, artifactCommands, artifactGraph)
Object.setPrototypeOf(this, KCLTypeError.prototype) Object.setPrototypeOf(this, KCLTypeError.prototype)
} }
} }
@ -98,9 +140,17 @@ export class KCLUnimplementedError extends KCLError {
msg: string, msg: string,
sourceRange: SourceRange, sourceRange: SourceRange,
operations: Operation[], operations: Operation[],
artifactCommands: ArtifactCommand[] artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph
) { ) {
super('unimplemented', msg, sourceRange, operations, artifactCommands) super(
'unimplemented',
msg,
sourceRange,
operations,
artifactCommands,
artifactGraph
)
Object.setPrototypeOf(this, KCLUnimplementedError.prototype) Object.setPrototypeOf(this, KCLUnimplementedError.prototype)
} }
} }
@ -110,9 +160,17 @@ export class KCLUnexpectedError extends KCLError {
msg: string, msg: string,
sourceRange: SourceRange, sourceRange: SourceRange,
operations: Operation[], operations: Operation[],
artifactCommands: ArtifactCommand[] artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph
) { ) {
super('unexpected', msg, sourceRange, operations, artifactCommands) super(
'unexpected',
msg,
sourceRange,
operations,
artifactCommands,
artifactGraph
)
Object.setPrototypeOf(this, KCLUnexpectedError.prototype) Object.setPrototypeOf(this, KCLUnexpectedError.prototype)
} }
} }
@ -122,14 +180,16 @@ export class KCLValueAlreadyDefined extends KCLError {
key: string, key: string,
sourceRange: SourceRange, sourceRange: SourceRange,
operations: Operation[], operations: Operation[],
artifactCommands: ArtifactCommand[] artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph
) { ) {
super( super(
'name', 'name',
`Key ${key} was already defined elsewhere`, `Key ${key} was already defined elsewhere`,
sourceRange, sourceRange,
operations, operations,
artifactCommands artifactCommands,
artifactGraph
) )
Object.setPrototypeOf(this, KCLValueAlreadyDefined.prototype) Object.setPrototypeOf(this, KCLValueAlreadyDefined.prototype)
} }
@ -140,14 +200,16 @@ export class KCLUndefinedValueError extends KCLError {
key: string, key: string,
sourceRange: SourceRange, sourceRange: SourceRange,
operations: Operation[], operations: Operation[],
artifactCommands: ArtifactCommand[] artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph
) { ) {
super( super(
'name', 'name',
`Key ${key} has not been defined`, `Key ${key} has not been defined`,
sourceRange, sourceRange,
operations, operations,
artifactCommands artifactCommands,
artifactGraph
) )
Object.setPrototypeOf(this, KCLUndefinedValueError.prototype) Object.setPrototypeOf(this, KCLUndefinedValueError.prototype)
} }
@ -167,9 +229,10 @@ export function lspDiagnosticsToKclErrors(
new KCLError( new KCLError(
'unexpected', 'unexpected',
message, message,
[posToOffset(doc, range.start)!, posToOffset(doc, range.end)!, true], [posToOffset(doc, range.start)!, posToOffset(doc, range.end)!, 0],
[], [],
[] [],
defaultArtifactGraph()
) )
) )
.sort((a, b) => { .sort((a, b) => {
@ -193,7 +256,7 @@ export function kclErrorsToDiagnostics(
errors: KCLError[] errors: KCLError[]
): CodeMirrorDiagnostic[] { ): CodeMirrorDiagnostic[] {
return errors return errors
?.filter((err) => err.sourceRange[2]) ?.filter((err) => isTopLevelModule(err.sourceRange))
.map((err) => { .map((err) => {
return { return {
from: err.sourceRange[0], from: err.sourceRange[0],
@ -208,7 +271,7 @@ export function complilationErrorsToDiagnostics(
errors: CompilationError[] errors: CompilationError[]
): CodeMirrorDiagnostic[] { ): CodeMirrorDiagnostic[] {
return errors return errors
?.filter((err) => err.sourceRange[2] === 0) ?.filter((err) => isTopLevelModule(err.sourceRange))
.map((err) => { .map((err) => {
let severity: any = 'error' let severity: any = 'error'
if (err.severity === 'Warning') { if (err.severity === 'Warning') {

View File

@ -6,6 +6,8 @@ import {
Sketch, Sketch,
initPromise, initPromise,
sketchFromKclValue, sketchFromKclValue,
defaultArtifactGraph,
topLevelRange,
} from './wasm' } from './wasm'
import { enginelessExecutor } from '../lib/testHelpers' import { enginelessExecutor } from '../lib/testHelpers'
import { KCLError } from './errors' import { KCLError } from './errors'
@ -480,9 +482,10 @@ const theExtrude = startSketchOn('XY')
new KCLError( new KCLError(
'undefined_value', 'undefined_value',
'memory item key `myVarZ` is not defined', 'memory item key `myVarZ` is not defined',
[129, 135, true], topLevelRange(129, 135),
[], [],
[] [],
defaultArtifactGraph()
) )
) )
}) })

View File

@ -1,5 +1,12 @@
import { getNodePathFromSourceRange, getNodeFromPath } from './queryAst' import { getNodePathFromSourceRange, getNodeFromPath } from './queryAst'
import { Identifier, assertParse, initPromise, Parameter } from './wasm' import {
Identifier,
assertParse,
initPromise,
Parameter,
SourceRange,
topLevelRange,
} from './wasm'
import { err } from 'lib/trap' import { err } from 'lib/trap'
beforeAll(async () => { beforeAll(async () => {
@ -17,11 +24,10 @@ const sk3 = startSketchAt([0, 0])
` `
const subStr = 'lineTo([3, 4], %, $yo)' const subStr = 'lineTo([3, 4], %, $yo)'
const lineToSubstringIndex = code.indexOf(subStr) const lineToSubstringIndex = code.indexOf(subStr)
const sourceRange: [number, number, boolean] = [ const sourceRange = topLevelRange(
lineToSubstringIndex, lineToSubstringIndex,
lineToSubstringIndex + subStr.length, lineToSubstringIndex + subStr.length
true, )
]
const ast = assertParse(code) const ast = assertParse(code)
const nodePath = getNodePathFromSourceRange(ast, sourceRange) const nodePath = getNodePathFromSourceRange(ast, sourceRange)
@ -29,7 +35,7 @@ const sk3 = startSketchAt([0, 0])
if (err(_node)) throw _node if (err(_node)) throw _node
const { node } = _node const { node } = _node
expect([node.start, node.end, true]).toEqual(sourceRange) expect(topLevelRange(node.start, node.end)).toEqual(sourceRange)
expect(node.type).toBe('CallExpression') expect(node.type).toBe('CallExpression')
}) })
it('gets path right for function definition params', () => { it('gets path right for function definition params', () => {
@ -45,11 +51,7 @@ const sk3 = startSketchAt([0, 0])
const b1 = cube([0,0], 10)` const b1 = cube([0,0], 10)`
const subStr = 'pos, scale' const subStr = 'pos, scale'
const subStrIndex = code.indexOf(subStr) const subStrIndex = code.indexOf(subStr)
const sourceRange: [number, number, boolean] = [ const sourceRange = topLevelRange(subStrIndex, subStrIndex + 'pos'.length)
subStrIndex,
subStrIndex + 'pos'.length,
true,
]
const ast = assertParse(code) const ast = assertParse(code)
const nodePath = getNodePathFromSourceRange(ast, sourceRange) const nodePath = getNodePathFromSourceRange(ast, sourceRange)
@ -81,11 +83,7 @@ const b1 = cube([0,0], 10)`
const b1 = cube([0,0], 10)` const b1 = cube([0,0], 10)`
const subStr = 'scale, 0' const subStr = 'scale, 0'
const subStrIndex = code.indexOf(subStr) const subStrIndex = code.indexOf(subStr)
const sourceRange: [number, number, boolean] = [ const sourceRange = topLevelRange(subStrIndex, subStrIndex + 'scale'.length)
subStrIndex,
subStrIndex + 'scale'.length,
true,
]
const ast = assertParse(code) const ast = assertParse(code)
const nodePath = getNodePathFromSourceRange(ast, sourceRange) const nodePath = getNodePathFromSourceRange(ast, sourceRange)

View File

@ -1,4 +1,11 @@
import { assertParse, recast, initPromise, Identifier } from './wasm' import {
assertParse,
recast,
initPromise,
Identifier,
SourceRange,
topLevelRange,
} from './wasm'
import { import {
createLiteral, createLiteral,
createIdentifier, createIdentifier,
@ -148,11 +155,7 @@ function giveSketchFnCallTagTestHelper(
// making it more of an integration test, but easier to read the test intention is the goal // making it more of an integration test, but easier to read the test intention is the goal
const ast = assertParse(code) const ast = assertParse(code)
const start = code.indexOf(searchStr) const start = code.indexOf(searchStr)
const range: [number, number, boolean] = [ const range = topLevelRange(start, start + searchStr.length)
start,
start + searchStr.length,
true,
]
const sketchRes = giveSketchFnCallTag(ast, range) const sketchRes = giveSketchFnCallTag(ast, range)
if (err(sketchRes)) throw sketchRes if (err(sketchRes)) throw sketchRes
const { modifiedAst, tag, isTagExisting } = sketchRes const { modifiedAst, tag, isTagExisting } = sketchRes
@ -230,7 +233,7 @@ yo2 = hmm([identifierGuy + 5])`
const { modifiedAst } = moveValueIntoNewVariable( const { modifiedAst } = moveValueIntoNewVariable(
ast, ast,
execState.memory, execState.memory,
[startIndex, startIndex, true], topLevelRange(startIndex, startIndex),
'newVar' 'newVar'
) )
const newCode = recast(modifiedAst) const newCode = recast(modifiedAst)
@ -244,7 +247,7 @@ yo2 = hmm([identifierGuy + 5])`
const { modifiedAst } = moveValueIntoNewVariable( const { modifiedAst } = moveValueIntoNewVariable(
ast, ast,
execState.memory, execState.memory,
[startIndex, startIndex, true], topLevelRange(startIndex, startIndex),
'newVar' 'newVar'
) )
const newCode = recast(modifiedAst) const newCode = recast(modifiedAst)
@ -258,7 +261,7 @@ yo2 = hmm([identifierGuy + 5])`
const { modifiedAst } = moveValueIntoNewVariable( const { modifiedAst } = moveValueIntoNewVariable(
ast, ast,
execState.memory, execState.memory,
[startIndex, startIndex, true], topLevelRange(startIndex, startIndex),
'newVar' 'newVar'
) )
const newCode = recast(modifiedAst) const newCode = recast(modifiedAst)
@ -272,7 +275,7 @@ yo2 = hmm([identifierGuy + 5])`
const { modifiedAst } = moveValueIntoNewVariable( const { modifiedAst } = moveValueIntoNewVariable(
ast, ast,
execState.memory, execState.memory,
[startIndex, startIndex, true], topLevelRange(startIndex, startIndex),
'newVar' 'newVar'
) )
const newCode = recast(modifiedAst) const newCode = recast(modifiedAst)
@ -286,7 +289,7 @@ yo2 = hmm([identifierGuy + 5])`
const { modifiedAst } = moveValueIntoNewVariable( const { modifiedAst } = moveValueIntoNewVariable(
ast, ast,
execState.memory, execState.memory,
[startIndex, startIndex, true], topLevelRange(startIndex, startIndex),
'newVar' 'newVar'
) )
const newCode = recast(modifiedAst) const newCode = recast(modifiedAst)
@ -306,18 +309,16 @@ describe('testing sketchOnExtrudedFace', () => {
const ast = assertParse(code) const ast = assertParse(code)
const segmentSnippet = `line([9.7, 9.19], %)` const segmentSnippet = `line([9.7, 9.19], %)`
const segmentRange: [number, number, boolean] = [ const segmentRange = topLevelRange(
code.indexOf(segmentSnippet), code.indexOf(segmentSnippet),
code.indexOf(segmentSnippet) + segmentSnippet.length, code.indexOf(segmentSnippet) + segmentSnippet.length
true, )
]
const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange) const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange)
const extrudeSnippet = `extrude(5 + 7, %)` const extrudeSnippet = `extrude(5 + 7, %)`
const extrudeRange: [number, number, boolean] = [ const extrudeRange = topLevelRange(
code.indexOf(extrudeSnippet), code.indexOf(extrudeSnippet),
code.indexOf(extrudeSnippet) + extrudeSnippet.length, code.indexOf(extrudeSnippet) + extrudeSnippet.length
true, )
]
const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange) const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange)
const extruded = sketchOnExtrudedFace( const extruded = sketchOnExtrudedFace(
@ -346,18 +347,16 @@ sketch001 = startSketchOn(part001, seg01)`)
|> extrude(5 + 7, %)` |> extrude(5 + 7, %)`
const ast = assertParse(code) const ast = assertParse(code)
const segmentSnippet = `close(%)` const segmentSnippet = `close(%)`
const segmentRange: [number, number, boolean] = [ const segmentRange = topLevelRange(
code.indexOf(segmentSnippet), code.indexOf(segmentSnippet),
code.indexOf(segmentSnippet) + segmentSnippet.length, code.indexOf(segmentSnippet) + segmentSnippet.length
true, )
]
const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange) const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange)
const extrudeSnippet = `extrude(5 + 7, %)` const extrudeSnippet = `extrude(5 + 7, %)`
const extrudeRange: [number, number, boolean] = [ const extrudeRange = topLevelRange(
code.indexOf(extrudeSnippet), code.indexOf(extrudeSnippet),
code.indexOf(extrudeSnippet) + extrudeSnippet.length, code.indexOf(extrudeSnippet) + extrudeSnippet.length
true, )
]
const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange) const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange)
const extruded = sketchOnExtrudedFace( const extruded = sketchOnExtrudedFace(
@ -386,18 +385,16 @@ sketch001 = startSketchOn(part001, seg01)`)
|> extrude(5 + 7, %)` |> extrude(5 + 7, %)`
const ast = assertParse(code) const ast = assertParse(code)
const sketchSnippet = `startProfileAt([3.58, 2.06], %)` const sketchSnippet = `startProfileAt([3.58, 2.06], %)`
const sketchRange: [number, number, boolean] = [ const sketchRange = topLevelRange(
code.indexOf(sketchSnippet), code.indexOf(sketchSnippet),
code.indexOf(sketchSnippet) + sketchSnippet.length, code.indexOf(sketchSnippet) + sketchSnippet.length
true, )
]
const sketchPathToNode = getNodePathFromSourceRange(ast, sketchRange) const sketchPathToNode = getNodePathFromSourceRange(ast, sketchRange)
const extrudeSnippet = `extrude(5 + 7, %)` const extrudeSnippet = `extrude(5 + 7, %)`
const extrudeRange: [number, number, boolean] = [ const extrudeRange = topLevelRange(
code.indexOf(extrudeSnippet), code.indexOf(extrudeSnippet),
code.indexOf(extrudeSnippet) + extrudeSnippet.length, code.indexOf(extrudeSnippet) + extrudeSnippet.length
true, )
]
const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange) const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange)
const extruded = sketchOnExtrudedFace( const extruded = sketchOnExtrudedFace(
@ -435,18 +432,16 @@ sketch001 = startSketchOn(part001, 'END')`)
part001 = extrude(5 + 7, sketch001)` part001 = extrude(5 + 7, sketch001)`
const ast = assertParse(code) const ast = assertParse(code)
const segmentSnippet = `line([4.99, -0.46], %)` const segmentSnippet = `line([4.99, -0.46], %)`
const segmentRange: [number, number, boolean] = [ const segmentRange = topLevelRange(
code.indexOf(segmentSnippet), code.indexOf(segmentSnippet),
code.indexOf(segmentSnippet) + segmentSnippet.length, code.indexOf(segmentSnippet) + segmentSnippet.length
true, )
]
const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange) const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange)
const extrudeSnippet = `extrude(5 + 7, sketch001)` const extrudeSnippet = `extrude(5 + 7, sketch001)`
const extrudeRange: [number, number, boolean] = [ const extrudeRange = topLevelRange(
code.indexOf(extrudeSnippet), code.indexOf(extrudeSnippet),
code.indexOf(extrudeSnippet) + extrudeSnippet.length, code.indexOf(extrudeSnippet) + extrudeSnippet.length
true, )
]
const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange) const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange)
const updatedAst = sketchOnExtrudedFace( const updatedAst = sketchOnExtrudedFace(
@ -471,11 +466,10 @@ describe('Testing deleteSegmentFromPipeExpression', () => {
const ast = assertParse(code) const ast = assertParse(code)
const execState = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const lineOfInterest = 'line([306.21, 198.85], %, $a)' const lineOfInterest = 'line([306.21, 198.85], %, $a)'
const range: [number, number, boolean] = [ const range = topLevelRange(
code.indexOf(lineOfInterest), code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length, code.indexOf(lineOfInterest) + lineOfInterest.length
true, )
]
const pathToNode = getNodePathFromSourceRange(ast, range) const pathToNode = getNodePathFromSourceRange(ast, range)
const modifiedAst = deleteSegmentFromPipeExpression( const modifiedAst = deleteSegmentFromPipeExpression(
[], [],
@ -549,11 +543,10 @@ ${!replace1 ? ` |> ${line}\n` : ''} |> angledLine([-65, ${
const ast = assertParse(code) const ast = assertParse(code)
const execState = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const lineOfInterest = line const lineOfInterest = line
const range: [number, number, boolean] = [ const range = topLevelRange(
code.indexOf(lineOfInterest), code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length, code.indexOf(lineOfInterest) + lineOfInterest.length
true, )
]
const pathToNode = getNodePathFromSourceRange(ast, range) const pathToNode = getNodePathFromSourceRange(ast, range)
const dependentSegments = findUsesOfTagInPipe(ast, pathToNode) const dependentSegments = findUsesOfTagInPipe(ast, pathToNode)
const modifiedAst = deleteSegmentFromPipeExpression( const modifiedAst = deleteSegmentFromPipeExpression(
@ -638,11 +631,10 @@ describe('Testing removeSingleConstraintInfo', () => {
const execState = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const lineOfInterest = expectedFinish.split('(')[0] + '(' const lineOfInterest = expectedFinish.split('(')[0] + '('
const range: [number, number, boolean] = [ const range = topLevelRange(
code.indexOf(lineOfInterest) + 1, code.indexOf(lineOfInterest) + 1,
code.indexOf(lineOfInterest) + lineOfInterest.length, code.indexOf(lineOfInterest) + lineOfInterest.length
true, )
]
const pathToNode = getNodePathFromSourceRange(ast, range) const pathToNode = getNodePathFromSourceRange(ast, range)
let argPosition: SimplifiedArgDetails let argPosition: SimplifiedArgDetails
if (key === 'arrayIndex' && typeof value === 'number') { if (key === 'arrayIndex' && typeof value === 'number') {
@ -692,11 +684,10 @@ describe('Testing removeSingleConstraintInfo', () => {
const execState = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
const lineOfInterest = expectedFinish.split('(')[0] + '(' const lineOfInterest = expectedFinish.split('(')[0] + '('
const range: [number, number, boolean] = [ const range = topLevelRange(
code.indexOf(lineOfInterest) + 1, code.indexOf(lineOfInterest) + 1,
code.indexOf(lineOfInterest) + lineOfInterest.length, code.indexOf(lineOfInterest) + lineOfInterest.length
true, )
]
let argPosition: SimplifiedArgDetails let argPosition: SimplifiedArgDetails
if (key === 'arrayIndex' && typeof value === 'number') { if (key === 'arrayIndex' && typeof value === 'number') {
argPosition = { argPosition = {
@ -889,11 +880,10 @@ sketch002 = startSketchOn({
const execState = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
// deleteFromSelection // deleteFromSelection
const range: [number, number, boolean] = [ const range = topLevelRange(
codeBefore.indexOf(lineOfInterest), codeBefore.indexOf(lineOfInterest),
codeBefore.indexOf(lineOfInterest) + lineOfInterest.length, codeBefore.indexOf(lineOfInterest) + lineOfInterest.length
true, )
]
const artifact = { type } as Artifact const artifact = { type } as Artifact
const newAst = await deleteFromSelection( const newAst = await deleteFromSelection(
ast, ast,

View File

@ -8,6 +8,8 @@ import {
makeDefaultPlanes, makeDefaultPlanes,
PipeExpression, PipeExpression,
VariableDeclarator, VariableDeclarator,
SourceRange,
topLevelRange,
} from '../wasm' } from '../wasm'
import { import {
EdgeTreatmentType, EdgeTreatmentType,
@ -77,11 +79,10 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
code: string, code: string,
expectedExtrudeSnippet: string expectedExtrudeSnippet: string
): CallExpression | PipeExpression | Error { ): CallExpression | PipeExpression | Error {
const extrudeRange: [number, number, boolean] = [ const extrudeRange = topLevelRange(
code.indexOf(expectedExtrudeSnippet), code.indexOf(expectedExtrudeSnippet),
code.indexOf(expectedExtrudeSnippet) + expectedExtrudeSnippet.length, code.indexOf(expectedExtrudeSnippet) + expectedExtrudeSnippet.length
true, )
]
const expectedExtrudePath = getNodePathFromSourceRange(ast, extrudeRange) const expectedExtrudePath = getNodePathFromSourceRange(ast, extrudeRange)
const expectedExtrudeNodeResult = getNodeFromPath< const expectedExtrudeNodeResult = getNodeFromPath<
VariableDeclarator | CallExpression VariableDeclarator | CallExpression
@ -112,11 +113,10 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
const ast = assertParse(code) const ast = assertParse(code)
// selection // selection
const segmentRange: [number, number, boolean] = [ const segmentRange = topLevelRange(
code.indexOf(selectedSegmentSnippet), code.indexOf(selectedSegmentSnippet),
code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length, code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length
true, )
]
const selection: Selection = { const selection: Selection = {
codeRef: codeRefFromRange(segmentRange, ast), codeRef: codeRefFromRange(segmentRange, ast),
} }
@ -260,12 +260,12 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async (
const ast = assertParse(code) const ast = assertParse(code)
// selection // selection
const segmentRanges: Array<[number, number, boolean]> = selectionSnippets.map( const segmentRanges: Array<SourceRange> = selectionSnippets.map(
(selectionSnippet) => [ (selectionSnippet) =>
topLevelRange(
code.indexOf(selectionSnippet), code.indexOf(selectionSnippet),
code.indexOf(selectionSnippet) + selectionSnippet.length, code.indexOf(selectionSnippet) + selectionSnippet.length
true, )
]
) )
// executeAst // executeAst
@ -596,11 +596,10 @@ extrude001 = extrude(-5, sketch001)
it('should correctly identify getOppositeEdge and baseEdge edges', () => { it('should correctly identify getOppositeEdge and baseEdge edges', () => {
const ast = assertParse(code) const ast = assertParse(code)
const lineOfInterest = `line([7.11, 3.48], %, $seg01)` const lineOfInterest = `line([7.11, 3.48], %, $seg01)`
const range: [number, number, boolean] = [ const range = topLevelRange(
code.indexOf(lineOfInterest), code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length, code.indexOf(lineOfInterest) + lineOfInterest.length
true, )
]
const pathToNode = getNodePathFromSourceRange(ast, range) const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return if (err(pathToNode)) return
const callExp = getNodeFromPath<CallExpression>( const callExp = getNodeFromPath<CallExpression>(
@ -615,11 +614,10 @@ extrude001 = extrude(-5, sketch001)
it('should correctly identify getPreviousAdjacentEdge edges', () => { it('should correctly identify getPreviousAdjacentEdge edges', () => {
const ast = assertParse(code) const ast = assertParse(code)
const lineOfInterest = `line([-6.37, 3.88], %, $seg02)` const lineOfInterest = `line([-6.37, 3.88], %, $seg02)`
const range: [number, number, boolean] = [ const range = topLevelRange(
code.indexOf(lineOfInterest), code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length, code.indexOf(lineOfInterest) + lineOfInterest.length
true, )
]
const pathToNode = getNodePathFromSourceRange(ast, range) const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return if (err(pathToNode)) return
const callExp = getNodeFromPath<CallExpression>( const callExp = getNodeFromPath<CallExpression>(
@ -634,11 +632,10 @@ extrude001 = extrude(-5, sketch001)
it('should correctly identify no edges', () => { it('should correctly identify no edges', () => {
const ast = assertParse(code) const ast = assertParse(code)
const lineOfInterest = `line([-3.29, -13.85], %)` const lineOfInterest = `line([-3.29, -13.85], %)`
const range: [number, number, boolean] = [ const range = topLevelRange(
code.indexOf(lineOfInterest), code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length, code.indexOf(lineOfInterest) + lineOfInterest.length
true, )
]
const pathToNode = getNodePathFromSourceRange(ast, range) const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return if (err(pathToNode)) return
const callExp = getNodeFromPath<CallExpression>( const callExp = getNodeFromPath<CallExpression>(
@ -660,13 +657,12 @@ describe('Testing button states', () => {
) => { ) => {
const ast = assertParse(code) const ast = assertParse(code)
const range: [number, number, boolean] = segmentSnippet const range = segmentSnippet
? [ ? topLevelRange(
code.indexOf(segmentSnippet), code.indexOf(segmentSnippet),
code.indexOf(segmentSnippet) + segmentSnippet.length, code.indexOf(segmentSnippet) + segmentSnippet.length
true, )
] : topLevelRange(ast.end, ast.end) // empty line in the end of the code
: [ast.end, ast.end, true] // empty line in the end of the code
const selectionRanges: Selections = { const selectionRanges: Selections = {
graphSelections: [ graphSelections: [

View File

@ -1,4 +1,5 @@
import { import {
ArtifactGraph,
CallExpression, CallExpression,
Expr, Expr,
Identifier, Identifier,
@ -31,11 +32,7 @@ import {
import { err, trap } from 'lib/trap' import { err, trap } from 'lib/trap'
import { Selection, Selections } from 'lib/selections' import { Selection, Selections } from 'lib/selections'
import { KclCommandValue } from 'lib/commandTypes' import { KclCommandValue } from 'lib/commandTypes'
import { import { Artifact, getSweepFromSuspectedPath } from 'lang/std/artifactGraph'
Artifact,
ArtifactGraph,
getSweepFromSuspectedPath,
} from 'lang/std/artifactGraph'
import { import {
kclManager, kclManager,
engineCommandManager, engineCommandManager,

View File

@ -1,9 +1,8 @@
import { ArtifactGraph } from 'lang/std/artifactGraph'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { Expr } from 'wasm-lib/kcl/bindings/Expr' import { Expr } from 'wasm-lib/kcl/bindings/Expr'
import { Program } from 'wasm-lib/kcl/bindings/Program' import { Program } from 'wasm-lib/kcl/bindings/Program'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { PathToNode, VariableDeclarator } from 'lang/wasm' import { ArtifactGraph, PathToNode, VariableDeclarator } from 'lang/wasm'
import { import {
getPathToExtrudeForSegmentSelection, getPathToExtrudeForSegmentSelection,
mutateAstWithTagForSketchSegment, mutateAstWithTagForSketchSegment,

View File

@ -4,6 +4,7 @@ import {
initPromise, initPromise,
PathToNode, PathToNode,
Identifier, Identifier,
topLevelRange,
} from './wasm' } from './wasm'
import { import {
findAllPreviousVariables, findAllPreviousVariables,
@ -57,7 +58,7 @@ variableBelowShouldNotBeIncluded = 3
const { variables, bodyPath, insertIndex } = findAllPreviousVariables( const { variables, bodyPath, insertIndex } = findAllPreviousVariables(
ast, ast,
execState.memory, execState.memory,
[rangeStart, rangeStart, true] topLevelRange(rangeStart, rangeStart)
) )
expect(variables).toEqual([ expect(variables).toEqual([
{ key: 'baseThick', value: 1 }, { key: 'baseThick', value: 1 },
@ -87,7 +88,10 @@ yo2 = hmm([identifierGuy + 5])`
it('find a safe binaryExpression', () => { it('find a safe binaryExpression', () => {
const ast = assertParse(code) const ast = assertParse(code)
const rangeStart = code.indexOf('100 + 100') + 2 const rangeStart = code.indexOf('100 + 100') + 2
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true]) const result = isNodeSafeToReplace(
ast,
topLevelRange(rangeStart, rangeStart)
)
if (err(result)) throw result if (err(result)) throw result
expect(result.isSafe).toBe(true) expect(result.isSafe).toBe(true)
expect(result.value?.type).toBe('BinaryExpression') expect(result.value?.type).toBe('BinaryExpression')
@ -100,7 +104,10 @@ yo2 = hmm([identifierGuy + 5])`
it('find a safe Identifier', () => { it('find a safe Identifier', () => {
const ast = assertParse(code) const ast = assertParse(code)
const rangeStart = code.indexOf('abc') const rangeStart = code.indexOf('abc')
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true]) const result = isNodeSafeToReplace(
ast,
topLevelRange(rangeStart, rangeStart)
)
if (err(result)) throw result if (err(result)) throw result
expect(result.isSafe).toBe(true) expect(result.isSafe).toBe(true)
expect(result.value?.type).toBe('Identifier') expect(result.value?.type).toBe('Identifier')
@ -109,7 +116,10 @@ yo2 = hmm([identifierGuy + 5])`
it('find a safe CallExpression', () => { it('find a safe CallExpression', () => {
const ast = assertParse(code) const ast = assertParse(code)
const rangeStart = code.indexOf('def') const rangeStart = code.indexOf('def')
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true]) const result = isNodeSafeToReplace(
ast,
topLevelRange(rangeStart, rangeStart)
)
if (err(result)) throw result if (err(result)) throw result
expect(result.isSafe).toBe(true) expect(result.isSafe).toBe(true)
expect(result.value?.type).toBe('CallExpression') expect(result.value?.type).toBe('CallExpression')
@ -122,7 +132,7 @@ yo2 = hmm([identifierGuy + 5])`
it('find an UNsafe CallExpression, as it has a PipeSubstitution', () => { it('find an UNsafe CallExpression, as it has a PipeSubstitution', () => {
const ast = assertParse(code) const ast = assertParse(code)
const rangeStart = code.indexOf('ghi') const rangeStart = code.indexOf('ghi')
const range: [number, number, boolean] = [rangeStart, rangeStart, true] const range = topLevelRange(rangeStart, rangeStart)
const result = isNodeSafeToReplace(ast, range) const result = isNodeSafeToReplace(ast, range)
if (err(result)) throw result if (err(result)) throw result
expect(result.isSafe).toBe(false) expect(result.isSafe).toBe(false)
@ -132,7 +142,10 @@ yo2 = hmm([identifierGuy + 5])`
it('find an UNsafe Identifier, as it is a callee', () => { it('find an UNsafe Identifier, as it is a callee', () => {
const ast = assertParse(code) const ast = assertParse(code)
const rangeStart = code.indexOf('ine([2.8,') const rangeStart = code.indexOf('ine([2.8,')
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true]) const result = isNodeSafeToReplace(
ast,
topLevelRange(rangeStart, rangeStart)
)
if (err(result)) throw result if (err(result)) throw result
expect(result.isSafe).toBe(false) expect(result.isSafe).toBe(false)
expect(result.value?.type).toBe('CallExpression') expect(result.value?.type).toBe('CallExpression')
@ -143,7 +156,10 @@ yo2 = hmm([identifierGuy + 5])`
it("find a safe BinaryExpression that's assigned to a variable", () => { it("find a safe BinaryExpression that's assigned to a variable", () => {
const ast = assertParse(code) const ast = assertParse(code)
const rangeStart = code.indexOf('5 + 6') + 1 const rangeStart = code.indexOf('5 + 6') + 1
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true]) const result = isNodeSafeToReplace(
ast,
topLevelRange(rangeStart, rangeStart)
)
if (err(result)) throw result if (err(result)) throw result
expect(result.isSafe).toBe(true) expect(result.isSafe).toBe(true)
expect(result.value?.type).toBe('BinaryExpression') expect(result.value?.type).toBe('BinaryExpression')
@ -156,7 +172,10 @@ yo2 = hmm([identifierGuy + 5])`
it('find a safe BinaryExpression that has a CallExpression within', () => { it('find a safe BinaryExpression that has a CallExpression within', () => {
const ast = assertParse(code) const ast = assertParse(code)
const rangeStart = code.indexOf('jkl') + 1 const rangeStart = code.indexOf('jkl') + 1
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true]) const result = isNodeSafeToReplace(
ast,
topLevelRange(rangeStart, rangeStart)
)
if (err(result)) throw result if (err(result)) throw result
expect(result.isSafe).toBe(true) expect(result.isSafe).toBe(true)
expect(result.value?.type).toBe('BinaryExpression') expect(result.value?.type).toBe('BinaryExpression')
@ -173,7 +192,10 @@ yo2 = hmm([identifierGuy + 5])`
const ast = assertParse(code) const ast = assertParse(code)
const rangeStart = code.indexOf('identifierGuy') + 1 const rangeStart = code.indexOf('identifierGuy') + 1
const result = isNodeSafeToReplace(ast, [rangeStart, rangeStart, true]) const result = isNodeSafeToReplace(
ast,
topLevelRange(rangeStart, rangeStart)
)
if (err(result)) throw result if (err(result)) throw result
expect(result.isSafe).toBe(true) expect(result.isSafe).toBe(true)
@ -222,11 +244,10 @@ describe('testing getNodePathFromSourceRange', () => {
const sourceIndex = code.indexOf(searchLn) + searchLn.length const sourceIndex = code.indexOf(searchLn) + searchLn.length
const ast = assertParse(code) const ast = assertParse(code)
const result = getNodePathFromSourceRange(ast, [ const result = getNodePathFromSourceRange(
sourceIndex, ast,
sourceIndex, topLevelRange(sourceIndex, sourceIndex)
true, )
])
expect(result).toEqual([ expect(result).toEqual([
['body', ''], ['body', ''],
[0, 'index'], [0, 'index'],
@ -241,11 +262,10 @@ describe('testing getNodePathFromSourceRange', () => {
const sourceIndex = code.indexOf(searchLn) + searchLn.length const sourceIndex = code.indexOf(searchLn) + searchLn.length
const ast = assertParse(code) const ast = assertParse(code)
const result = getNodePathFromSourceRange(ast, [ const result = getNodePathFromSourceRange(
sourceIndex, ast,
sourceIndex, topLevelRange(sourceIndex, sourceIndex)
true, )
])
const expected = [ const expected = [
['body', ''], ['body', ''],
[0, 'index'], [0, 'index'],
@ -257,18 +277,16 @@ describe('testing getNodePathFromSourceRange', () => {
expect(result).toEqual(expected) expect(result).toEqual(expected)
// expect similar result for start of line // expect similar result for start of line
const startSourceIndex = code.indexOf(searchLn) const startSourceIndex = code.indexOf(searchLn)
const startResult = getNodePathFromSourceRange(ast, [ const startResult = getNodePathFromSourceRange(
startSourceIndex, ast,
startSourceIndex, topLevelRange(startSourceIndex, startSourceIndex)
true, )
])
expect(startResult).toEqual([...expected, ['callee', 'CallExpression']]) expect(startResult).toEqual([...expected, ['callee', 'CallExpression']])
// expect similar result when whole line is selected // expect similar result when whole line is selected
const selectWholeThing = getNodePathFromSourceRange(ast, [ const selectWholeThing = getNodePathFromSourceRange(
startSourceIndex, ast,
sourceIndex, topLevelRange(startSourceIndex, sourceIndex)
true, )
])
expect(selectWholeThing).toEqual(expected) expect(selectWholeThing).toEqual(expected)
}) })
@ -283,11 +301,10 @@ describe('testing getNodePathFromSourceRange', () => {
const sourceIndex = code.indexOf(searchLn) const sourceIndex = code.indexOf(searchLn)
const ast = assertParse(code) const ast = assertParse(code)
const result = getNodePathFromSourceRange(ast, [ const result = getNodePathFromSourceRange(
sourceIndex, ast,
sourceIndex, topLevelRange(sourceIndex, sourceIndex)
true, )
])
expect(result).toEqual([ expect(result).toEqual([
['body', ''], ['body', ''],
[1, 'index'], [1, 'index'],
@ -313,11 +330,10 @@ describe('testing getNodePathFromSourceRange', () => {
const sourceIndex = code.indexOf(searchLn) const sourceIndex = code.indexOf(searchLn)
const ast = assertParse(code) const ast = assertParse(code)
const result = getNodePathFromSourceRange(ast, [ const result = getNodePathFromSourceRange(
sourceIndex, ast,
sourceIndex, topLevelRange(sourceIndex, sourceIndex)
true, )
])
expect(result).toEqual([ expect(result).toEqual([
['body', ''], ['body', ''],
[1, 'index'], [1, 'index'],
@ -341,11 +357,10 @@ describe('testing getNodePathFromSourceRange', () => {
const sourceIndex = code.indexOf(searchLn) const sourceIndex = code.indexOf(searchLn)
const ast = assertParse(code) const ast = assertParse(code)
const result = getNodePathFromSourceRange(ast, [ const result = getNodePathFromSourceRange(
sourceIndex, ast,
sourceIndex, topLevelRange(sourceIndex, sourceIndex)
true, )
])
expect(result).toEqual([ expect(result).toEqual([
['body', ''], ['body', ''],
[0, 'index'], [0, 'index'],
@ -375,7 +390,7 @@ part001 = startSketchAt([-1.41, 3.46])
const result = hasExtrudeSketch({ const result = hasExtrudeSketch({
ast, ast,
selection: { selection: {
codeRef: codeRefFromRange([100, 101, true], ast), codeRef: codeRefFromRange(topLevelRange(100, 101), ast),
}, },
programMemory: execState.memory, programMemory: execState.memory,
}) })
@ -395,7 +410,7 @@ part001 = startSketchAt([-1.41, 3.46])
const result = hasExtrudeSketch({ const result = hasExtrudeSketch({
ast, ast,
selection: { selection: {
codeRef: codeRefFromRange([100, 101, true], ast), codeRef: codeRefFromRange(topLevelRange(100, 101), ast),
}, },
programMemory: execState.memory, programMemory: execState.memory,
}) })
@ -409,7 +424,7 @@ part001 = startSketchAt([-1.41, 3.46])
const result = hasExtrudeSketch({ const result = hasExtrudeSketch({
ast, ast,
selection: { selection: {
codeRef: codeRefFromRange([10, 11, true], ast), codeRef: codeRefFromRange(topLevelRange(10, 11), ast),
}, },
programMemory: execState.memory, programMemory: execState.memory,
}) })
@ -431,11 +446,10 @@ describe('Testing findUsesOfTagInPipe', () => {
const lineOfInterest = `198.85], %, $seg01` const lineOfInterest = `198.85], %, $seg01`
const characterIndex = const characterIndex =
exampleCode.indexOf(lineOfInterest) + lineOfInterest.length exampleCode.indexOf(lineOfInterest) + lineOfInterest.length
const pathToNode = getNodePathFromSourceRange(ast, [ const pathToNode = getNodePathFromSourceRange(
characterIndex, ast,
characterIndex, topLevelRange(characterIndex, characterIndex)
true, )
])
const result = findUsesOfTagInPipe(ast, pathToNode) const result = findUsesOfTagInPipe(ast, pathToNode)
expect(result).toHaveLength(2) expect(result).toHaveLength(2)
result.forEach((range) => { result.forEach((range) => {
@ -448,11 +462,10 @@ describe('Testing findUsesOfTagInPipe', () => {
const lineOfInterest = `line([306.21, 198.82], %)` const lineOfInterest = `line([306.21, 198.82], %)`
const characterIndex = const characterIndex =
exampleCode.indexOf(lineOfInterest) + lineOfInterest.length exampleCode.indexOf(lineOfInterest) + lineOfInterest.length
const pathToNode = getNodePathFromSourceRange(ast, [ const pathToNode = getNodePathFromSourceRange(
characterIndex, ast,
characterIndex, topLevelRange(characterIndex, characterIndex)
true, )
])
const result = findUsesOfTagInPipe(ast, pathToNode) const result = findUsesOfTagInPipe(ast, pathToNode)
expect(result).toHaveLength(0) expect(result).toHaveLength(0)
}) })
@ -498,7 +511,10 @@ sketch003 = startSketchOn(extrude001, 'END')
exampleCode.indexOf(lineOfInterest) + lineOfInterest.length exampleCode.indexOf(lineOfInterest) + lineOfInterest.length
const extruded = hasSketchPipeBeenExtruded( const extruded = hasSketchPipeBeenExtruded(
{ {
codeRef: codeRefFromRange([characterIndex, characterIndex, true], ast), codeRef: codeRefFromRange(
topLevelRange(characterIndex, characterIndex),
ast
),
}, },
ast ast
) )
@ -511,7 +527,10 @@ sketch003 = startSketchOn(extrude001, 'END')
exampleCode.indexOf(lineOfInterest) + lineOfInterest.length exampleCode.indexOf(lineOfInterest) + lineOfInterest.length
const extruded = hasSketchPipeBeenExtruded( const extruded = hasSketchPipeBeenExtruded(
{ {
codeRef: codeRefFromRange([characterIndex, characterIndex, true], ast), codeRef: codeRefFromRange(
topLevelRange(characterIndex, characterIndex),
ast
),
}, },
ast ast
) )
@ -524,7 +543,10 @@ sketch003 = startSketchOn(extrude001, 'END')
exampleCode.indexOf(lineOfInterest) + lineOfInterest.length exampleCode.indexOf(lineOfInterest) + lineOfInterest.length
const extruded = hasSketchPipeBeenExtruded( const extruded = hasSketchPipeBeenExtruded(
{ {
codeRef: codeRefFromRange([characterIndex, characterIndex, true], ast), codeRef: codeRefFromRange(
topLevelRange(characterIndex, characterIndex),
ast
),
}, },
ast ast
) )
@ -651,11 +673,10 @@ myNestedVar = [
}) })
const literalIndex = code.indexOf(literalOfInterest) const literalIndex = code.indexOf(literalOfInterest)
const pathToNode2 = getNodePathFromSourceRange(ast, [ const pathToNode2 = getNodePathFromSourceRange(
literalIndex + 2, ast,
literalIndex + 2, topLevelRange(literalIndex + 2, literalIndex + 2)
true, )
])
expect(pathToNode).toEqual(pathToNode2) expect(pathToNode).toEqual(pathToNode2)
}) })
}) })

View File

@ -2,6 +2,7 @@ import { ToolTip } from 'lang/langHelpers'
import { Selection, Selections } from 'lib/selections' import { Selection, Selections } from 'lib/selections'
import { import {
ArrayExpression, ArrayExpression,
ArtifactGraph,
BinaryExpression, BinaryExpression,
CallExpression, CallExpression,
Expr, Expr,
@ -16,8 +17,8 @@ import {
sketchFromKclValue, sketchFromKclValue,
sketchFromKclValueOptional, sketchFromKclValueOptional,
SourceRange, SourceRange,
sourceRangeFromRust,
SyntaxType, SyntaxType,
topLevelRange,
VariableDeclaration, VariableDeclaration,
VariableDeclarator, VariableDeclarator,
} from './wasm' } from './wasm'
@ -32,7 +33,7 @@ import {
import { err, Reason } from 'lib/trap' import { err, Reason } from 'lib/trap'
import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement' import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { ArtifactGraph, codeRefFromRange } from './std/artifactGraph' import { codeRefFromRange } from './std/artifactGraph'
/** /**
* Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type. * Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type.
@ -819,7 +820,7 @@ export function isLinesParallelAndConstrained(
return { return {
isParallelAndConstrained, isParallelAndConstrained,
selection: { selection: {
codeRef: codeRefFromRange(sourceRangeFromRust(prevSourceRange), ast), codeRef: codeRefFromRange(prevSourceRange, ast),
artifact: artifactGraph.get(prevSegment.__geoMeta.id), artifact: artifactGraph.get(prevSegment.__geoMeta.id),
}, },
} }
@ -937,7 +938,7 @@ export function findUsesOfTagInPipe(
const tagArgValue = const tagArgValue =
tagArg.type === 'TagDeclarator' ? String(tagArg.value) : tagArg.name tagArg.type === 'TagDeclarator' ? String(tagArg.value) : tagArg.name
if (tagArgValue === tag) if (tagArgValue === tag)
dependentRanges.push([node.start, node.end, true]) dependentRanges.push(topLevelRange(node.start, node.end))
}, },
}) })
return dependentRanges return dependentRanges

View File

@ -1,559 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`testing createArtifactGraph > code with an extrusion, fillet and sketch of face: > snapshot of the artifactGraph 1`] = `
Map {
"UUID-0" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
12,
31,
true,
],
},
"id": "UUID",
"pathIds": [
"UUID",
],
"type": "plane",
},
"UUID-1" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
37,
64,
true,
],
},
"id": "UUID",
"planeId": "UUID",
"segIds": [
"UUID",
"UUID",
"UUID",
"UUID",
"UUID",
],
"solid2dId": "UUID",
"sweepId": "UUID",
"type": "path",
},
"UUID-2" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
70,
86,
true,
],
},
"edgeIds": [
"UUID",
"UUID",
],
"id": "UUID",
"pathId": "UUID",
"surfaceId": "UUID",
"type": "segment",
},
"UUID-3" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
92,
119,
true,
],
},
"edgeCutId": "UUID",
"edgeIds": [
"UUID",
"UUID",
],
"id": "UUID",
"pathId": "UUID",
"surfaceId": "UUID",
"type": "segment",
},
"UUID-4" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
125,
150,
true,
],
},
"edgeIds": [
"UUID",
"UUID",
],
"id": "UUID",
"pathId": "UUID",
"surfaceId": "UUID",
"type": "segment",
},
"UUID-5" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
156,
203,
true,
],
},
"edgeIds": [
"UUID",
"UUID",
],
"id": "UUID",
"pathId": "UUID",
"surfaceId": "UUID",
"type": "segment",
},
"UUID-6" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
209,
217,
true,
],
},
"edgeIds": [],
"id": "UUID",
"pathId": "UUID",
"type": "segment",
},
"UUID-7" => {
"id": "UUID",
"pathId": "UUID",
"type": "solid2D",
},
"UUID-8" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
231,
254,
true,
],
},
"edgeIds": [
"UUID",
"UUID",
"UUID",
"UUID",
"UUID",
"UUID",
"UUID",
"UUID",
],
"id": "UUID",
"pathId": "UUID",
"subType": "extrusion",
"surfaceIds": [
"UUID",
"UUID",
"UUID",
"UUID",
"UUID",
"UUID",
],
"type": "sweep",
},
"UUID-9" => {
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [],
"segId": "UUID",
"sweepId": "UUID",
"type": "wall",
},
"UUID-10" => {
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [
"UUID",
],
"segId": "UUID",
"sweepId": "UUID",
"type": "wall",
},
"UUID-11" => {
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [],
"segId": "UUID",
"sweepId": "UUID",
"type": "wall",
},
"UUID-12" => {
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [],
"segId": "UUID",
"sweepId": "UUID",
"type": "wall",
},
"UUID-13" => {
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [],
"subType": "start",
"sweepId": "UUID",
"type": "cap",
},
"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",
"type": "sweepEdge",
},
"UUID-23" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
260,
299,
true,
],
},
"consumedEdgeId": "UUID",
"edgeIds": [],
"id": "UUID",
"subType": "fillet",
"type": "edgeCut",
},
"UUID-24" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
350,
377,
true,
],
},
"id": "UUID",
"planeId": "UUID",
"segIds": [
"UUID",
"UUID",
"UUID",
"UUID",
],
"solid2dId": "UUID",
"sweepId": "UUID",
"type": "path",
},
"UUID-25" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
383,
398,
true,
],
},
"edgeIds": [
"UUID",
"UUID",
],
"id": "UUID",
"pathId": "UUID",
"surfaceId": "UUID",
"type": "segment",
},
"UUID-26" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
404,
420,
true,
],
},
"edgeIds": [
"UUID",
"UUID",
],
"id": "UUID",
"pathId": "UUID",
"surfaceId": "UUID",
"type": "segment",
},
"UUID-27" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
426,
473,
true,
],
},
"edgeIds": [
"UUID",
"UUID",
],
"id": "UUID",
"pathId": "UUID",
"surfaceId": "UUID",
"type": "segment",
},
"UUID-28" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
479,
487,
true,
],
},
"edgeIds": [],
"id": "UUID",
"pathId": "UUID",
"type": "segment",
},
"UUID-29" => {
"id": "UUID",
"pathId": "UUID",
"type": "solid2D",
},
"UUID-30" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
501,
522,
true,
],
},
"edgeIds": [
"UUID",
"UUID",
"UUID",
"UUID",
"UUID",
"UUID",
],
"id": "UUID",
"pathId": "UUID",
"subType": "extrusion",
"surfaceIds": [
"UUID",
"UUID",
"UUID",
"UUID",
],
"type": "sweep",
},
"UUID-31" => {
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [],
"segId": "UUID",
"sweepId": "UUID",
"type": "wall",
},
"UUID-32" => {
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [],
"segId": "UUID",
"sweepId": "UUID",
"type": "wall",
},
"UUID-33" => {
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [],
"segId": "UUID",
"sweepId": "UUID",
"type": "wall",
},
"UUID-34" => {
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [],
"subType": "end",
"sweepId": "UUID",
"type": "cap",
},
"UUID-35" => {
"id": "UUID",
"segId": "UUID",
"subType": "opposite",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-36" => {
"id": "UUID",
"segId": "UUID",
"subType": "adjacent",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-37" => {
"id": "UUID",
"segId": "UUID",
"subType": "opposite",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-38" => {
"id": "UUID",
"segId": "UUID",
"subType": "adjacent",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-39" => {
"id": "UUID",
"segId": "UUID",
"subType": "opposite",
"sweepId": "UUID",
"type": "sweepEdge",
},
"UUID-40" => {
"id": "UUID",
"segId": "UUID",
"subType": "adjacent",
"sweepId": "UUID",
"type": "sweepEdge",
},
}
`;

View File

@ -1,960 +0,0 @@
import {
makeDefaultPlanes,
assertParse,
initPromise,
Program,
ArtifactCommand,
ExecState,
} from 'lang/wasm'
import { Models } from '@kittycad/lib'
import {
ResponseMap,
createArtifactGraph,
filterArtifacts,
expandPlane,
expandPath,
expandSweep,
ArtifactGraph,
expandSegment,
getArtifactsToUpdate,
} from './artifactGraph'
import { err } from 'lib/trap'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { VITE_KC_DEV_TOKEN } from 'env'
import fsp from 'fs/promises'
import fs from 'fs'
import { chromium } from 'playwright'
import * as d3 from 'd3-force'
import path from 'path'
import pixelmatch from 'pixelmatch'
import { PNG } from 'pngjs'
import { Node } from 'wasm-lib/kcl/bindings/Node'
/*
Note this is an integration test, these tests connect to our real dev server and make websocket commands.
It's needed for testing the artifactGraph, as it is tied to the websocket commands.
*/
const pathStart = 'src/lang/std/artifactMapCache'
const fullPath = `${pathStart}/artifactMapCache.json`
const exampleCode1 = `sketch001 = startSketchOn('XY')
|> startProfileAt([-5, -5], %)
|> line([0, 10], %)
|> line([10.55, 0], %, $seg01)
|> line([0, -10], %, $seg02)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-10, sketch001)
|> fillet({ radius: 5, tags: [seg01] }, %)
sketch002 = startSketchOn(extrude001, seg02)
|> startProfileAt([-2, -6], %)
|> line([2, 3], %)
|> line([2, -3], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude002 = extrude(5, sketch002)
`
const exampleCodeNo3D = `sketch003 = startSketchOn('YZ')
|> startProfileAt([5.82, 0], %)
|> angledLine([180, 11.54], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
8.21
], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
sketch004 = startSketchOn('-XZ')
|> startProfileAt([0, 14.36], %)
|> line([15.49, 0.05], %)
|> tangentialArcTo([0, 0], %)
|> tangentialArcTo([-6.8, 8.17], %)
`
const sketchOnFaceOnFaceEtc = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line([4, 8], %)
|> line([5, -8], %, $seg01)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(6, sketch001)
sketch002 = startSketchOn(extrude001, seg01)
|> startProfileAt([-0.5, 0.5], %)
|> line([2, 5], %)
|> line([2, -5], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude002 = extrude(5, sketch002)
sketch003 = startSketchOn(extrude002, 'END')
|> startProfileAt([1, 1.5], %)
|> line([0.5, 2], %, $seg02)
|> line([1, -2], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude003 = extrude(4, sketch003)
sketch004 = startSketchOn(extrude003, seg02)
|> startProfileAt([-3, 14], %)
|> line([0.5, 1], %)
|> line([0.5, -2], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude004 = extrude(3, sketch004)
`
const exampleCodeOffsetPlanes = `
offsetPlane001 = offsetPlane("XY", 20)
offsetPlane002 = offsetPlane("XZ", -50)
offsetPlane003 = offsetPlane("YZ", 10)
sketch002 = startSketchOn(offsetPlane001)
|> startProfileAt([0, 0], %)
|> line([6.78, 15.01], %)
`
// add more code snippets here and use `getCommands` to get the artifactCommands and responseMap for more tests
const codeToWriteCacheFor = {
exampleCode1,
sketchOnFaceOnFaceEtc,
exampleCodeNo3D,
exampleCodeOffsetPlanes,
} as const
type CodeKey = keyof typeof codeToWriteCacheFor
type CacheShape = {
[key in CodeKey]: {
artifactCommands: ArtifactCommand[]
responseMap: ResponseMap
execStateArtifacts: ExecState['artifacts']
}
}
beforeAll(async () => {
await initPromise
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
await new Promise((resolve) => {
engineCommandManager.start({
// disableWebRTC: true,
token: VITE_KC_DEV_TOKEN,
// there does seem to be a minimum resolution, not sure what it is but 256 works ok.
width: 256,
height: 256,
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
setMediaStream: () => {},
setIsStreamReady: () => {},
// eslint-disable-next-line @typescript-eslint/no-misused-promises
callbackOnEngineLiteConnect: async () => {
const cacheEntries = Object.entries(codeToWriteCacheFor) as [
CodeKey,
string
][]
const cacheToWriteToFileTemp: Partial<CacheShape> = {}
for (const [codeKey, code] of cacheEntries) {
const ast = assertParse(code)
await kclManager.executeAst({ ast })
cacheToWriteToFileTemp[codeKey] = {
artifactCommands: kclManager.execState.artifactCommands,
responseMap: engineCommandManager.responseMap,
execStateArtifacts: kclManager.execState.artifacts,
}
}
const cache = JSON.stringify(cacheToWriteToFileTemp)
await fsp.mkdir(pathStart, { recursive: true })
await fsp.writeFile(fullPath, cache)
resolve(true)
},
})
})
}, 20_000)
afterAll(() => {
engineCommandManager.tearDown()
})
describe('testing createArtifactGraph', () => {
describe('code with offset planes and a sketch:', () => {
let ast: Node<Program>
let theMap: ReturnType<typeof createArtifactGraph>
it('setup', () => {
// putting this logic in here because describe blocks runs before beforeAll has finished
const {
artifactCommands,
responseMap,
ast: _ast,
execStateArtifacts,
} = getCommands('exampleCodeOffsetPlanes')
ast = _ast
theMap = createArtifactGraph({
artifactCommands,
responseMap,
ast,
execStateArtifacts,
})
})
it(`there should be one sketch`, () => {
const sketches = [...filterArtifacts({ types: ['path'] }, theMap)].map(
(path) => expandPath(path[1], theMap)
)
expect(sketches).toHaveLength(1)
sketches.forEach((path) => {
if (err(path)) throw path
expect(path.type).toBe('path')
})
})
it(`there should be three offsetPlanes`, () => {
const offsetPlanes = [
...filterArtifacts({ types: ['plane'] }, theMap),
].map((plane) => expandPlane(plane[1], theMap))
expect(offsetPlanes).toHaveLength(3)
offsetPlanes.forEach((path) => {
expect(path.type).toBe('plane')
})
})
it(`Only one offset plane should have a path`, () => {
const offsetPlanes = [
...filterArtifacts({ types: ['plane'] }, theMap),
].map((plane) => expandPlane(plane[1], theMap))
const offsetPlaneWithPaths = offsetPlanes.filter(
(plane) => plane.paths.length
)
expect(offsetPlaneWithPaths).toHaveLength(1)
})
})
describe('code with an extrusion, fillet and sketch of face:', () => {
let ast: Node<Program>
let theMap: ReturnType<typeof createArtifactGraph>
it('setup', () => {
// putting this logic in here because describe blocks runs before beforeAll has finished
const {
artifactCommands,
responseMap,
ast: _ast,
execStateArtifacts,
} = getCommands('exampleCode1')
ast = _ast
theMap = createArtifactGraph({
artifactCommands,
responseMap,
ast,
execStateArtifacts,
})
})
it('there should be two planes for the extrusion and the sketch on face', () => {
const planes = [...filterArtifacts({ types: ['plane'] }, theMap)].map(
(plane) => expandPlane(plane[1], theMap)
)
expect(planes).toHaveLength(1)
planes.forEach((path) => {
expect(path.type).toBe('plane')
})
})
it('there should be two paths for the extrusion and the sketch on face', () => {
const paths = [...filterArtifacts({ types: ['path'] }, theMap)].map(
(path) => expandPath(path[1], theMap)
)
expect(paths).toHaveLength(2)
paths.forEach((path) => {
if (err(path)) throw path
expect(path.type).toBe('path')
})
})
it('there should be two extrusions, for the original and the sketchOnFace, the first extrusion should have 6 sides of the cube', () => {
const extrusions = [...filterArtifacts({ types: ['sweep'] }, theMap)].map(
(extrusion) => expandSweep(extrusion[1], theMap)
)
expect(extrusions).toHaveLength(2)
extrusions.forEach((extrusion, index) => {
if (err(extrusion)) throw extrusion
expect(extrusion.type).toBe('sweep')
const firstExtrusionIsACubeIE6Sides = 6
// Each face of the triangular prism (5), but without the bottom cap.
// The engine doesn't generate that.
const secondExtrusionIsATriangularPrism = 4
expect(extrusion.surfaces.length).toBe(
!index
? firstExtrusionIsACubeIE6Sides
: secondExtrusionIsATriangularPrism
)
})
})
it('there should be 5 + 4 segments, 4 (+close) from the first extrusion and 3 (+close) from the second', () => {
const segments = [...filterArtifacts({ types: ['segment'] }, theMap)].map(
(segment) => expandSegment(segment[1], theMap)
)
expect(segments).toHaveLength(9)
})
it('snapshot of the artifactGraph', () => {
const stableMap = new Map(
[...theMap].map(([, artifact], index): [string, any] => {
const stableValue: any = {}
Object.entries(artifact).forEach(([propName, value]) => {
if (
propName === 'type' ||
propName === 'codeRef' ||
propName === 'subType'
) {
stableValue[propName] = value
return
}
if (Array.isArray(value))
stableValue[propName] = value.map(() => 'UUID')
if (typeof value === 'string' && value)
stableValue[propName] = 'UUID'
})
return [`UUID-${index}`, stableValue]
})
)
expect(stableMap).toMatchSnapshot()
})
it('screenshot graph', async () => {
// Ostensibly this takes a screen shot of the graph of the artifactGraph
// but it's it also tests that all of the id links are correct because if one
// of the edges refers to a non-existent node, the graph will throw.
// further more we can check that each edge is bi-directional, if it's not
// by checking the arrow heads going both ways, on the graph.
await GraphTheGraph(theMap, 2000, 2000, 'exampleCode1.png')
}, 20000)
})
describe(`code with sketches but no extrusions or other 3D elements`, () => {
let ast: Node<Program>
let theMap: ReturnType<typeof createArtifactGraph>
it(`setup`, () => {
// putting this logic in here because describe blocks runs before beforeAll has finished
const {
artifactCommands,
responseMap,
ast: _ast,
execStateArtifacts,
} = getCommands('exampleCodeNo3D')
ast = _ast
theMap = createArtifactGraph({
artifactCommands,
responseMap,
ast,
execStateArtifacts,
})
})
it('there should be two planes, one for each sketch path', () => {
const planes = [...filterArtifacts({ types: ['plane'] }, theMap)].map(
(plane) => expandPlane(plane[1], theMap)
)
expect(planes).toHaveLength(2)
planes.forEach((path) => {
expect(path.type).toBe('plane')
})
})
it('there should be two paths, one on each plane', () => {
const paths = [...filterArtifacts({ types: ['path'] }, theMap)].map(
(path) => expandPath(path[1], theMap)
)
expect(paths).toHaveLength(2)
paths.forEach((path) => {
if (err(path)) throw path
expect(path.type).toBe('path')
})
})
it(`there should be 1 solid2D, just for the first closed path`, () => {
const solid2Ds = [...filterArtifacts({ types: ['solid2D'] }, theMap)]
expect(solid2Ds).toHaveLength(1)
})
it('there should be no extrusions', () => {
const extrusions = [...filterArtifacts({ types: ['sweep'] }, theMap)].map(
(extrusion) => expandSweep(extrusion[1], theMap)
)
expect(extrusions).toHaveLength(0)
})
it('there should be 8 segments, 4 + 1 (close) from the first sketch and 3 from the second', () => {
const segments = [...filterArtifacts({ types: ['segment'] }, theMap)].map(
(segment) => expandSegment(segment[1], theMap)
)
expect(segments).toHaveLength(8)
})
it('screenshot graph', async () => {
// Ostensibly this takes a screen shot of the graph of the artifactGraph
// but it's it also tests that all of the id links are correct because if one
// of the edges refers to a non-existent node, the graph will throw.
// further more we can check that each edge is bi-directional, if it's not
// by checking the arrow heads going both ways, on the graph.
await GraphTheGraph(theMap, 2000, 2000, 'exampleCodeNo3D.png')
}, 20000)
})
})
describe('capture graph of sketchOnFaceOnFace...', () => {
describe('code with an extrusion, fillet and sketch of face:', () => {
let ast: Node<Program>
let theMap: ReturnType<typeof createArtifactGraph>
it('setup', async () => {
// putting this logic in here because describe blocks runs before beforeAll has finished
const {
artifactCommands,
responseMap,
ast: _ast,
execStateArtifacts,
} = getCommands('sketchOnFaceOnFaceEtc')
ast = _ast
theMap = createArtifactGraph({
artifactCommands,
responseMap,
ast,
execStateArtifacts,
})
// Ostensibly this takes a screen shot of the graph of the artifactGraph
// but it's it also tests that all of the id links are correct because if one
// of the edges refers to a non-existent node, the graph will throw.
// further more we can check that each edge is bi-directional, if it's not
// by checking the arrow heads going both ways, on the graph.
await GraphTheGraph(theMap, 3000, 3000, 'sketchOnFaceOnFaceEtc.png')
}, 20000)
})
})
function getCommands(
codeKey: CodeKey
): CacheShape[CodeKey] & { ast: Node<Program> } {
const ast = assertParse(codeKey)
const file = fs.readFileSync(fullPath, 'utf-8')
const parsed: CacheShape = JSON.parse(file)
// these either already exist from the last run, or were created in
const artifactCommands = parsed[codeKey].artifactCommands
const responseMap = parsed[codeKey].responseMap
const execStateArtifacts = parsed[codeKey].execStateArtifacts
return {
artifactCommands,
responseMap,
ast,
execStateArtifacts,
}
}
async function GraphTheGraph(
theMap: ArtifactGraph,
sizeX: number,
sizeY: number,
imageName: string
) {
const nodes: Array<{ id: string; label: string }> = []
const edges: Array<{ source: string; target: string; label: string }> = []
let index = 0
for (const [commandId, artifact] of theMap) {
nodes.push({
id: commandId,
label: `${artifact.type}-${index++}`,
})
Object.entries(artifact).forEach(([propName, value]) => {
if (
propName === 'type' ||
propName === 'codeRef' ||
propName === 'subType' ||
propName === 'id'
)
return
if (Array.isArray(value))
value.forEach((v) => {
v && edges.push({ source: commandId, target: v, label: propName })
})
if (typeof value === 'string' && value)
edges.push({ source: commandId, target: value, label: propName })
})
}
// Create a force simulation to calculate node positions
const simulation = d3
.forceSimulation(nodes as any)
.force(
'link',
d3
.forceLink(edges)
.id((d: any) => d.id)
.distance(100)
)
.force('charge', d3.forceManyBody().strength(-300))
.force('center', d3.forceCenter(300, 200))
.stop()
// Run the simulation
for (let i = 0; i < 300; ++i) simulation.tick()
// Create traces for Plotly
const nodeTrace = {
x: nodes.map((node: any) => node.x),
y: nodes.map((node: any) => node.y),
text: nodes.map((node) => node.label), // Use the custom label
mode: 'markers+text',
type: 'scatter',
marker: { size: 20, color: 'gray' }, // Nodes in gray
textfont: { size: 14, color: 'black' }, // Labels in black
textposition: 'top center', // Position text on top
}
const edgeTrace = {
x: [],
y: [],
mode: 'lines',
type: 'scatter',
line: { width: 2, color: 'lightgray' }, // Edges in light gray
}
const annotations: any[] = []
edges.forEach((edge) => {
const sourceNode = nodes.find(
(node: any) => node.id === (edge as any).source.id
)
const targetNode = nodes.find(
(node: any) => node.id === (edge as any).target.id
)
// Check if nodes are found
if (!sourceNode || !targetNode) {
throw new Error(
// @ts-ignore
`Node not found: ${!sourceNode ? edge.source.id : edge.target.id}`
)
}
// @ts-ignore
edgeTrace.x.push(sourceNode.x, targetNode.x, null)
// @ts-ignore
edgeTrace.y.push(sourceNode.y, targetNode.y, null)
// Calculate offset for arrowhead
const offsetFactor = 0.9 // Adjust this factor to control the offset distance
// @ts-ignore
const offsetX = (targetNode.x - sourceNode.x) * offsetFactor
// @ts-ignore
const offsetY = (targetNode.y - sourceNode.y) * offsetFactor
// Add arrowhead annotation with offset
annotations.push({
// @ts-ignore
ax: sourceNode.x,
// @ts-ignore
ay: sourceNode.y,
// @ts-ignore
x: targetNode.x - offsetX,
// @ts-ignore
y: targetNode.y - offsetY,
xref: 'x',
yref: 'y',
axref: 'x',
ayref: 'y',
showarrow: true,
arrowhead: 2,
arrowsize: 1,
arrowwidth: 2,
arrowcolor: 'darkgray', // Arrowheads in dark gray
})
// Add edge label annotation closer to the edge tail (25% of the length)
// @ts-ignore
const labelX = sourceNode.x * 0.75 + targetNode.x * 0.25
// @ts-ignore
const labelY = sourceNode.y * 0.75 + targetNode.y * 0.25
annotations.push({
x: labelX,
y: labelY,
xref: 'x',
yref: 'y',
text: edge.label,
showarrow: false,
font: { size: 12, color: 'black' }, // Edge labels in black
align: 'center',
})
})
const data = [edgeTrace, nodeTrace]
const layout = {
// title: 'Force-Directed Graph with Nodes and Edges',
xaxis: { showgrid: false, zeroline: false, showticklabels: false },
yaxis: { showgrid: false, zeroline: false, showticklabels: false },
showlegend: false,
annotations: annotations,
}
// Export to PNG using Playwright
const browser = await chromium.launch()
const page = await browser.newPage()
await page.setContent(`
<html>
<head>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
</head>
<body>
<div id="plotly-graph" style="width:${sizeX}px;height:${sizeY}px;"></div>
<script>
Plotly.newPlot('plotly-graph', ${JSON.stringify(
data
)}, ${JSON.stringify(layout)})
</script>
</body>
</html>
`)
await page.waitForSelector('#plotly-graph')
const element = await page.$('#plotly-graph')
// @ts-ignore
await element.screenshot({
path: `./e2e/playwright/temp3.png`,
})
await browser.close()
const originalImgPath = path.resolve(
`./src/lang/std/artifactMapGraphs/${imageName}`
)
// chop the top 30 pixels off the image
const originalImgExists = fs.existsSync(originalImgPath)
const originalImg = originalImgExists
? PNG.sync.read(fs.readFileSync(originalImgPath))
: null
// const img1Data = new Uint8Array(img1.data)
// const img1DataChopped = img1Data.slice(30 * img1.width * 4)
// img1.data = Buffer.from(img1DataChopped)
const newImagePath = path.resolve('./e2e/playwright/temp3.png')
const newImage = PNG.sync.read(fs.readFileSync(newImagePath))
const newImageData = new Uint8Array(newImage.data)
const newImageDataChopped = newImageData.slice(30 * newImage.width * 4)
newImage.data = Buffer.from(newImageDataChopped)
const { width, height } = originalImg ?? newImage
const diff = new PNG({ width, height })
const imageSizeDifferent = originalImg?.data.length !== newImage.data.length
let numDiffPixels = 0
if (!imageSizeDifferent) {
numDiffPixels = pixelmatch(
originalImg.data,
newImage.data,
diff.data,
width,
height,
{
threshold: 0.1,
}
)
}
if (numDiffPixels > 10 || imageSizeDifferent) {
console.warn('numDiffPixels', numDiffPixels)
// write file out to final place
fs.writeFileSync(
`src/lang/std/artifactMapGraphs/${imageName}`,
PNG.sync.write(newImage)
)
}
}
describe('testing getArtifactsToUpdate', () => {
it('should return an array of artifacts to update', () => {
const { artifactCommands, responseMap, ast, execStateArtifacts } =
getCommands('exampleCode1')
const map = createArtifactGraph({
artifactCommands,
responseMap,
ast,
execStateArtifacts,
})
const getArtifact = (id: string) => map.get(id)
const currentPlaneId = 'UUID-1'
const getUpdateObjects = (type: Models['ModelingCmd_type']['type']) => {
const artifactCommand = artifactCommands.find(
(a) => a.command.type === type
)
if (!artifactCommand) {
throw new Error(`No artifactCommand found for ${type}`)
}
const artifactsToUpdate = getArtifactsToUpdate({
artifactCommand,
responseMap,
getArtifact,
currentPlaneId,
ast,
execStateArtifacts,
})
return artifactsToUpdate.map(({ artifact }) => artifact)
}
expect(getUpdateObjects('start_path')).toEqual([
{
type: 'path',
segIds: [],
id: expect.any(String),
planeId: 'UUID-1',
sweepId: undefined,
codeRef: {
pathToNode: [['body', '']],
range: [37, 64, true],
},
},
])
expect(getUpdateObjects('extrude')).toEqual([
{
type: 'sweep',
subType: 'extrusion',
pathId: expect.any(String),
id: expect.any(String),
surfaceIds: [],
edgeIds: [],
codeRef: {
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
{
type: 'path',
id: expect.any(String),
segIds: expect.any(Array),
planeId: expect.any(String),
sweepId: expect.any(String),
codeRef: {
range: [37, 64, true],
pathToNode: [['body', '']],
},
solid2dId: expect.any(String),
},
])
expect(getUpdateObjects('extend_path')).toEqual([
{
type: 'segment',
id: expect.any(String),
pathId: expect.any(String),
surfaceId: undefined,
edgeIds: [],
codeRef: {
range: [70, 86, true],
pathToNode: [['body', '']],
},
},
{
type: 'path',
id: expect.any(String),
segIds: expect.any(Array),
planeId: expect.any(String),
sweepId: expect.any(String),
codeRef: {
range: [37, 64, true],
pathToNode: [['body', '']],
},
solid2dId: expect.any(String),
},
])
expect(getUpdateObjects('solid3d_fillet_edge')).toEqual([
{
type: 'edgeCut',
subType: 'fillet',
id: expect.any(String),
consumedEdgeId: expect.any(String),
edgeIds: [],
surfaceId: undefined,
codeRef: {
range: [260, 299, true],
pathToNode: [['body', '']],
},
},
{
type: 'segment',
id: expect.any(String),
pathId: expect.any(String),
surfaceId: expect.any(String),
edgeIds: expect.any(Array),
codeRef: {
range: [92, 119, true],
pathToNode: [['body', '']],
},
edgeCutId: expect.any(String),
},
])
expect(getUpdateObjects('solid3d_get_extrusion_face_info')).toEqual([
{
type: 'wall',
id: expect.any(String),
segId: expect.any(String),
edgeCutEdgeIds: [],
sweepId: expect.any(String),
pathIds: [],
},
{
type: 'segment',
id: expect.any(String),
pathId: expect.any(String),
surfaceId: expect.any(String),
edgeIds: expect.any(Array),
codeRef: {
range: [156, 203, true],
pathToNode: [['body', '']],
},
},
{
type: 'sweep',
subType: 'extrusion',
id: expect.any(String),
pathId: expect.any(String),
surfaceIds: expect.any(Array),
edgeIds: expect.any(Array),
codeRef: {
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
{
type: 'wall',
id: expect.any(String),
segId: expect.any(String),
edgeCutEdgeIds: [],
sweepId: expect.any(String),
pathIds: [],
},
{
type: 'segment',
id: expect.any(String),
pathId: expect.any(String),
surfaceId: expect.any(String),
edgeIds: expect.any(Array),
codeRef: {
range: [125, 150, true],
pathToNode: [['body', '']],
},
},
{
type: 'sweep',
subType: 'extrusion',
id: expect.any(String),
pathId: expect.any(String),
surfaceIds: expect.any(Array),
edgeIds: expect.any(Array),
codeRef: {
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
{
type: 'wall',
id: expect.any(String),
segId: expect.any(String),
edgeCutEdgeIds: [],
sweepId: expect.any(String),
pathIds: [],
},
{
type: 'segment',
id: expect.any(String),
pathId: expect.any(String),
surfaceId: expect.any(String),
edgeIds: expect.any(Array),
codeRef: {
range: [92, 119, true],
pathToNode: [['body', '']],
},
edgeCutId: expect.any(String),
},
{
type: 'sweep',
subType: 'extrusion',
id: expect.any(String),
pathId: expect.any(String),
surfaceIds: expect.any(Array),
edgeIds: expect.any(Array),
codeRef: {
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
{
type: 'wall',
id: expect.any(String),
segId: expect.any(String),
edgeCutEdgeIds: [],
sweepId: expect.any(String),
pathIds: [],
},
{
type: 'segment',
id: expect.any(String),
pathId: expect.any(String),
surfaceId: expect.any(String),
edgeIds: expect.any(Array),
codeRef: {
range: [70, 86, true],
pathToNode: [['body', '']],
},
},
{
type: 'sweep',
subType: 'extrusion',
id: expect.any(String),
pathId: expect.any(String),
surfaceIds: expect.any(Array),
edgeIds: expect.any(Array),
codeRef: {
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
{
type: 'cap',
subType: 'start',
id: expect.any(String),
edgeCutEdgeIds: [],
sweepId: expect.any(String),
pathIds: [],
},
{
type: 'sweep',
subType: 'extrusion',
id: expect.any(String),
pathId: expect.any(String),
surfaceIds: expect.any(Array),
edgeIds: expect.any(Array),
codeRef: {
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
{
type: 'cap',
subType: 'end',
id: expect.any(String),
edgeCutEdgeIds: [],
sweepId: expect.any(String),
pathIds: [],
},
{
type: 'sweep',
subType: 'extrusion',
id: expect.any(String),
pathId: expect.any(String),
surfaceIds: expect.any(Array),
edgeIds: expect.any(Array),
codeRef: {
range: [231, 254, true],
pathToNode: [['body', '']],
},
},
])
})
})

View File

@ -1,17 +1,25 @@
import { import {
ArtifactCommand, Artifact,
ExecState, ArtifactGraph,
ArtifactId,
PathToNode, PathToNode,
Program, Program,
SourceRange, SourceRange,
sourceRangeFromRust, PathArtifact,
PlaneArtifact,
WallArtifact,
SegmentArtifact,
Solid2dArtifact as Solid2D,
SweepArtifact,
SweepEdge,
CapArtifact,
EdgeCut,
} from 'lang/wasm' } from 'lang/wasm'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { getNodePathFromSourceRange } from 'lang/queryAst' import { getNodePathFromSourceRange } from 'lang/queryAst'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { Node } from 'wasm-lib/kcl/bindings/Node'
export type ArtifactId = string export type { Artifact, ArtifactId, SegmentArtifact } from 'lang/wasm'
interface BaseArtifact { interface BaseArtifact {
id: ArtifactId id: ArtifactId
@ -22,30 +30,12 @@ export interface CodeRef {
pathToNode: PathToNode pathToNode: PathToNode
} }
export interface PlaneArtifact extends BaseArtifact {
type: 'plane'
pathIds: Array<ArtifactId>
codeRef: CodeRef
}
export interface PlaneArtifactRich extends BaseArtifact { export interface PlaneArtifactRich extends BaseArtifact {
type: 'plane' type: 'plane'
paths: Array<PathArtifact> paths: Array<PathArtifact>
codeRef: CodeRef codeRef: CodeRef
} }
export interface PathArtifact extends BaseArtifact {
type: 'path'
planeId: ArtifactId
segIds: Array<ArtifactId>
sweepId?: ArtifactId
solid2dId?: ArtifactId
codeRef: CodeRef
}
interface solid2D extends BaseArtifact {
type: 'solid2D'
pathId: ArtifactId
}
export interface PathArtifactRich extends BaseArtifact { export interface PathArtifactRich extends BaseArtifact {
type: 'path' type: 'path'
/** A path must always lie on a plane */ /** A path must always lie on a plane */
@ -53,18 +43,10 @@ export interface PathArtifactRich extends BaseArtifact {
/** A path must always contain 0 or more segments */ /** A path must always contain 0 or more segments */
segments: Array<SegmentArtifact> segments: Array<SegmentArtifact>
/** A path may not result in a sweep artifact */ /** A path may not result in a sweep artifact */
sweep?: SweepArtifact sweep: SweepArtifact | null
codeRef: CodeRef codeRef: CodeRef
} }
export interface SegmentArtifact extends BaseArtifact {
type: 'segment'
pathId: ArtifactId
surfaceId?: ArtifactId
edgeIds: Array<ArtifactId>
edgeCutId?: ArtifactId
codeRef: CodeRef
}
interface SegmentArtifactRich extends BaseArtifact { interface SegmentArtifactRich extends BaseArtifact {
type: 'segment' type: 'segment'
path: PathArtifact path: PathArtifact
@ -74,15 +56,6 @@ interface SegmentArtifactRich extends BaseArtifact {
codeRef: CodeRef codeRef: CodeRef
} }
/** A Sweep is a more generic term for extrude, revolve, loft and sweep*/
interface SweepArtifact extends BaseArtifact {
type: 'sweep'
subType: 'extrusion' | 'revolve' | 'loft' | 'sweep'
pathId: string
surfaceIds: Array<string>
edgeIds: Array<string>
codeRef: CodeRef
}
interface SweepArtifactRich extends BaseArtifact { interface SweepArtifactRich extends BaseArtifact {
type: 'sweep' type: 'sweep'
subType: 'extrusion' | 'revolve' | 'loft' | 'sweep' subType: 'extrusion' | 'revolve' | 'loft' | 'sweep'
@ -92,58 +65,6 @@ interface SweepArtifactRich extends BaseArtifact {
codeRef: CodeRef codeRef: CodeRef
} }
interface WallArtifact extends BaseArtifact {
type: 'wall'
segId: ArtifactId
edgeCutEdgeIds: Array<ArtifactId>
sweepId: ArtifactId
pathIds: Array<ArtifactId>
}
interface CapArtifact extends BaseArtifact {
type: 'cap'
subType: 'start' | 'end'
edgeCutEdgeIds: Array<ArtifactId>
sweepId: ArtifactId
pathIds: Array<ArtifactId>
}
interface SweepEdge extends BaseArtifact {
type: 'sweepEdge'
segId: ArtifactId
sweepId: ArtifactId
subType: 'opposite' | 'adjacent'
}
/** A edgeCut is a more generic term for both fillet or chamfer */
interface EdgeCut extends BaseArtifact {
type: 'edgeCut'
subType: 'fillet' | 'chamfer'
consumedEdgeId: ArtifactId
edgeIds: Array<ArtifactId>
surfaceId?: ArtifactId
codeRef: CodeRef
}
interface EdgeCutEdge extends BaseArtifact {
type: 'edgeCutEdge'
edgeCutId: ArtifactId
surfaceId: ArtifactId
}
export type Artifact =
| PlaneArtifact
| PathArtifact
| SegmentArtifact
| SweepArtifact
| WallArtifact
| CapArtifact
| SweepEdge
| EdgeCut
| EdgeCutEdge
| solid2D
export type ArtifactGraph = Map<ArtifactId, Artifact>
export type EngineCommand = Models['WebSocketRequest_type'] export type EngineCommand = Models['WebSocketRequest_type']
type OkWebSocketResponseData = Models['OkWebSocketResponseData_type'] type OkWebSocketResponseData = Models['OkWebSocketResponseData_type']
@ -152,437 +73,6 @@ export interface ResponseMap {
[commandId: string]: OkWebSocketResponseData [commandId: string]: OkWebSocketResponseData
} }
/** Creates a graph of artifacts from a list of ordered commands and their responses
* muting the Map should happen entirely this function, other functions called within
* should return data on how to update the map, and not do so directly.
*/
export function createArtifactGraph({
artifactCommands,
responseMap,
ast,
execStateArtifacts,
}: {
artifactCommands: Array<ArtifactCommand>
responseMap: ResponseMap
ast: Node<Program>
execStateArtifacts: ExecState['artifacts']
}) {
const myMap = new Map<ArtifactId, Artifact>()
/** see docstring for {@link getArtifactsToUpdate} as to why this is needed */
let currentPlaneId = ''
for (const artifactCommand of artifactCommands) {
if (artifactCommand.command.type === 'enable_sketch_mode') {
currentPlaneId = artifactCommand.command.entity_id
}
if (artifactCommand.command.type === 'sketch_mode_disable') {
currentPlaneId = ''
}
const artifactsToUpdate = getArtifactsToUpdate({
artifactCommand,
responseMap,
getArtifact: (id: ArtifactId) => myMap.get(id),
currentPlaneId,
ast,
execStateArtifacts,
})
artifactsToUpdate.forEach(({ id, artifact }) => {
const mergedArtifact = mergeArtifacts(myMap.get(id), artifact)
myMap.set(id, mergedArtifact)
})
}
return myMap
}
/** Merges two artifacts, since our artifacts only contain strings and arrays of string for values we coerce that
* but maybe types can be improved here.
*/
function mergeArtifacts(
oldArtifact: Artifact | undefined,
newArtifact: Artifact
): Artifact {
// only has string and array of strings
interface GenericArtifact {
[key: string]: string | Array<string>
}
if (!oldArtifact) return newArtifact
// merging artifacts of different types should never happen, but if it does, just return the new artifact
if (oldArtifact.type !== newArtifact.type) return newArtifact
const _oldArtifact = oldArtifact as any as GenericArtifact
const mergedArtifact = { ...oldArtifact, ...newArtifact } as GenericArtifact
Object.entries(newArtifact as any as GenericArtifact).forEach(
([propName, value]) => {
const otherValue = _oldArtifact[propName]
if (Array.isArray(value) && Array.isArray(otherValue)) {
mergedArtifact[propName] = [...new Set([...otherValue, ...value])]
}
}
)
return mergedArtifact as any as Artifact
}
/**
* Processes a single command and it's response in order to populate the artifact map
* It does not mutate the map directly, but returns an array of artifacts to update
*
* @param currentPlaneId is only needed for `start_path` commands because this command does not have a pathId
* instead it relies on the id used with the `enable_sketch_mode` command, so this much be kept track of
* outside of this function. It would be good to update the `start_path` command to include the planeId so we
* can remove this.
*/
export function getArtifactsToUpdate({
artifactCommand,
getArtifact,
responseMap,
currentPlaneId,
ast,
execStateArtifacts,
}: {
artifactCommand: ArtifactCommand
responseMap: ResponseMap
/** Passing in a getter because we don't wan this function to update the map directly */
getArtifact: (id: ArtifactId) => Artifact | undefined
currentPlaneId: ArtifactId
ast: Node<Program>
execStateArtifacts: ExecState['artifacts']
}): Array<{
id: ArtifactId
artifact: Artifact
}> {
const range = sourceRangeFromRust(artifactCommand.range)
const pathToNode = getNodePathFromSourceRange(ast, range)
const id = artifactCommand.cmdId
const response = responseMap[id]
const cmd = artifactCommand.command
const returnArr: ReturnType<typeof getArtifactsToUpdate> = []
if (!response) return returnArr
if (cmd.type === 'make_plane' && range[1] !== 0) {
// If we're calling `make_plane` and the code range doesn't end at `0`
// it's not a default plane, but a custom one from the offsetPlane standard library function
return [
{
id,
artifact: {
type: 'plane',
id,
pathIds: [],
codeRef: { range, pathToNode },
},
},
]
} else if (cmd.type === 'enable_sketch_mode') {
const plane = getArtifact(currentPlaneId)
const pathIds = plane?.type === 'plane' ? plane?.pathIds : []
const codeRef =
plane?.type === 'plane' ? plane?.codeRef : { range, pathToNode }
const existingPlane = getArtifact(currentPlaneId)
if (existingPlane?.type === 'wall') {
return [
{
id: currentPlaneId,
artifact: {
type: 'wall',
id: currentPlaneId,
segId: existingPlane.segId,
edgeCutEdgeIds: existingPlane.edgeCutEdgeIds,
sweepId: existingPlane.sweepId,
pathIds: existingPlane.pathIds,
},
},
]
} else {
return [
{
id: currentPlaneId,
artifact: { type: 'plane', id: currentPlaneId, pathIds, codeRef },
},
]
}
} else if (cmd.type === 'start_path') {
returnArr.push({
id,
artifact: {
type: 'path',
id,
segIds: [],
planeId: currentPlaneId,
sweepId: undefined,
codeRef: { range, pathToNode },
},
})
const plane = getArtifact(currentPlaneId)
const codeRef =
plane?.type === 'plane' ? plane?.codeRef : { range, pathToNode }
if (plane?.type === 'plane') {
returnArr.push({
id: currentPlaneId,
artifact: { type: 'plane', id: currentPlaneId, pathIds: [id], codeRef },
})
}
if (plane?.type === 'wall') {
returnArr.push({
id: currentPlaneId,
artifact: {
type: 'wall',
id: currentPlaneId,
segId: plane.segId,
edgeCutEdgeIds: plane.edgeCutEdgeIds,
sweepId: plane.sweepId,
pathIds: [id],
},
})
}
return returnArr
} else if (cmd.type === 'extend_path' || cmd.type === 'close_path') {
const pathId = cmd.type === 'extend_path' ? cmd.path : cmd.path_id
returnArr.push({
id,
artifact: {
type: 'segment',
id,
pathId,
surfaceId: undefined,
edgeIds: [],
codeRef: { range, pathToNode },
},
})
const path = getArtifact(pathId)
if (path?.type === 'path')
returnArr.push({
id: pathId,
artifact: { ...path, segIds: [id] },
})
if (
response?.type === 'modeling' &&
response.data.modeling_response.type === 'close_path'
) {
returnArr.push({
id: response.data.modeling_response.data.face_id,
artifact: {
type: 'solid2D',
id: response.data.modeling_response.data.face_id,
pathId,
},
})
const path = getArtifact(pathId)
if (path?.type === 'path')
returnArr.push({
id: pathId,
artifact: {
...path,
solid2dId: response.data.modeling_response.data.face_id,
},
})
}
return returnArr
} else if (
cmd.type === 'extrude' ||
cmd.type === 'revolve' ||
cmd.type === 'sweep'
) {
const subType = cmd.type === 'extrude' ? 'extrusion' : cmd.type
returnArr.push({
id,
artifact: {
type: 'sweep',
subType: subType,
id,
pathId: cmd.target,
surfaceIds: [],
edgeIds: [],
codeRef: { range, pathToNode },
},
})
const path = getArtifact(cmd.target)
if (path?.type === 'path')
returnArr.push({
id: cmd.target,
artifact: { ...path, sweepId: id },
})
return returnArr
} else if (
cmd.type === 'loft' &&
response.type === 'modeling' &&
response.data.modeling_response.type === 'loft'
) {
returnArr.push({
id,
artifact: {
type: 'sweep',
subType: 'loft',
id,
// TODO: make sure to revisit this choice, don't think it matters for now
pathId: cmd.section_ids[0],
surfaceIds: [],
edgeIds: [],
codeRef: { range, pathToNode },
},
})
for (const sectionId of cmd.section_ids) {
const path = getArtifact(sectionId)
if (path?.type === 'path')
returnArr.push({
id: sectionId,
artifact: { ...path, sweepId: id },
})
}
return returnArr
} else if (
cmd.type === 'solid3d_get_extrusion_face_info' &&
response?.type === 'modeling' &&
response.data.modeling_response.type === 'solid3d_get_extrusion_face_info'
) {
let lastPath: PathArtifact
response.data.modeling_response.data.faces.forEach(
({ curve_id, cap, face_id }) => {
if (cap === 'none' && curve_id && face_id) {
const seg = getArtifact(curve_id)
if (seg?.type !== 'segment') return
const path = getArtifact(seg.pathId)
if (path?.type === 'path' && seg?.type === 'segment') {
lastPath = path
returnArr.push({
id: face_id,
artifact: {
type: 'wall',
id: face_id,
segId: curve_id,
edgeCutEdgeIds: [],
// TODO: Add explicit check for sweepId. Should never use ''
sweepId: path.sweepId ?? '',
pathIds: [],
},
})
returnArr.push({
id: curve_id,
artifact: { ...seg, surfaceId: face_id },
})
if (path.sweepId) {
const sweep = getArtifact(path.sweepId)
if (sweep?.type === 'sweep') {
returnArr.push({
id: path.sweepId,
artifact: {
...sweep,
surfaceIds: [face_id],
},
})
}
}
}
}
}
)
response.data.modeling_response.data.faces.forEach(({ cap, face_id }) => {
if ((cap === 'top' || cap === 'bottom') && face_id) {
const path = lastPath
if (path?.type === 'path') {
returnArr.push({
id: face_id,
artifact: {
type: 'cap',
id: face_id,
subType: cap === 'bottom' ? 'start' : 'end',
edgeCutEdgeIds: [],
// TODO: Add explicit check for sweepId. Should never use ''
sweepId: path.sweepId ?? '',
pathIds: [],
},
})
if (path.sweepId) {
const sweep = getArtifact(path.sweepId)
if (sweep?.type !== 'sweep') return
returnArr.push({
id: path.sweepId,
artifact: {
...sweep,
surfaceIds: [face_id],
},
})
}
}
}
})
return returnArr
} else if (
// is opposite edge
(cmd.type === 'solid3d_get_opposite_edge' &&
response.type === 'modeling' &&
response.data.modeling_response.type === 'solid3d_get_opposite_edge' &&
response.data.modeling_response.data.edge) ||
// or is adjacent edge
(cmd.type === 'solid3d_get_next_adjacent_edge' &&
response.type === 'modeling' &&
response.data.modeling_response.type ===
'solid3d_get_next_adjacent_edge' &&
response.data.modeling_response.data.edge)
) {
const wall = getArtifact(cmd.face_id)
if (wall?.type !== 'wall') return returnArr
const sweep = getArtifact(wall.sweepId)
if (sweep?.type !== 'sweep') return returnArr
const path = getArtifact(sweep.pathId)
if (path?.type !== 'path') return returnArr
const segment = getArtifact(cmd.edge_id)
if (segment?.type !== 'segment') return returnArr
return [
{
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'
: 'opposite',
segId: cmd.edge_id,
// TODO: Add explicit check for sweepId. Should never use ''
sweepId: path.sweepId ?? '',
},
},
{
id: cmd.edge_id,
artifact: {
...segment,
edgeIds: [response.data.modeling_response.data.edge],
},
},
{
id: sweep.id,
artifact: {
...sweep,
edgeIds: [response.data.modeling_response.data.edge],
},
},
]
} else if (cmd.type === 'solid3d_fillet_edge') {
returnArr.push({
id,
artifact: {
type: 'edgeCut',
id,
subType: cmd.cut_type,
consumedEdgeId: cmd.edge_id,
edgeIds: [],
surfaceId: undefined,
codeRef: { range, pathToNode },
},
})
const consumedEdge = getArtifact(cmd.edge_id)
if (consumedEdge?.type === 'segment') {
returnArr.push({
id: cmd.edge_id,
artifact: { ...consumedEdge, edgeCutId: id },
})
}
return returnArr
}
return []
}
/** filter map items of a specific type */ /** filter map items of a specific type */
export function filterArtifacts<T extends Artifact['type'][]>( export function filterArtifacts<T extends Artifact['type'][]>(
{ {
@ -676,7 +166,7 @@ export function expandPath(
}, },
artifactGraph artifactGraph
) )
: undefined : null
const plane = getArtifactOfTypes( const plane = getArtifactOfTypes(
{ key: path.planeId, types: ['plane', 'wall'] }, { key: path.planeId, types: ['plane', 'wall'] },
artifactGraph artifactGraph
@ -778,11 +268,11 @@ export function getCapCodeRef(
} }
export function getSolid2dCodeRef( export function getSolid2dCodeRef(
solid2D: solid2D, solid2d: Solid2D,
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): CodeRef | Error { ): CodeRef | Error {
const path = getArtifactOfTypes( const path = getArtifactOfTypes(
{ key: solid2D.pathId, types: ['path'] }, { key: solid2d.pathId, types: ['path'] },
artifactGraph artifactGraph
) )
if (err(path)) return path if (err(path)) return path
@ -881,7 +371,7 @@ export function getCodeRefsByArtifactId(
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): Array<CodeRef> | null { ): Array<CodeRef> | null {
const artifact = artifactGraph.get(id) const artifact = artifactGraph.get(id)
if (artifact?.type === 'solid2D') { if (artifact?.type === 'solid2d') {
const codeRef = getSolid2dCodeRef(artifact, artifactGraph) const codeRef = getSolid2dCodeRef(artifact, artifactGraph)
if (err(codeRef)) return null if (err(codeRef)) return null
return [codeRef] return [codeRef]

View File

@ -1,9 +1,7 @@
import { import {
ArtifactCommand, ArtifactGraph,
defaultRustSourceRange, defaultSourceRange,
ExecState, ExecState,
Program,
RustSourceRange,
SourceRange, SourceRange,
} from 'lang/wasm' } from 'lang/wasm'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env' import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env'
@ -17,12 +15,7 @@ import {
darkModeMatcher, darkModeMatcher,
} from 'lib/theme' } from 'lib/theme'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes' import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import { import { EngineCommand, ResponseMap } from 'lang/std/artifactGraph'
ArtifactGraph,
EngineCommand,
ResponseMap,
createArtifactGraph,
} from 'lang/std/artifactGraph'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { exportMake } from 'lib/exportMake' import { exportMake } from 'lib/exportMake'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
@ -36,7 +29,6 @@ import { KclManager } from 'lang/KclSingleton'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
import { markOnce } from 'lib/performance' import { markOnce } from 'lib/performance'
import { MachineManager } from 'components/MachineManagerProvider' import { MachineManager } from 'components/MachineManagerProvider'
import { Node } from 'wasm-lib/kcl/bindings/Node'
// TODO(paultag): This ought to be tweakable. // TODO(paultag): This ought to be tweakable.
const pingIntervalMs = 5_000 const pingIntervalMs = 5_000
@ -1309,8 +1301,8 @@ export enum EngineCommandManagerEvents {
interface PendingMessage { interface PendingMessage {
command: EngineCommand command: EngineCommand
range: RustSourceRange range: SourceRange
idToRangeMap: { [key: string]: RustSourceRange } idToRangeMap: { [key: string]: SourceRange }
resolve: (data: [Models['WebSocketResponse_type']]) => void resolve: (data: [Models['WebSocketResponse_type']]) => void
reject: (reason: string) => void reject: (reason: string) => void
promise: Promise<[Models['WebSocketResponse_type']]> promise: Promise<[Models['WebSocketResponse_type']]>
@ -1994,7 +1986,7 @@ export class EngineCommandManager extends EventTarget {
{ {
command, command,
idToRangeMap: {}, idToRangeMap: {},
range: defaultRustSourceRange(), range: defaultSourceRange(),
}, },
true // isSceneCommand true // isSceneCommand
) )
@ -2025,9 +2017,9 @@ export class EngineCommandManager extends EventTarget {
return Promise.reject(new Error('rangeStr is undefined')) return Promise.reject(new Error('rangeStr is undefined'))
if (commandStr === undefined) if (commandStr === undefined)
return Promise.reject(new Error('commandStr is undefined')) return Promise.reject(new Error('commandStr is undefined'))
const range: RustSourceRange = JSON.parse(rangeStr) const range: SourceRange = JSON.parse(rangeStr)
const command: EngineCommand = JSON.parse(commandStr) const command: EngineCommand = JSON.parse(commandStr)
const idToRangeMap: { [key: string]: RustSourceRange } = const idToRangeMap: { [key: string]: SourceRange } =
JSON.parse(idToRangeStr) JSON.parse(idToRangeStr)
// Current executeAst is stale, going to interrupt, a new executeAst will trigger // Current executeAst is stale, going to interrupt, a new executeAst will trigger
@ -2087,17 +2079,8 @@ export class EngineCommandManager extends EventTarget {
Object.values(this.pendingCommands).map((a) => a.promise) Object.values(this.pendingCommands).map((a) => a.promise)
) )
} }
updateArtifactGraph( updateArtifactGraph(execStateArtifactGraph: ExecState['artifactGraph']) {
ast: Node<Program>, this.artifactGraph = execStateArtifactGraph
artifactCommands: ArtifactCommand[],
execStateArtifacts: ExecState['artifacts']
) {
this.artifactGraph = createArtifactGraph({
artifactCommands,
responseMap: this.responseMap,
ast,
execStateArtifacts,
})
// TODO check if these still need to be deferred once e2e tests are working again. // TODO check if these still need to be deferred once e2e tests are working again.
if (this.artifactGraph.size) { if (this.artifactGraph.size) {
this.deferredArtifactEmptied(null) this.deferredArtifactEmptied(null)

View File

@ -11,8 +11,8 @@ import {
assertParse, assertParse,
recast, recast,
initPromise, initPromise,
SourceRange,
CallExpression, CallExpression,
topLevelRange,
} from '../wasm' } from '../wasm'
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst' import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
import { enginelessExecutor } from '../../lib/testHelpers' import { enginelessExecutor } from '../../lib/testHelpers'
@ -124,7 +124,10 @@ describe('testing changeSketchArguments', () => {
execState.memory, execState.memory,
{ {
type: 'sourceRange', type: 'sourceRange',
sourceRange: [sourceStart, sourceStart + lineToChange.length, true], sourceRange: topLevelRange(
sourceStart,
sourceStart + lineToChange.length
),
}, },
{ {
type: 'straight-segment', type: 'straight-segment',
@ -219,11 +222,10 @@ describe('testing addTagForSketchOnFace', () => {
const ast = assertParse(code) const ast = assertParse(code)
await enginelessExecutor(ast) await enginelessExecutor(ast)
const sourceStart = code.indexOf(originalLine) const sourceStart = code.indexOf(originalLine)
const sourceRange: [number, number, boolean] = [ const sourceRange = topLevelRange(
sourceStart, sourceStart,
sourceStart + originalLine.length, sourceStart + originalLine.length
true, )
]
if (err(ast)) return ast if (err(ast)) return ast
const pathToNode = getNodePathFromSourceRange(ast, sourceRange) const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const sketchOnFaceRetVal = addTagForSketchOnFace( const sketchOnFaceRetVal = addTagForSketchOnFace(
@ -292,11 +294,10 @@ ${insertCode}
await enginelessExecutor(ast) await enginelessExecutor(ast)
const sourceStart = code.indexOf(originalChamfer) const sourceStart = code.indexOf(originalChamfer)
const extraChars = originalChamfer.indexOf('chamfer') const extraChars = originalChamfer.indexOf('chamfer')
const sourceRange: [number, number, boolean] = [ const sourceRange = topLevelRange(
sourceStart + extraChars, sourceStart + extraChars,
sourceStart + originalChamfer.length - extraChars, sourceStart + originalChamfer.length - extraChars
true, )
]
if (err(ast)) throw ast if (err(ast)) throw ast
const pathToNode = getNodePathFromSourceRange(ast, sourceRange) const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
@ -357,7 +358,6 @@ describe('testing getConstraintInfo', () => {
offset = 0 offset = 0
}, %) }, %)
|> tangentialArcTo([3.14, 13.14], %)` |> tangentialArcTo([3.14, 13.14], %)`
const ast = assertParse(code)
test.each([ test.each([
[ [
'line', 'line',
@ -366,7 +366,7 @@ describe('testing getConstraintInfo', () => {
type: 'xRelative', type: 'xRelative',
isConstrained: false, isConstrained: false,
value: '3', value: '3',
sourceRange: [78, 79, true], sourceRange: topLevelRange(78, 79),
argPosition: { type: 'arrayItem', index: 0 }, argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'line', stdLibFnName: 'line',
@ -375,7 +375,7 @@ describe('testing getConstraintInfo', () => {
type: 'yRelative', type: 'yRelative',
isConstrained: false, isConstrained: false,
value: '4', value: '4',
sourceRange: [81, 82, true], sourceRange: topLevelRange(81, 82),
argPosition: { type: 'arrayItem', index: 1 }, argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'line', stdLibFnName: 'line',
@ -389,7 +389,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle', type: 'angle',
isConstrained: false, isConstrained: false,
value: '3.14', value: '3.14',
sourceRange: [118, 122, true], sourceRange: topLevelRange(118, 122),
argPosition: { type: 'objectProperty', key: 'angle' }, argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLine', stdLibFnName: 'angledLine',
@ -398,7 +398,7 @@ describe('testing getConstraintInfo', () => {
type: 'length', type: 'length',
isConstrained: false, isConstrained: false,
value: '3.14', value: '3.14',
sourceRange: [137, 141, true], sourceRange: topLevelRange(137, 141),
argPosition: { type: 'objectProperty', key: 'length' }, argPosition: { type: 'objectProperty', key: 'length' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLine', stdLibFnName: 'angledLine',
@ -412,7 +412,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute', type: 'xAbsolute',
isConstrained: false, isConstrained: false,
value: '6.14', value: '6.14',
sourceRange: [164, 168, true], sourceRange: topLevelRange(164, 168),
argPosition: { type: 'arrayItem', index: 0 }, argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'lineTo', stdLibFnName: 'lineTo',
@ -421,7 +421,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute', type: 'yAbsolute',
isConstrained: false, isConstrained: false,
value: '3.14', value: '3.14',
sourceRange: [170, 174, true], sourceRange: topLevelRange(170, 174),
argPosition: { type: 'arrayItem', index: 1 }, argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'lineTo', stdLibFnName: 'lineTo',
@ -435,7 +435,7 @@ describe('testing getConstraintInfo', () => {
type: 'horizontal', type: 'horizontal',
isConstrained: true, isConstrained: true,
value: 'xLineTo', value: 'xLineTo',
sourceRange: [185, 192, true], sourceRange: topLevelRange(185, 192),
argPosition: undefined, argPosition: undefined,
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'xLineTo', stdLibFnName: 'xLineTo',
@ -444,7 +444,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute', type: 'xAbsolute',
isConstrained: false, isConstrained: false,
value: '8', value: '8',
sourceRange: [193, 194, true], sourceRange: topLevelRange(193, 194),
argPosition: { type: 'singleValue' }, argPosition: { type: 'singleValue' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'xLineTo', stdLibFnName: 'xLineTo',
@ -458,7 +458,7 @@ describe('testing getConstraintInfo', () => {
type: 'vertical', type: 'vertical',
isConstrained: true, isConstrained: true,
value: 'yLineTo', value: 'yLineTo',
sourceRange: [204, 211, true], sourceRange: topLevelRange(204, 211),
argPosition: undefined, argPosition: undefined,
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'yLineTo', stdLibFnName: 'yLineTo',
@ -467,7 +467,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute', type: 'yAbsolute',
isConstrained: false, isConstrained: false,
value: '5', value: '5',
sourceRange: [212, 213, true], sourceRange: topLevelRange(212, 213),
argPosition: { type: 'singleValue' }, argPosition: { type: 'singleValue' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'yLineTo', stdLibFnName: 'yLineTo',
@ -481,7 +481,7 @@ describe('testing getConstraintInfo', () => {
type: 'vertical', type: 'vertical',
isConstrained: true, isConstrained: true,
value: 'yLine', value: 'yLine',
sourceRange: [223, 228, true], sourceRange: topLevelRange(223, 228),
argPosition: undefined, argPosition: undefined,
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'yLine', stdLibFnName: 'yLine',
@ -490,7 +490,7 @@ describe('testing getConstraintInfo', () => {
type: 'yRelative', type: 'yRelative',
isConstrained: false, isConstrained: false,
value: '3.14', value: '3.14',
sourceRange: [229, 233, true], sourceRange: topLevelRange(229, 233),
argPosition: { type: 'singleValue' }, argPosition: { type: 'singleValue' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'yLine', stdLibFnName: 'yLine',
@ -504,7 +504,7 @@ describe('testing getConstraintInfo', () => {
type: 'horizontal', type: 'horizontal',
isConstrained: true, isConstrained: true,
value: 'xLine', value: 'xLine',
sourceRange: [247, 252, true], sourceRange: topLevelRange(247, 252),
argPosition: undefined, argPosition: undefined,
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'xLine', stdLibFnName: 'xLine',
@ -513,7 +513,7 @@ describe('testing getConstraintInfo', () => {
type: 'xRelative', type: 'xRelative',
isConstrained: false, isConstrained: false,
value: '3.14', value: '3.14',
sourceRange: [253, 257, true], sourceRange: topLevelRange(253, 257),
argPosition: { type: 'singleValue' }, argPosition: { type: 'singleValue' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'xLine', stdLibFnName: 'xLine',
@ -527,7 +527,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle', type: 'angle',
isConstrained: false, isConstrained: false,
value: '3.14', value: '3.14',
sourceRange: [301, 305, true], sourceRange: topLevelRange(301, 305),
argPosition: { type: 'objectProperty', key: 'angle' }, argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfXLength', stdLibFnName: 'angledLineOfXLength',
@ -536,7 +536,7 @@ describe('testing getConstraintInfo', () => {
type: 'xRelative', type: 'xRelative',
isConstrained: false, isConstrained: false,
value: '3.14', value: '3.14',
sourceRange: [320, 324, true], sourceRange: topLevelRange(320, 324),
argPosition: { type: 'objectProperty', key: 'length' }, argPosition: { type: 'objectProperty', key: 'length' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfXLength', stdLibFnName: 'angledLineOfXLength',
@ -550,7 +550,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle', type: 'angle',
isConstrained: false, isConstrained: false,
value: '30', value: '30',
sourceRange: [373, 375, true], sourceRange: topLevelRange(373, 375),
argPosition: { type: 'objectProperty', key: 'angle' }, argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfYLength', stdLibFnName: 'angledLineOfYLength',
@ -559,7 +559,7 @@ describe('testing getConstraintInfo', () => {
type: 'yRelative', type: 'yRelative',
isConstrained: false, isConstrained: false,
value: '3', value: '3',
sourceRange: [390, 391, true], sourceRange: topLevelRange(390, 391),
argPosition: { type: 'objectProperty', key: 'length' }, argPosition: { type: 'objectProperty', key: 'length' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfYLength', stdLibFnName: 'angledLineOfYLength',
@ -573,7 +573,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle', type: 'angle',
isConstrained: false, isConstrained: false,
value: '12.14', value: '12.14',
sourceRange: [434, 439, true], sourceRange: topLevelRange(434, 439),
argPosition: { type: 'objectProperty', key: 'angle' }, argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToX', stdLibFnName: 'angledLineToX',
@ -582,7 +582,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute', type: 'xAbsolute',
isConstrained: false, isConstrained: false,
value: '12', value: '12',
sourceRange: [450, 452, true], sourceRange: topLevelRange(450, 452),
argPosition: { type: 'objectProperty', key: 'to' }, argPosition: { type: 'objectProperty', key: 'to' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToX', stdLibFnName: 'angledLineToX',
@ -596,7 +596,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle', type: 'angle',
isConstrained: false, isConstrained: false,
value: '30', value: '30',
sourceRange: [495, 497, true], sourceRange: topLevelRange(495, 497),
argPosition: { type: 'objectProperty', key: 'angle' }, argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToY', stdLibFnName: 'angledLineToY',
@ -605,7 +605,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute', type: 'yAbsolute',
isConstrained: false, isConstrained: false,
value: '10.14', value: '10.14',
sourceRange: [508, 513, true], sourceRange: topLevelRange(508, 513),
argPosition: { type: 'objectProperty', key: 'to' }, argPosition: { type: 'objectProperty', key: 'to' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToY', stdLibFnName: 'angledLineToY',
@ -619,7 +619,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle', type: 'angle',
isConstrained: false, isConstrained: false,
value: '3.14', value: '3.14',
sourceRange: [567, 571, true], sourceRange: topLevelRange(567, 571),
argPosition: { type: 'objectProperty', key: 'angle' }, argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineThatIntersects', stdLibFnName: 'angledLineThatIntersects',
@ -628,7 +628,7 @@ describe('testing getConstraintInfo', () => {
type: 'intersectionOffset', type: 'intersectionOffset',
isConstrained: false, isConstrained: false,
value: '0', value: '0',
sourceRange: [608, 609, true], sourceRange: topLevelRange(608, 609),
argPosition: { type: 'objectProperty', key: 'offset' }, argPosition: { type: 'objectProperty', key: 'offset' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineThatIntersects', stdLibFnName: 'angledLineThatIntersects',
@ -637,7 +637,7 @@ describe('testing getConstraintInfo', () => {
type: 'intersectionTag', type: 'intersectionTag',
isConstrained: false, isConstrained: false,
value: 'a', value: 'a',
sourceRange: [592, 593, true], sourceRange: topLevelRange(592, 593),
argPosition: { argPosition: {
key: 'intersectTag', key: 'intersectTag',
type: 'objectProperty', type: 'objectProperty',
@ -654,7 +654,7 @@ describe('testing getConstraintInfo', () => {
type: 'tangentialWithPrevious', type: 'tangentialWithPrevious',
isConstrained: true, isConstrained: true,
value: 'tangentialArcTo', value: 'tangentialArcTo',
sourceRange: [623, 638, true], sourceRange: topLevelRange(623, 638),
argPosition: undefined, argPosition: undefined,
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'tangentialArcTo', stdLibFnName: 'tangentialArcTo',
@ -663,7 +663,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute', type: 'xAbsolute',
isConstrained: false, isConstrained: false,
value: '3.14', value: '3.14',
sourceRange: [640, 644, true], sourceRange: topLevelRange(640, 644),
argPosition: { type: 'arrayItem', index: 0 }, argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'tangentialArcTo', stdLibFnName: 'tangentialArcTo',
@ -672,7 +672,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute', type: 'yAbsolute',
isConstrained: false, isConstrained: false,
value: '13.14', value: '13.14',
sourceRange: [646, 651, true], sourceRange: topLevelRange(646, 651),
argPosition: { type: 'arrayItem', index: 1 }, argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'tangentialArcTo', stdLibFnName: 'tangentialArcTo',
@ -680,11 +680,11 @@ describe('testing getConstraintInfo', () => {
], ],
], ],
])('testing %s when inputs are unconstrained', (functionName, expected) => { ])('testing %s when inputs are unconstrained', (functionName, expected) => {
const sourceRange: SourceRange = [ const ast = assertParse(code)
const sourceRange = topLevelRange(
code.indexOf(functionName), code.indexOf(functionName),
code.indexOf(functionName) + functionName.length, code.indexOf(functionName) + functionName.length
true, )
]
if (err(ast)) return ast if (err(ast)) return ast
const pathToNode = getNodePathFromSourceRange(ast, sourceRange) const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const callExp = getNodeFromPath<Node<CallExpression>>( const callExp = getNodeFromPath<Node<CallExpression>>(
@ -717,7 +717,6 @@ describe('testing getConstraintInfo', () => {
offset = 0 offset = 0
}, %) }, %)
|> tangentialArcTo([3.14, 13.14], %)` |> tangentialArcTo([3.14, 13.14], %)`
const ast = assertParse(code)
test.each([ test.each([
[ [
`angledLine(`, `angledLine(`,
@ -726,7 +725,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle', type: 'angle',
isConstrained: false, isConstrained: false,
value: '3.14', value: '3.14',
sourceRange: [112, 116, true], sourceRange: topLevelRange(112, 116),
argPosition: { type: 'arrayItem', index: 0 }, argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLine', stdLibFnName: 'angledLine',
@ -735,7 +734,7 @@ describe('testing getConstraintInfo', () => {
type: 'length', type: 'length',
isConstrained: false, isConstrained: false,
value: '3.14', value: '3.14',
sourceRange: [118, 122, true], sourceRange: topLevelRange(118, 122),
argPosition: { type: 'arrayItem', index: 1 }, argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLine', stdLibFnName: 'angledLine',
@ -749,7 +748,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle', type: 'angle',
isConstrained: false, isConstrained: false,
value: '3.14', value: '3.14',
sourceRange: [277, 281, true], sourceRange: topLevelRange(277, 281),
argPosition: { type: 'arrayItem', index: 0 }, argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfXLength', stdLibFnName: 'angledLineOfXLength',
@ -758,7 +757,7 @@ describe('testing getConstraintInfo', () => {
type: 'xRelative', type: 'xRelative',
isConstrained: false, isConstrained: false,
value: '3.14', value: '3.14',
sourceRange: [283, 287, true], sourceRange: topLevelRange(283, 287),
argPosition: { type: 'arrayItem', index: 1 }, argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfXLength', stdLibFnName: 'angledLineOfXLength',
@ -772,7 +771,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle', type: 'angle',
isConstrained: false, isConstrained: false,
value: '30', value: '30',
sourceRange: [321, 323, true], sourceRange: topLevelRange(321, 323),
argPosition: { type: 'arrayItem', index: 0 }, argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfYLength', stdLibFnName: 'angledLineOfYLength',
@ -781,7 +780,7 @@ describe('testing getConstraintInfo', () => {
type: 'yRelative', type: 'yRelative',
isConstrained: false, isConstrained: false,
value: '3', value: '3',
sourceRange: [325, 326, true], sourceRange: topLevelRange(325, 326),
argPosition: { type: 'arrayItem', index: 1 }, argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfYLength', stdLibFnName: 'angledLineOfYLength',
@ -795,7 +794,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle', type: 'angle',
isConstrained: false, isConstrained: false,
value: '12', value: '12',
sourceRange: [354, 356, true], sourceRange: topLevelRange(354, 356),
argPosition: { type: 'arrayItem', index: 0 }, argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToX', stdLibFnName: 'angledLineToX',
@ -804,7 +803,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute', type: 'xAbsolute',
isConstrained: false, isConstrained: false,
value: '12', value: '12',
sourceRange: [358, 360, true], sourceRange: topLevelRange(358, 360),
argPosition: { type: 'arrayItem', index: 1 }, argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToX', stdLibFnName: 'angledLineToX',
@ -818,7 +817,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle', type: 'angle',
isConstrained: false, isConstrained: false,
value: '30', value: '30',
sourceRange: [388, 390, true], sourceRange: topLevelRange(388, 390),
argPosition: { type: 'arrayItem', index: 0 }, argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToY', stdLibFnName: 'angledLineToY',
@ -827,7 +826,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute', type: 'yAbsolute',
isConstrained: false, isConstrained: false,
value: '10', value: '10',
sourceRange: [392, 394, true], sourceRange: topLevelRange(392, 394),
argPosition: { type: 'arrayItem', index: 1 }, argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToY', stdLibFnName: 'angledLineToY',
@ -835,11 +834,11 @@ describe('testing getConstraintInfo', () => {
], ],
], ],
])('testing %s when inputs are unconstrained', (functionName, expected) => { ])('testing %s when inputs are unconstrained', (functionName, expected) => {
const sourceRange: SourceRange = [ const ast = assertParse(code)
const sourceRange = topLevelRange(
code.indexOf(functionName), code.indexOf(functionName),
code.indexOf(functionName) + functionName.length, code.indexOf(functionName) + functionName.length
true, )
]
if (err(ast)) return ast if (err(ast)) return ast
const pathToNode = getNodePathFromSourceRange(ast, sourceRange) const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const callExp = getNodeFromPath<Node<CallExpression>>( const callExp = getNodeFromPath<Node<CallExpression>>(
@ -872,7 +871,6 @@ describe('testing getConstraintInfo', () => {
offset = 0 + 0 offset = 0 + 0
}, %) }, %)
|> tangentialArcTo([3.14 + 0, 13.14 + 0], %)` |> tangentialArcTo([3.14 + 0, 13.14 + 0], %)`
const ast = assertParse(code)
test.each([ test.each([
[ [
'line', 'line',
@ -881,7 +879,7 @@ describe('testing getConstraintInfo', () => {
type: 'xRelative', type: 'xRelative',
isConstrained: true, isConstrained: true,
value: '3 + 0', value: '3 + 0',
sourceRange: [83, 88, true], sourceRange: topLevelRange(83, 88),
argPosition: { type: 'arrayItem', index: 0 }, argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'line', stdLibFnName: 'line',
@ -890,7 +888,7 @@ describe('testing getConstraintInfo', () => {
type: 'yRelative', type: 'yRelative',
isConstrained: true, isConstrained: true,
value: '4 + 0', value: '4 + 0',
sourceRange: [90, 95, true], sourceRange: topLevelRange(90, 95),
argPosition: { type: 'arrayItem', index: 1 }, argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'line', stdLibFnName: 'line',
@ -904,7 +902,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle', type: 'angle',
isConstrained: true, isConstrained: true,
value: '3.14 + 0', value: '3.14 + 0',
sourceRange: [129, 137, true], sourceRange: topLevelRange(129, 137),
argPosition: { type: 'objectProperty', key: 'angle' }, argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLine', stdLibFnName: 'angledLine',
@ -913,7 +911,7 @@ describe('testing getConstraintInfo', () => {
type: 'length', type: 'length',
isConstrained: true, isConstrained: true,
value: '3.14 + 0', value: '3.14 + 0',
sourceRange: [148, 156, true], sourceRange: topLevelRange(148, 156),
argPosition: { type: 'objectProperty', key: 'length' }, argPosition: { type: 'objectProperty', key: 'length' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLine', stdLibFnName: 'angledLine',
@ -927,7 +925,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute', type: 'xAbsolute',
isConstrained: true, isConstrained: true,
value: '6.14 + 0', value: '6.14 + 0',
sourceRange: [178, 186, true], sourceRange: topLevelRange(178, 186),
argPosition: { type: 'arrayItem', index: 0 }, argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'lineTo', stdLibFnName: 'lineTo',
@ -936,7 +934,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute', type: 'yAbsolute',
isConstrained: true, isConstrained: true,
value: '3.14 + 0', value: '3.14 + 0',
sourceRange: [188, 196, true], sourceRange: topLevelRange(188, 196),
argPosition: { type: 'arrayItem', index: 1 }, argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'lineTo', stdLibFnName: 'lineTo',
@ -950,7 +948,7 @@ describe('testing getConstraintInfo', () => {
type: 'horizontal', type: 'horizontal',
isConstrained: true, isConstrained: true,
value: 'xLineTo', value: 'xLineTo',
sourceRange: [209, 216, true], sourceRange: topLevelRange(209, 216),
argPosition: undefined, argPosition: undefined,
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'xLineTo', stdLibFnName: 'xLineTo',
@ -959,7 +957,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute', type: 'xAbsolute',
isConstrained: true, isConstrained: true,
value: '8 + 0', value: '8 + 0',
sourceRange: [217, 222, true], sourceRange: topLevelRange(217, 222),
argPosition: { type: 'singleValue' }, argPosition: { type: 'singleValue' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'xLineTo', stdLibFnName: 'xLineTo',
@ -973,7 +971,7 @@ describe('testing getConstraintInfo', () => {
type: 'vertical', type: 'vertical',
isConstrained: true, isConstrained: true,
value: 'yLineTo', value: 'yLineTo',
sourceRange: [234, 241, true], sourceRange: topLevelRange(234, 241),
argPosition: undefined, argPosition: undefined,
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'yLineTo', stdLibFnName: 'yLineTo',
@ -982,7 +980,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute', type: 'yAbsolute',
isConstrained: true, isConstrained: true,
value: '5 + 0', value: '5 + 0',
sourceRange: [242, 247, true], sourceRange: topLevelRange(242, 247),
argPosition: { type: 'singleValue' }, argPosition: { type: 'singleValue' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'yLineTo', stdLibFnName: 'yLineTo',
@ -996,7 +994,7 @@ describe('testing getConstraintInfo', () => {
type: 'vertical', type: 'vertical',
isConstrained: true, isConstrained: true,
value: 'yLine', value: 'yLine',
sourceRange: [259, 264, true], sourceRange: topLevelRange(259, 264),
argPosition: undefined, argPosition: undefined,
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'yLine', stdLibFnName: 'yLine',
@ -1005,7 +1003,7 @@ describe('testing getConstraintInfo', () => {
type: 'yRelative', type: 'yRelative',
isConstrained: true, isConstrained: true,
value: '3.14 + 0', value: '3.14 + 0',
sourceRange: [265, 273, true], sourceRange: topLevelRange(265, 273),
argPosition: { type: 'singleValue' }, argPosition: { type: 'singleValue' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'yLine', stdLibFnName: 'yLine',
@ -1019,7 +1017,7 @@ describe('testing getConstraintInfo', () => {
type: 'horizontal', type: 'horizontal',
isConstrained: true, isConstrained: true,
value: 'xLine', value: 'xLine',
sourceRange: [289, 294, true], sourceRange: topLevelRange(289, 294),
argPosition: undefined, argPosition: undefined,
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'xLine', stdLibFnName: 'xLine',
@ -1028,7 +1026,7 @@ describe('testing getConstraintInfo', () => {
type: 'xRelative', type: 'xRelative',
isConstrained: true, isConstrained: true,
value: '3.14 + 0', value: '3.14 + 0',
sourceRange: [295, 303, true], sourceRange: topLevelRange(295, 303),
argPosition: { type: 'singleValue' }, argPosition: { type: 'singleValue' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'xLine', stdLibFnName: 'xLine',
@ -1042,7 +1040,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle', type: 'angle',
isConstrained: true, isConstrained: true,
value: '3.14 + 0', value: '3.14 + 0',
sourceRange: [345, 353, true], sourceRange: topLevelRange(345, 353),
argPosition: { type: 'objectProperty', key: 'angle' }, argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfXLength', stdLibFnName: 'angledLineOfXLength',
@ -1051,7 +1049,7 @@ describe('testing getConstraintInfo', () => {
type: 'xRelative', type: 'xRelative',
isConstrained: true, isConstrained: true,
value: '3.14 + 0', value: '3.14 + 0',
sourceRange: [364, 372, true], sourceRange: topLevelRange(364, 372),
argPosition: { type: 'objectProperty', key: 'length' }, argPosition: { type: 'objectProperty', key: 'length' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfXLength', stdLibFnName: 'angledLineOfXLength',
@ -1065,7 +1063,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle', type: 'angle',
isConstrained: true, isConstrained: true,
value: '30 + 0', value: '30 + 0',
sourceRange: [416, 422, true], sourceRange: topLevelRange(416, 422),
argPosition: { type: 'objectProperty', key: 'angle' }, argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfYLength', stdLibFnName: 'angledLineOfYLength',
@ -1074,7 +1072,7 @@ describe('testing getConstraintInfo', () => {
type: 'yRelative', type: 'yRelative',
isConstrained: true, isConstrained: true,
value: '3 + 0', value: '3 + 0',
sourceRange: [433, 438, true], sourceRange: topLevelRange(433, 438),
argPosition: { type: 'objectProperty', key: 'length' }, argPosition: { type: 'objectProperty', key: 'length' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineOfYLength', stdLibFnName: 'angledLineOfYLength',
@ -1088,7 +1086,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle', type: 'angle',
isConstrained: true, isConstrained: true,
value: '12.14 + 0', value: '12.14 + 0',
sourceRange: [476, 485, true], sourceRange: topLevelRange(476, 485),
argPosition: { type: 'objectProperty', key: 'angle' }, argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToX', stdLibFnName: 'angledLineToX',
@ -1097,7 +1095,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute', type: 'xAbsolute',
isConstrained: true, isConstrained: true,
value: '12 + 0', value: '12 + 0',
sourceRange: [492, 498, true], sourceRange: topLevelRange(492, 498),
argPosition: { type: 'objectProperty', key: 'to' }, argPosition: { type: 'objectProperty', key: 'to' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToX', stdLibFnName: 'angledLineToX',
@ -1111,7 +1109,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle', type: 'angle',
isConstrained: true, isConstrained: true,
value: '30 + 0', value: '30 + 0',
sourceRange: [536, 542, true], sourceRange: topLevelRange(536, 542),
argPosition: { type: 'objectProperty', key: 'angle' }, argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToY', stdLibFnName: 'angledLineToY',
@ -1120,7 +1118,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute', type: 'yAbsolute',
isConstrained: true, isConstrained: true,
value: '10.14 + 0', value: '10.14 + 0',
sourceRange: [549, 558, true], sourceRange: topLevelRange(549, 558),
argPosition: { type: 'objectProperty', key: 'to' }, argPosition: { type: 'objectProperty', key: 'to' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineToY', stdLibFnName: 'angledLineToY',
@ -1134,7 +1132,7 @@ describe('testing getConstraintInfo', () => {
type: 'angle', type: 'angle',
isConstrained: true, isConstrained: true,
value: '3.14 + 0', value: '3.14 + 0',
sourceRange: [616, 624, true], sourceRange: topLevelRange(616, 624),
argPosition: { type: 'objectProperty', key: 'angle' }, argPosition: { type: 'objectProperty', key: 'angle' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineThatIntersects', stdLibFnName: 'angledLineThatIntersects',
@ -1143,7 +1141,7 @@ describe('testing getConstraintInfo', () => {
type: 'intersectionOffset', type: 'intersectionOffset',
isConstrained: true, isConstrained: true,
value: '0 + 0', value: '0 + 0',
sourceRange: [671, 676, true], sourceRange: topLevelRange(671, 676),
argPosition: { type: 'objectProperty', key: 'offset' }, argPosition: { type: 'objectProperty', key: 'offset' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineThatIntersects', stdLibFnName: 'angledLineThatIntersects',
@ -1152,7 +1150,7 @@ describe('testing getConstraintInfo', () => {
type: 'intersectionTag', type: 'intersectionTag',
isConstrained: false, isConstrained: false,
value: 'a', value: 'a',
sourceRange: [650, 651, true], sourceRange: topLevelRange(650, 651),
argPosition: { key: 'intersectTag', type: 'objectProperty' }, argPosition: { key: 'intersectTag', type: 'objectProperty' },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'angledLineThatIntersects', stdLibFnName: 'angledLineThatIntersects',
@ -1166,7 +1164,7 @@ describe('testing getConstraintInfo', () => {
type: 'tangentialWithPrevious', type: 'tangentialWithPrevious',
isConstrained: true, isConstrained: true,
value: 'tangentialArcTo', value: 'tangentialArcTo',
sourceRange: [697, 712, true], sourceRange: topLevelRange(697, 712),
argPosition: undefined, argPosition: undefined,
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'tangentialArcTo', stdLibFnName: 'tangentialArcTo',
@ -1175,7 +1173,7 @@ describe('testing getConstraintInfo', () => {
type: 'xAbsolute', type: 'xAbsolute',
isConstrained: true, isConstrained: true,
value: '3.14 + 0', value: '3.14 + 0',
sourceRange: [714, 722, true], sourceRange: topLevelRange(714, 722),
argPosition: { type: 'arrayItem', index: 0 }, argPosition: { type: 'arrayItem', index: 0 },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'tangentialArcTo', stdLibFnName: 'tangentialArcTo',
@ -1184,7 +1182,7 @@ describe('testing getConstraintInfo', () => {
type: 'yAbsolute', type: 'yAbsolute',
isConstrained: true, isConstrained: true,
value: '13.14 + 0', value: '13.14 + 0',
sourceRange: [724, 733, true], sourceRange: topLevelRange(724, 733),
argPosition: { type: 'arrayItem', index: 1 }, argPosition: { type: 'arrayItem', index: 1 },
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'tangentialArcTo', stdLibFnName: 'tangentialArcTo',
@ -1192,11 +1190,11 @@ describe('testing getConstraintInfo', () => {
], ],
], ],
])('testing %s when inputs are unconstrained', (functionName, expected) => { ])('testing %s when inputs are unconstrained', (functionName, expected) => {
const sourceRange: SourceRange = [ const ast = assertParse(code)
const sourceRange = topLevelRange(
code.indexOf(functionName), code.indexOf(functionName),
code.indexOf(functionName) + functionName.length, code.indexOf(functionName) + functionName.length
true, )
]
if (err(ast)) return ast if (err(ast)) return ast
const pathToNode = getNodePathFromSourceRange(ast, sourceRange) const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const callExp = getNodeFromPath<Node<CallExpression>>( const callExp = getNodeFromPath<Node<CallExpression>>(

View File

@ -12,6 +12,7 @@ import {
VariableDeclaration, VariableDeclaration,
Identifier, Identifier,
sketchFromKclValue, sketchFromKclValue,
topLevelRange,
} from 'lang/wasm' } from 'lang/wasm'
import { import {
getNodeFromPath, getNodeFromPath,
@ -222,7 +223,7 @@ const commonConstraintInfoHelper = (
code.slice(input1.start, input1.end), code.slice(input1.start, input1.end),
stdLibFnName, stdLibFnName,
isArr ? abbreviatedInputs[0].arrayInput : abbreviatedInputs[0].objInput, isArr ? abbreviatedInputs[0].arrayInput : abbreviatedInputs[0].objInput,
[input1.start, input1.end, true], topLevelRange(input1.start, input1.end),
pathToFirstArg pathToFirstArg
) )
) )
@ -234,7 +235,7 @@ const commonConstraintInfoHelper = (
code.slice(input2.start, input2.end), code.slice(input2.start, input2.end),
stdLibFnName, stdLibFnName,
isArr ? abbreviatedInputs[1].arrayInput : abbreviatedInputs[1].objInput, isArr ? abbreviatedInputs[1].arrayInput : abbreviatedInputs[1].objInput,
[input2.start, input2.end, true], topLevelRange(input2.start, input2.end),
pathToSecondArg pathToSecondArg
) )
) )
@ -266,7 +267,7 @@ const horzVertConstraintInfoHelper = (
callee.name, callee.name,
stdLibFnName, stdLibFnName,
undefined, undefined,
[callee.start, callee.end, true], topLevelRange(callee.start, callee.end),
pathToCallee pathToCallee
), ),
constrainInfo( constrainInfo(
@ -275,7 +276,7 @@ const horzVertConstraintInfoHelper = (
code.slice(firstArg.start, firstArg.end), code.slice(firstArg.start, firstArg.end),
stdLibFnName, stdLibFnName,
abbreviatedInput, abbreviatedInput,
[firstArg.start, firstArg.end, true], topLevelRange(firstArg.start, firstArg.end),
pathToFirstArg pathToFirstArg
), ),
] ]
@ -905,7 +906,7 @@ export const tangentialArcTo: SketchLineHelper = {
callee.name, callee.name,
'tangentialArcTo', 'tangentialArcTo',
undefined, undefined,
[callee.start, callee.end, true], topLevelRange(callee.start, callee.end),
pathToCallee pathToCallee
), ),
constrainInfo( constrainInfo(
@ -914,7 +915,7 @@ export const tangentialArcTo: SketchLineHelper = {
code.slice(firstArg.elements[0].start, firstArg.elements[0].end), code.slice(firstArg.elements[0].start, firstArg.elements[0].end),
'tangentialArcTo', 'tangentialArcTo',
0, 0,
[firstArg.elements[0].start, firstArg.elements[0].end, true], topLevelRange(firstArg.elements[0].start, firstArg.elements[0].end),
pathToFirstArg pathToFirstArg
), ),
constrainInfo( constrainInfo(
@ -923,7 +924,7 @@ export const tangentialArcTo: SketchLineHelper = {
code.slice(firstArg.elements[1].start, firstArg.elements[1].end), code.slice(firstArg.elements[1].start, firstArg.elements[1].end),
'tangentialArcTo', 'tangentialArcTo',
1, 1,
[firstArg.elements[1].start, firstArg.elements[1].end, true], topLevelRange(firstArg.elements[1].start, firstArg.elements[1].end),
pathToSecondArg pathToSecondArg
), ),
] ]
@ -1052,7 +1053,7 @@ export const circle: SketchLineHelper = {
code.slice(radiusDetails.expr.start, radiusDetails.expr.end), code.slice(radiusDetails.expr.start, radiusDetails.expr.end),
'circle', 'circle',
'radius', 'radius',
[radiusDetails.expr.start, radiusDetails.expr.end, true], topLevelRange(radiusDetails.expr.start, radiusDetails.expr.end),
pathToRadiusLiteral pathToRadiusLiteral
), ),
{ {
@ -1061,11 +1062,10 @@ export const circle: SketchLineHelper = {
isConstrained: isNotLiteralArrayOrStatic( isConstrained: isNotLiteralArrayOrStatic(
centerDetails.expr.elements[0] centerDetails.expr.elements[0]
), ),
sourceRange: [ sourceRange: topLevelRange(
centerDetails.expr.elements[0].start, centerDetails.expr.elements[0].start,
centerDetails.expr.elements[0].end, centerDetails.expr.elements[0].end
true, ),
],
pathToNode: pathToXArg, pathToNode: pathToXArg,
value: code.slice( value: code.slice(
centerDetails.expr.elements[0].start, centerDetails.expr.elements[0].start,
@ -1083,11 +1083,10 @@ export const circle: SketchLineHelper = {
isConstrained: isNotLiteralArrayOrStatic( isConstrained: isNotLiteralArrayOrStatic(
centerDetails.expr.elements[1] centerDetails.expr.elements[1]
), ),
sourceRange: [ sourceRange: topLevelRange(
centerDetails.expr.elements[1].start, centerDetails.expr.elements[1].start,
centerDetails.expr.elements[1].end, centerDetails.expr.elements[1].end
true, ),
],
pathToNode: pathToYArg, pathToNode: pathToYArg,
value: code.slice( value: code.slice(
centerDetails.expr.elements[1].start, centerDetails.expr.elements[1].start,
@ -1763,7 +1762,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
code.slice(angle.start, angle.end), code.slice(angle.start, angle.end),
'angledLineThatIntersects', 'angledLineThatIntersects',
'angle', 'angle',
[angle.start, angle.end, true], topLevelRange(angle.start, angle.end),
pathToAngleProp pathToAngleProp
) )
) )
@ -1782,7 +1781,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
code.slice(offset.start, offset.end), code.slice(offset.start, offset.end),
'angledLineThatIntersects', 'angledLineThatIntersects',
'offset', 'offset',
[offset.start, offset.end, true], topLevelRange(offset.start, offset.end),
pathToOffsetProp pathToOffsetProp
) )
) )
@ -1801,7 +1800,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
code.slice(tag.start, tag.end), code.slice(tag.start, tag.end),
'angledLineThatIntersects', 'angledLineThatIntersects',
'intersectTag', 'intersectTag',
[tag.start, tag.end, true], topLevelRange(tag.start, tag.end),
pathToTagProp pathToTagProp
) )
returnVal.push(info) returnVal.push(info)

View File

@ -5,6 +5,7 @@ import {
initPromise, initPromise,
sketchFromKclValue, sketchFromKclValue,
SourceRange, SourceRange,
topLevelRange,
} from '../wasm' } from '../wasm'
import { import {
ConstraintType, ConstraintType,
@ -31,10 +32,10 @@ async function testingSwapSketchFnCall({
constraintType: ConstraintType constraintType: ConstraintType
}): Promise<{ }): Promise<{
newCode: string newCode: string
originalRange: [number, number, boolean] originalRange: SourceRange
}> { }> {
const startIndex = inputCode.indexOf(callToSwap) const startIndex = inputCode.indexOf(callToSwap)
const range: SourceRange = [startIndex, startIndex + callToSwap.length, true] const range = topLevelRange(startIndex, startIndex + callToSwap.length)
const ast = assertParse(inputCode) const ast = assertParse(inputCode)
const execState = await enginelessExecutor(ast) const execState = await enginelessExecutor(ast)
@ -375,7 +376,10 @@ part001 = startSketchOn('XY')
execState.memory.get('part001'), execState.memory.get('part001'),
'part001' 'part001'
) as Sketch ) as Sketch
const _segment = getSketchSegmentFromSourceRange(sg, [index, index, true]) const _segment = getSketchSegmentFromSourceRange(
sg,
topLevelRange(index, index)
)
if (err(_segment)) throw _segment if (err(_segment)) throw _segment
const { __geoMeta, ...segment } = _segment.segment const { __geoMeta, ...segment } = _segment.segment
expect(segment).toEqual({ expect(segment).toEqual({
@ -390,7 +394,7 @@ part001 = startSketchOn('XY')
const index = code.indexOf('// segment-in-start') - 7 const index = code.indexOf('// segment-in-start') - 7
const _segment = getSketchSegmentFromSourceRange( const _segment = getSketchSegmentFromSourceRange(
sketchFromKclValue(execState.memory.get('part001'), 'part001') as Sketch, sketchFromKclValue(execState.memory.get('part001'), 'part001') as Sketch,
[index, index, true] topLevelRange(index, index)
) )
if (err(_segment)) throw _segment if (err(_segment)) throw _segment
const { __geoMeta, ...segment } = _segment.segment const { __geoMeta, ...segment } = _segment.segment

View File

@ -9,6 +9,7 @@ import {
Path, Path,
PathToNode, PathToNode,
Expr, Expr,
topLevelRange,
} from '../wasm' } from '../wasm'
import { err } from 'lib/trap' import { err } from 'lib/trap'
@ -31,7 +32,7 @@ export function getSketchSegmentFromPathToNode(
const node = nodeMeta.node const node = nodeMeta.node
if (!node || typeof node.start !== 'number' || !node.end) if (!node || typeof node.start !== 'number' || !node.end)
return new Error('no node found') return new Error('no node found')
const sourceRange: SourceRange = [node.start, node.end, true] const sourceRange = topLevelRange(node.start, node.end)
return getSketchSegmentFromSourceRange(sketch, sourceRange) return getSketchSegmentFromSourceRange(sketch, sourceRange)
} }
export function getSketchSegmentFromSourceRange( export function getSketchSegmentFromSourceRange(

View File

@ -1,4 +1,11 @@
import { assertParse, Expr, recast, initPromise, Program } from '../wasm' import {
assertParse,
Expr,
recast,
initPromise,
Program,
topLevelRange,
} from '../wasm'
import { import {
getConstraintType, getConstraintType,
getTransformInfos, getTransformInfos,
@ -125,7 +132,7 @@ describe('testing transformAstForSketchLines for equal length constraint', () =>
) )
} }
const start = codeBeforeLine + line.indexOf('|> ' + 5) const start = codeBeforeLine + line.indexOf('|> ' + 5)
const range: [number, number, boolean] = [start, start, true] const range = topLevelRange(start, start)
return { return {
codeRef: codeRefFromRange(range, ast), codeRef: codeRefFromRange(range, ast),
} }
@ -297,7 +304,7 @@ part001 = startSketchOn('XY')
const comment = ln.split('//')[1] const comment = ln.split('//')[1]
const start = inputScript.indexOf('//' + comment) - 7 const start = inputScript.indexOf('//' + comment) - 7
return { return {
codeRef: codeRefFromRange([start, start, true], ast), codeRef: codeRefFromRange(topLevelRange(start, start), ast),
} }
}) })
@ -386,7 +393,7 @@ part001 = startSketchOn('XY')
const comment = ln.split('//')[1] const comment = ln.split('//')[1]
const start = inputScript.indexOf('//' + comment) - 7 const start = inputScript.indexOf('//' + comment) - 7
return { return {
codeRef: codeRefFromRange([start, start, true], ast), codeRef: codeRefFromRange(topLevelRange(start, start), ast),
} }
}) })
@ -446,7 +453,7 @@ part001 = startSketchOn('XY')
const comment = ln.split('//')[1] const comment = ln.split('//')[1]
const start = inputScript.indexOf('//' + comment) - 7 const start = inputScript.indexOf('//' + comment) - 7
return { return {
codeRef: codeRefFromRange([start, start, true], ast), codeRef: codeRefFromRange(topLevelRange(start, start), ast),
} }
}) })
@ -541,7 +548,7 @@ async function helperThing(
const comment = ln.split('//')[1] const comment = ln.split('//')[1]
const start = inputScript.indexOf('//' + comment) - 7 const start = inputScript.indexOf('//' + comment) - 7
return { return {
codeRef: codeRefFromRange([start, start, true], ast), codeRef: codeRefFromRange(topLevelRange(start, start), ast),
} }
}) })
@ -610,7 +617,7 @@ part001 = startSketchOn('XY')
} }
const offsetIndex = index - 7 const offsetIndex = index - 7
const expectedConstraintLevel = getConstraintLevelFromSourceRange( const expectedConstraintLevel = getConstraintLevelFromSourceRange(
[offsetIndex, offsetIndex, true], topLevelRange(offsetIndex, offsetIndex),
ast ast
) )
if (err(expectedConstraintLevel)) { if (err(expectedConstraintLevel)) {

View File

@ -5,8 +5,9 @@ import {
Literal, Literal,
ArrayExpression, ArrayExpression,
BinaryExpression, BinaryExpression,
ArtifactGraph,
} from './wasm' } from './wasm'
import { ArtifactGraph, filterArtifacts } from 'lang/std/artifactGraph' import { filterArtifacts } from 'lang/std/artifactGraph'
import { isOverlap } from 'lib/utils' import { isOverlap } from 'lib/utils'
export function updatePathToNodeFromMap( export function updatePathToNodeFromMap(

View File

@ -44,17 +44,30 @@ import { EnvironmentRef } from '../wasm-lib/kcl/bindings/EnvironmentRef'
import { Environment } from '../wasm-lib/kcl/bindings/Environment' import { Environment } from '../wasm-lib/kcl/bindings/Environment'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError' import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError'
import { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange' import { SourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
import { getAllCurrentSettings } from 'lib/settings/settingsUtils' import { getAllCurrentSettings } from 'lib/settings/settingsUtils'
import { Operation } from 'wasm-lib/kcl/bindings/Operation' import { Operation } from 'wasm-lib/kcl/bindings/Operation'
import { KclErrorWithOutputs } from 'wasm-lib/kcl/bindings/KclErrorWithOutputs' import { KclErrorWithOutputs } from 'wasm-lib/kcl/bindings/KclErrorWithOutputs'
import { Artifact } from 'wasm-lib/kcl/bindings/Artifact' import { Artifact as RustArtifact } from 'wasm-lib/kcl/bindings/Artifact'
import { ArtifactId } from 'wasm-lib/kcl/bindings/ArtifactId' import { ArtifactId } from 'wasm-lib/kcl/bindings/Artifact'
import { ArtifactCommand } from 'wasm-lib/kcl/bindings/ArtifactCommand' import { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
import { ArtifactGraph as RustArtifactGraph } from 'wasm-lib/kcl/bindings/Artifact'
import { Artifact } from './std/artifactGraph'
import { getNodePathFromSourceRange } from './queryAst'
export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact' export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/ArtifactCommand' export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
export type { ArtifactId } from 'wasm-lib/kcl/bindings/ArtifactId' export type { ArtifactId } from 'wasm-lib/kcl/bindings/Artifact'
export type { Cap as CapArtifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { CodeRef } from 'wasm-lib/kcl/bindings/Artifact'
export type { EdgeCut } from 'wasm-lib/kcl/bindings/Artifact'
export type { Path as PathArtifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { Plane as PlaneArtifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { Segment as SegmentArtifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { Solid2d as Solid2dArtifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { Sweep as SweepArtifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { SweepEdge } from 'wasm-lib/kcl/bindings/Artifact'
export type { Wall as WallArtifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { Configuration } from 'wasm-lib/kcl/bindings/Configuration' export type { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
export type { Program } from '../wasm-lib/kcl/bindings/Program' export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Expr } from '../wasm-lib/kcl/bindings/Expr' export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
@ -76,7 +89,7 @@ export type { BinaryPart } from '../wasm-lib/kcl/bindings/BinaryPart'
export type { Literal } from '../wasm-lib/kcl/bindings/Literal' export type { Literal } from '../wasm-lib/kcl/bindings/Literal'
export type { LiteralValue } from '../wasm-lib/kcl/bindings/LiteralValue' export type { LiteralValue } from '../wasm-lib/kcl/bindings/LiteralValue'
export type { ArrayExpression } from '../wasm-lib/kcl/bindings/ArrayExpression' export type { ArrayExpression } from '../wasm-lib/kcl/bindings/ArrayExpression'
export type { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange' export type { SourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
export type SyntaxType = export type SyntaxType =
| 'Program' | 'Program'
@ -105,35 +118,36 @@ export type { Solid } from '../wasm-lib/kcl/bindings/Solid'
export type { KclValue } from '../wasm-lib/kcl/bindings/KclValue' export type { KclValue } from '../wasm-lib/kcl/bindings/KclValue'
export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface' export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'
/**
* The first two items are the start and end points (byte offsets from the start of the file).
* The third item is whether the source range belongs to the 'main' file, i.e., the file currently
* being rendered/displayed in the editor (TODO we need to handle modules better in the frontend).
*/
export type SourceRange = [number, number, boolean]
/** /**
* Convert a SourceRange as used inside the KCL interpreter into the above one for use in the * Convert a SourceRange as used inside the KCL interpreter into the above one for use in the
* frontend (essentially we're eagerly checking whether the frontend should care about the SourceRange * frontend (essentially we're eagerly checking whether the frontend should care about the SourceRange
* so as not to expose details of the interpreter's current representation of module ids throughout * so as not to expose details of the interpreter's current representation of module ids throughout
* the frontend). * the frontend).
*/ */
export function sourceRangeFromRust(s: RustSourceRange): SourceRange { export function sourceRangeFromRust(s: SourceRange): SourceRange {
return [s[0], s[1], s[2] === 0] return [s[0], s[1], s[2]]
} }
/** /**
* Create a default SourceRange for testing or as a placeholder. * Create a default SourceRange for testing or as a placeholder.
*/ */
export function defaultSourceRange(): SourceRange { export function defaultSourceRange(): SourceRange {
return [0, 0, true] return [0, 0, 0]
} }
/** /**
* Create a default RustSourceRange for testing or as a placeholder. * Create a SourceRange for the top-level module.
*/ */
export function defaultRustSourceRange(): RustSourceRange { export function topLevelRange(start: number, end: number): SourceRange {
return [0, 0, 0] return [start, end, 0]
}
/**
* Returns true if this source range is from the file being executed. Returns
* false if it's from a file that was imported.
*/
export function isTopLevelModule(range: SourceRange): boolean {
return range[2] === 0
} }
export const wasmUrl = () => { export const wasmUrl = () => {
@ -234,7 +248,8 @@ export const parse = (code: string | Error): ParseResult | Error => {
parsed.msg, parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0]), sourceRangeFromRust(parsed.sourceRanges[0]),
[], [],
[] [],
defaultArtifactGraph()
) )
} }
} }
@ -258,8 +273,9 @@ export const isPathToNodeNumber = (
export interface ExecState { export interface ExecState {
memory: ProgramMemory memory: ProgramMemory
operations: Operation[] operations: Operation[]
artifacts: { [key in ArtifactId]?: Artifact } artifacts: { [key in ArtifactId]?: RustArtifact }
artifactCommands: ArtifactCommand[] artifactCommands: ArtifactCommand[]
artifactGraph: ArtifactGraph
} }
/** /**
@ -272,18 +288,53 @@ export function emptyExecState(): ExecState {
operations: [], operations: [],
artifacts: {}, artifacts: {},
artifactCommands: [], artifactCommands: [],
artifactGraph: defaultArtifactGraph(),
} }
} }
function execStateFromRust(execOutcome: RustExecOutcome): ExecState { function execStateFromRust(
execOutcome: RustExecOutcome,
program: Node<Program>
): ExecState {
const artifactGraph = rustArtifactGraphToMap(execOutcome.artifactGraph)
// We haven't ported pathToNode logic to Rust yet, so we need to fill it in.
for (const [id, artifact] of artifactGraph) {
if (!artifact) continue
if (!('codeRef' in artifact)) continue
const pathToNode = getNodePathFromSourceRange(
program,
sourceRangeFromRust(artifact.codeRef.range)
)
artifact.codeRef.pathToNode = pathToNode
}
return { return {
memory: ProgramMemory.fromRaw(execOutcome.memory), memory: ProgramMemory.fromRaw(execOutcome.memory),
operations: execOutcome.operations, operations: execOutcome.operations,
artifacts: execOutcome.artifacts, artifacts: execOutcome.artifacts,
artifactCommands: execOutcome.artifactCommands, artifactCommands: execOutcome.artifactCommands,
artifactGraph,
} }
} }
export type ArtifactGraph = Map<ArtifactId, Artifact>
function rustArtifactGraphToMap(
rustArtifactGraph: RustArtifactGraph
): ArtifactGraph {
const map = new Map<ArtifactId, Artifact>()
for (const [id, artifact] of Object.entries(rustArtifactGraph.map)) {
if (!artifact) continue
map.set(id, artifact)
}
return map
}
export function defaultArtifactGraph(): ArtifactGraph {
return new Map()
}
interface Memory { interface Memory {
[key: string]: KclValue | undefined [key: string]: KclValue | undefined
} }
@ -543,7 +594,7 @@ export const executor = async (
engineCommandManager, engineCommandManager,
fileSystemManager fileSystemManager
) )
return execStateFromRust(execOutcome) return execStateFromRust(execOutcome, node)
} catch (e: any) { } catch (e: any) {
console.log(e) console.log(e)
const parsed: KclErrorWithOutputs = JSON.parse(e.toString()) const parsed: KclErrorWithOutputs = JSON.parse(e.toString())
@ -552,7 +603,8 @@ export const executor = async (
parsed.error.msg, parsed.error.msg,
sourceRangeFromRust(parsed.error.sourceRanges[0]), sourceRangeFromRust(parsed.error.sourceRanges[0]),
parsed.operations, parsed.operations,
parsed.artifactCommands parsed.artifactCommands,
rustArtifactGraphToMap(parsed.artifactGraph)
) )
return Promise.reject(kclError) return Promise.reject(kclError)
@ -613,7 +665,8 @@ export const modifyAstForSketch = async (
parsed.msg, parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0]), sourceRangeFromRust(parsed.sourceRanges[0]),
[], [],
[] [],
defaultArtifactGraph()
) )
console.log(kclError) console.log(kclError)
@ -683,7 +736,8 @@ export function programMemoryInit(): ProgramMemory | Error {
parsed.msg, parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0]), sourceRangeFromRust(parsed.sourceRanges[0]),
[], [],
[] [],
defaultArtifactGraph()
) )
} }
} }

View File

@ -280,7 +280,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
args: { args: {
selection: { selection: {
inputType: 'selection', inputType: 'selection',
selectionTypes: ['solid2D', 'segment'], selectionTypes: ['solid2d', 'segment'],
multiple: false, // TODO: multiple selection multiple: false, // TODO: multiple selection
required: true, required: true,
skip: true, skip: true,
@ -312,7 +312,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
args: { args: {
profile: { profile: {
inputType: 'selection', inputType: 'selection',
selectionTypes: ['solid2D'], selectionTypes: ['solid2d'],
required: true, required: true,
skip: true, skip: true,
multiple: false, multiple: false,
@ -337,7 +337,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
args: { args: {
selection: { selection: {
inputType: 'selection', inputType: 'selection',
selectionTypes: ['solid2D'], selectionTypes: ['solid2d'],
multiple: true, multiple: true,
required: true, required: true,
skip: false, skip: false,
@ -373,7 +373,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
args: { args: {
selection: { selection: {
inputType: 'selection', inputType: 'selection',
selectionTypes: ['solid2D', 'segment'], selectionTypes: ['solid2d', 'segment'],
multiple: false, // TODO: multiple selection multiple: false, // TODO: multiple selection
required: true, required: true,
skip: true, skip: true,
@ -578,7 +578,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
selection: { selection: {
inputType: 'selection', inputType: 'selection',
selectionTypes: [ selectionTypes: [
'solid2D', 'solid2d',
'segment', 'segment',
'sweepEdge', 'sweepEdge',
'cap', 'cap',

View File

@ -116,16 +116,16 @@ export const loftValidator = async ({
} }
const { selection } = data const { selection } = data
if (selection.graphSelections.some((s) => s.artifact?.type !== 'solid2D')) { if (selection.graphSelections.some((s) => s.artifact?.type !== 'solid2d')) {
return 'Unable to loft, some selection are not solid2Ds' return 'Unable to loft, some selection are not solid2ds'
} }
const sectionIds = data.selection.graphSelections.flatMap((s) => const sectionIds = data.selection.graphSelections.flatMap((s) =>
s.artifact?.type === 'solid2D' ? s.artifact.pathId : [] s.artifact?.type === 'solid2d' ? s.artifact.pathId : []
) )
if (sectionIds.length < 2) { if (sectionIds.length < 2) {
return 'Unable to loft, selection contains less than two solid2Ds' return 'Unable to loft, selection contains less than two solid2ds'
} }
const loftCommand = async () => { const loftCommand = async () => {

58
src/lib/desktopFS.test.ts Normal file
View File

@ -0,0 +1,58 @@
import { getUniqueProjectName } from './desktopFS'
import { FileEntry } from './project'
/** Create a dummy project */
function project(name: string, children?: FileEntry[]): FileEntry {
return {
name,
children: children || [
{ name: 'main.kcl', children: null, path: 'main.kcl' },
],
path: `/projects/${name}`,
}
}
describe(`Getting unique project names`, () => {
it(`should return the same name if no conflicts`, () => {
const projectName = 'new-project'
const projects = [project('existing-project'), project('another-project')]
const result = getUniqueProjectName(projectName, projects)
expect(result).toBe(projectName)
})
it(`should return a unique name if there is a conflict`, () => {
const projectName = 'existing-project'
const projects = [project('existing-project'), project('another-project')]
const result = getUniqueProjectName(projectName, projects)
expect(result).toBe('existing-project-1')
})
it(`should increment an ending index until a unique one is found`, () => {
const projectName = 'existing-project-1'
const projects = [
project('existing-project'),
project('existing-project-1'),
project('existing-project-2'),
]
const result = getUniqueProjectName(projectName, projects)
expect(result).toBe('existing-project-3')
})
it(`should prefer the formatting of the index identifier if present`, () => {
const projectName = 'existing-project-$nn'
const projects = [
project('existing-project'),
project('existing-project-1'),
project('existing-project-2'),
]
const result = getUniqueProjectName(projectName, projects)
expect(result).toBe('existing-project-03')
})
it(`be able to get an incrementing index regardless of padding zeroes`, () => {
const projectName = 'existing-project-$nn'
const projects = [
project('existing-project'),
project('existing-project-01'),
project('existing-project-2'),
]
const result = getUniqueProjectName(projectName, projects)
expect(result).toBe('existing-project-03')
})
})

View File

@ -54,8 +54,10 @@ export function getNextProjectIndex(
const matches = projects.map((project) => project.name?.match(regex)) const matches = projects.map((project) => project.name?.match(regex))
const indices = matches const indices = matches
.filter(Boolean) .filter(Boolean)
.map((match) => match![1]) .map((match) => (match !== null ? match[1] : '-1'))
.map(Number) .map((maybeMatchIndex) => {
return parseInt(maybeMatchIndex || '0', 10)
})
const maxIndex = Math.max(...indices, -1) const maxIndex = Math.max(...indices, -1)
return maxIndex + 1 return maxIndex + 1
} }
@ -83,6 +85,33 @@ export function doesProjectNameNeedInterpolated(projectName: string) {
return projectName.includes(INDEX_IDENTIFIER) return projectName.includes(INDEX_IDENTIFIER)
} }
/**
* Given a target name, which may include our magic index interpolation string,
* and a list of projects, return a unique name that doesn't conflict with any
* of the existing projects, incrementing any ending number if necessary.
* @param name
* @param projects
* @returns
*/
export function getUniqueProjectName(name: string, projects: FileEntry[]) {
// The name may have our magic index interpolation string in it
const needsInterpolation = doesProjectNameNeedInterpolated(name)
if (needsInterpolation) {
const nextIndex = getNextProjectIndex(name, projects)
return interpolateProjectNameWithIndex(name, nextIndex)
} else {
let newName = name
while (projects.some((project) => project.name === newName)) {
const nameEndsWithNumber = newName.match(/\d+$/)
newName = nameEndsWithNumber
? newName.replace(/\d+$/, (num) => `${parseInt(num, 10) + 1}`)
: `${name}-1`
}
return newName
}
}
function escapeRegExpChars(string: string) { function escapeRegExpChars(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
} }

View File

@ -1,4 +1,4 @@
import { defaultRustSourceRange } from 'lang/wasm' import { defaultSourceRange } from 'lang/wasm'
import { filterOperations } from './operations' import { filterOperations } from './operations'
import { Operation } from 'wasm-lib/kcl/bindings/Operation' import { Operation } from 'wasm-lib/kcl/bindings/Operation'
@ -8,7 +8,7 @@ function stdlib(name: string): Operation {
name, name,
unlabeledArg: null, unlabeledArg: null,
labeledArgs: {}, labeledArgs: {},
sourceRange: defaultRustSourceRange(), sourceRange: defaultSourceRange(),
isError: false, isError: false,
} }
} }
@ -17,10 +17,10 @@ function userCall(name: string): Operation {
return { return {
type: 'UserDefinedFunctionCall', type: 'UserDefinedFunctionCall',
name, name,
functionSourceRange: defaultRustSourceRange(), functionSourceRange: defaultSourceRange(),
unlabeledArg: null, unlabeledArg: null,
labeledArgs: {}, labeledArgs: {},
sourceRange: defaultRustSourceRange(), sourceRange: defaultSourceRange(),
} }
} }
function userReturn(): Operation { function userReturn(): Operation {

View File

@ -3,8 +3,8 @@ import { VITE_KC_API_BASE_URL } from 'env'
import crossPlatformFetch from './crossPlatformFetch' import crossPlatformFetch from './crossPlatformFetch'
import { err, reportRejection } from './trap' import { err, reportRejection } from './trap'
import { Selections } from './selections' import { Selections } from './selections'
import { ArtifactGraph, getArtifactOfTypes } from 'lang/std/artifactGraph' import { getArtifactOfTypes } from 'lang/std/artifactGraph'
import { SourceRange } from 'lang/wasm' import { ArtifactGraph, SourceRange, topLevelRange } from 'lang/wasm'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { codeManager, editorManager, kclManager } from './singletons' import { codeManager, editorManager, kclManager } from './singletons'
import { ToastPromptToEditCadSuccess } from 'components/ToastTextToCad' import { ToastPromptToEditCadSuccess } from 'components/ToastTextToCad'
@ -334,7 +334,7 @@ const reBuildNewCodeWithRanges = (
} else if (change.added && !change.removed) { } else if (change.added && !change.removed) {
const start = newCodeWithRanges.length const start = newCodeWithRanges.length
const end = start + change.value.length const end = start + change.value.length
insertRanges.push([start, end, true]) insertRanges.push(topLevelRange(start, end))
newCodeWithRanges += change.value newCodeWithRanges += change.value
} }
} }

View File

@ -10,6 +10,7 @@ import {
SourceRange, SourceRange,
Expr, Expr,
defaultSourceRange, defaultSourceRange,
topLevelRange,
} from 'lang/wasm' } from 'lang/wasm'
import { ModelingMachineEvent } from 'machines/modelingMachine' import { ModelingMachineEvent } from 'machines/modelingMachine'
import { isNonNullable, uuidv4 } from 'lib/utils' import { isNonNullable, uuidv4 } from 'lib/utils'
@ -63,7 +64,7 @@ type Selection__old =
| 'line-end' | 'line-end'
| 'line-mid' | 'line-mid'
| 'extrude-wall' | 'extrude-wall'
| 'solid2D' | 'solid2d'
| 'start-cap' | 'start-cap'
| 'end-cap' | 'end-cap'
| 'point' | 'point'
@ -103,13 +104,13 @@ function convertSelectionToOld(selection: Selection): Selection__old | null {
// return {} as Selection__old // return {} as Selection__old
// TODO implementation // TODO implementation
const _artifact = selection.artifact const _artifact = selection.artifact
if (_artifact?.type === 'solid2D') { if (_artifact?.type === 'solid2d') {
const codeRef = getSolid2dCodeRef( const codeRef = getSolid2dCodeRef(
_artifact, _artifact,
engineCommandManager.artifactGraph engineCommandManager.artifactGraph
) )
if (err(codeRef)) return null if (err(codeRef)) return null
return { range: codeRef.range, type: 'solid2D' } return { range: codeRef.range, type: 'solid2d' }
} }
if (_artifact?.type === 'cap') { if (_artifact?.type === 'cap') {
const codeRef = getCapCodeRef(_artifact, engineCommandManager.artifactGraph) const codeRef = getCapCodeRef(_artifact, engineCommandManager.artifactGraph)
@ -269,7 +270,7 @@ export function getEventForSegmentSelection(
selectionType: 'singleCodeCursor', selectionType: 'singleCodeCursor',
selection: { selection: {
codeRef: { codeRef: {
range: [node.node.start, node.node.end, true], range: topLevelRange(node.node.start, node.node.end),
pathToNode: group.userData.pathToNode, pathToNode: group.userData.pathToNode,
}, },
}, },
@ -381,10 +382,13 @@ export function processCodeMirrorRanges({
if (!isChange) return null if (!isChange) return null
const codeBasedSelections: Selections['graphSelections'] = const codeBasedSelections: Selections['graphSelections'] =
codeMirrorRanges.map(({ from, to }) => { codeMirrorRanges.map(({ from, to }) => {
const pathToNode = getNodePathFromSourceRange(ast, [from, to, true]) const pathToNode = getNodePathFromSourceRange(
ast,
topLevelRange(from, to)
)
return { return {
codeRef: { codeRef: {
range: [from, to, true], range: topLevelRange(from, to),
pathToNode, pathToNode,
}, },
} }
@ -447,7 +451,10 @@ function updateSceneObjectColors(codeBasedSelections: Selection[]) {
if (err(nodeMeta)) return if (err(nodeMeta)) return
const node = nodeMeta.node const node = nodeMeta.node
const groupHasCursor = codeBasedSelections.some((selection) => { const groupHasCursor = codeBasedSelections.some((selection) => {
return isOverlap(selection?.codeRef?.range, [node.start, node.end, true]) return isOverlap(
selection?.codeRef?.range,
topLevelRange(node.start, node.end)
)
}) })
const color = groupHasCursor const color = groupHasCursor
@ -575,7 +582,7 @@ export function getSelectionTypeDisplayText(
([type, count]) => ([type, count]) =>
`${count} ${type `${count} ${type
.replace('wall', 'face') .replace('wall', 'face')
.replace('solid2D', 'face') .replace('solid2d', 'face')
.replace('segment', 'face')}${count > 1 ? 's' : ''}` .replace('segment', 'face')}${count > 1 ? 's' : ''}`
) )
.toArray() .toArray()
@ -650,7 +657,7 @@ export function codeToIdSelections(
const artifact = engineCommandManager.artifactGraph.get( const artifact = engineCommandManager.artifactGraph.get(
entry.artifact.solid2dId || '' entry.artifact.solid2dId || ''
) )
if (artifact?.type !== 'solid2D') { if (artifact?.type !== 'solid2d') {
bestCandidate = { bestCandidate = {
artifact: entry.artifact, artifact: entry.artifact,
selection, selection,
@ -873,7 +880,7 @@ export function updateSelections(
return { return {
artifact: artifact, artifact: artifact,
codeRef: { codeRef: {
range: [node.start, node.end, true], range: topLevelRange(node.start, node.end),
pathToNode: pathToNode, pathToNode: pathToNode,
}, },
} }
@ -887,7 +894,7 @@ export function updateSelections(
if (err(node)) return node if (err(node)) return node
pathToNodeBasedSelections.push({ pathToNodeBasedSelections.push({
codeRef: { codeRef: {
range: [node.node.start, node.node.end, true], range: topLevelRange(node.node.start, node.node.end),
pathToNode: pathToNode, pathToNode: pathToNode,
}, },
}) })

View File

@ -6,16 +6,16 @@ import {
hasLeadingZero, hasLeadingZero,
hasDigitsLeftOfDecimal, hasDigitsLeftOfDecimal,
} from './utils' } from './utils'
import { SourceRange } from '../lang/wasm' import { SourceRange, topLevelRange } from '../lang/wasm'
describe('testing isOverlapping', () => { describe('testing isOverlapping', () => {
testBothOrders([0, 3, true], [3, 10, true]) testBothOrders(topLevelRange(0, 3), topLevelRange(3, 10))
testBothOrders([0, 5, true], [3, 4, true]) testBothOrders(topLevelRange(0, 5), topLevelRange(3, 4))
testBothOrders([0, 5, true], [5, 10, true]) testBothOrders(topLevelRange(0, 5), topLevelRange(5, 10))
testBothOrders([0, 5, true], [6, 10, true], false) testBothOrders(topLevelRange(0, 5), topLevelRange(6, 10), false)
testBothOrders([0, 5, true], [-1, 1, true]) testBothOrders(topLevelRange(0, 5), topLevelRange(-1, 1))
testBothOrders([0, 5, true], [-1, 0, true]) testBothOrders(topLevelRange(0, 5), topLevelRange(-1, 0))
testBothOrders([0, 5, true], [-2, -1, true], false) testBothOrders(topLevelRange(0, 5), topLevelRange(-2, -1), false)
}) })
function testBothOrders(a: SourceRange, b: SourceRange, result = true) { function testBothOrders(a: SourceRange, b: SourceRange, result = true) {

View File

@ -161,7 +161,7 @@ const Home = () => {
}} }}
data-testid="home-new-file" data-testid="home-new-file"
> >
New project Create project
</ActionButton> </ActionButton>
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">

View File

@ -1382,12 +1382,6 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "iai"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71a816c97c42258aa5834d07590b718b4c9a598944cd39a52dc25b351185d678"
[[package]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.61" version = "0.1.61"
@ -1739,7 +1733,6 @@ dependencies = [
"gltf-json", "gltf-json",
"handlebars", "handlebars",
"http 1.2.0", "http 1.2.0",
"iai",
"image", "image",
"indexmap 2.7.0", "indexmap 2.7.0",
"insta", "insta",

View File

@ -17,6 +17,7 @@ use kittycad_modeling_cmds::{
websocket::{ModelingBatch, ModelingCmdReq, OkWebSocketResponseData, WebSocketRequest, WebSocketResponse}, websocket::{ModelingBatch, ModelingCmdReq, OkWebSocketResponseData, WebSocketRequest, WebSocketResponse},
}; };
use tokio::sync::RwLock; use tokio::sync::RwLock;
use uuid::Uuid;
const CPP_PREFIX: &str = "const double scaleFactor = 100;\n"; const CPP_PREFIX: &str = "const double scaleFactor = 100;\n";
const NEED_PLANES: bool = true; const NEED_PLANES: bool = true;
@ -369,6 +370,10 @@ impl kcl_lib::EngineManager for EngineConnection {
self.batch_end.clone() self.batch_end.clone()
} }
fn responses(&self) -> IndexMap<Uuid, WebSocketResponse> {
IndexMap::new()
}
fn take_artifact_commands(&self) -> Vec<ArtifactCommand> { fn take_artifact_commands(&self) -> Vec<ArtifactCommand> {
Vec::new() Vec::new()
} }

View File

@ -113,7 +113,6 @@ base64 = "0.22.1"
criterion = { version = "0.5.1", features = ["async_tokio"] } criterion = { version = "0.5.1", features = ["async_tokio"] }
expectorate = "1.1.0" expectorate = "1.1.0"
handlebars = "6.3.0" handlebars = "6.3.0"
iai = "0.1"
image = { version = "0.25.5", default-features = false, features = ["png"] } image = { version = "0.25.5", default-features = false, features = ["png"] }
insta = { version = "1.41.1", features = ["json", "filters", "redactions"] } insta = { version = "1.41.1", features = ["json", "filters", "redactions"] }
itertools = "0.13.0" itertools = "0.13.0"
@ -129,10 +128,6 @@ workspace = true
name = "compiler_benchmark_criterion" name = "compiler_benchmark_criterion"
harness = false harness = false
[[bench]]
name = "compiler_benchmark_iai"
harness = false
[[bench]] [[bench]]
name = "digest_benchmark" name = "digest_benchmark"
harness = false harness = false
@ -142,15 +137,7 @@ name = "lsp_semantic_tokens_benchmark_criterion"
harness = false harness = false
required-features = ["lsp-test-util"] required-features = ["lsp-test-util"]
[[bench]]
name = "lsp_semantic_tokens_benchmark_iai"
harness = false
required-features = ["lsp-test-util"]
[[bench]] [[bench]]
name = "executor_benchmark_criterion" name = "executor_benchmark_criterion"
harness = false harness = false
[[bench]]
name = "executor_benchmark_iai"
harness = false

View File

@ -1,35 +0,0 @@
use iai::black_box;
pub fn parse(program: &str) {
black_box(kcl_lib::Program::parse(program).unwrap());
}
fn parse_kitt() {
parse(KITT_PROGRAM)
}
fn parse_pipes() {
parse(PIPES_PROGRAM)
}
fn parse_cube() {
parse(CUBE_PROGRAM)
}
fn parse_math() {
parse(MATH_PROGRAM)
}
fn parse_lsystem() {
parse(LSYSTEM_PROGRAM)
}
iai::main! {
parse_kitt,
parse_pipes,
parse_cube,
parse_math,
parse_lsystem,
}
const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl");
const PIPES_PROGRAM: &str = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
const MATH_PROGRAM: &str = include_str!("../../tests/executor/inputs/math.kcl");
const LSYSTEM_PROGRAM: &str = include_str!("../../tests/executor/inputs/lsystem.kcl");

View File

@ -1,27 +0,0 @@
use iai::black_box;
async fn execute_server_rack_heavy() {
let code = SERVER_RACK_HEAVY_PROGRAM;
black_box(
kcl_lib::test_server::execute_and_snapshot(code, kcl_lib::UnitLength::Mm, None)
.await
.unwrap(),
);
}
async fn execute_server_rack_lite() {
let code = SERVER_RACK_LITE_PROGRAM;
black_box(
kcl_lib::test_server::execute_and_snapshot(code, kcl_lib::UnitLength::Mm, None)
.await
.unwrap(),
);
}
iai::main! {
execute_server_rack_lite,
execute_server_rack_heavy,
}
const SERVER_RACK_HEAVY_PROGRAM: &str = include_str!("../../tests/executor/inputs/server-rack-heavy.kcl");
const SERVER_RACK_LITE_PROGRAM: &str = include_str!("../../tests/executor/inputs/server-rack-lite.kcl");

View File

@ -1,45 +0,0 @@
use iai::black_box;
use kcl_lib::kcl_lsp_server;
use tower_lsp::LanguageServer;
async fn kcl_lsp_semantic_tokens(code: &str) {
let server = kcl_lsp_server(false).await.unwrap();
// Send open file.
server
.did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams {
text_document: tower_lsp::lsp_types::TextDocumentItem {
uri: "file:///test.kcl".try_into().unwrap(),
language_id: "kcl".to_string(),
version: 1,
text: code.to_string(),
},
})
.await;
// Send semantic tokens request.
black_box(
server
.semantic_tokens_full(tower_lsp::lsp_types::SemanticTokensParams {
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
partial_result_params: Default::default(),
work_done_progress_params: Default::default(),
})
.await
.unwrap()
.unwrap(),
);
}
async fn semantic_tokens_global_tags() {
let code = GLOBAL_TAGS_FILE;
kcl_lsp_semantic_tokens(code).await;
}
iai::main! {
semantic_tokens_global_tags,
}
const GLOBAL_TAGS_FILE: &str = include_str!("../../tests/executor/inputs/global-tags.kcl");

View File

@ -383,6 +383,16 @@ impl EngineManager for EngineConnection {
self.batch_end.clone() self.batch_end.clone()
} }
fn responses(&self) -> IndexMap<Uuid, WebSocketResponse> {
self.responses
.iter()
.map(|entry| {
let (k, v) = entry.pair();
(*k, v.clone())
})
.collect()
}
fn take_artifact_commands(&self) -> Vec<ArtifactCommand> { fn take_artifact_commands(&self) -> Vec<ArtifactCommand> {
let mut artifact_commands = self.artifact_commands.lock().unwrap(); let mut artifact_commands = self.artifact_commands.lock().unwrap();
std::mem::take(&mut *artifact_commands) std::mem::take(&mut *artifact_commands)

View File

@ -77,6 +77,10 @@ impl crate::engine::EngineManager for EngineConnection {
self.batch_end.clone() self.batch_end.clone()
} }
fn responses(&self) -> IndexMap<Uuid, WebSocketResponse> {
IndexMap::new()
}
fn take_artifact_commands(&self) -> Vec<ArtifactCommand> { fn take_artifact_commands(&self) -> Vec<ArtifactCommand> {
let mut artifact_commands = self.artifact_commands.lock().unwrap(); let mut artifact_commands = self.artifact_commands.lock().unwrap();
std::mem::take(&mut *artifact_commands) std::mem::take(&mut *artifact_commands)

View File

@ -52,6 +52,7 @@ pub struct EngineConnection {
manager: Arc<EngineCommandManager>, manager: Arc<EngineCommandManager>,
batch: Arc<Mutex<Vec<(WebSocketRequest, SourceRange)>>>, batch: Arc<Mutex<Vec<(WebSocketRequest, SourceRange)>>>,
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>, batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>,
responses: Arc<Mutex<IndexMap<Uuid, WebSocketResponse>>>,
artifact_commands: Arc<Mutex<Vec<ArtifactCommand>>>, artifact_commands: Arc<Mutex<Vec<ArtifactCommand>>>,
execution_kind: Arc<Mutex<ExecutionKind>>, execution_kind: Arc<Mutex<ExecutionKind>>,
} }
@ -66,6 +67,7 @@ impl EngineConnection {
manager: Arc::new(manager), manager: Arc::new(manager),
batch: Arc::new(Mutex::new(Vec::new())), batch: Arc::new(Mutex::new(Vec::new())),
batch_end: Arc::new(Mutex::new(IndexMap::new())), batch_end: Arc::new(Mutex::new(IndexMap::new())),
responses: Arc::new(Mutex::new(IndexMap::new())),
artifact_commands: Arc::new(Mutex::new(Vec::new())), artifact_commands: Arc::new(Mutex::new(Vec::new())),
execution_kind: Default::default(), execution_kind: Default::default(),
}) })
@ -106,6 +108,11 @@ impl crate::engine::EngineManager for EngineConnection {
self.batch_end.clone() self.batch_end.clone()
} }
fn responses(&self) -> IndexMap<Uuid, WebSocketResponse> {
let responses = self.responses.lock().unwrap();
responses.clone()
}
fn take_artifact_commands(&self) -> Vec<ArtifactCommand> { fn take_artifact_commands(&self) -> Vec<ArtifactCommand> {
let mut artifact_commands = self.artifact_commands.lock().unwrap(); let mut artifact_commands = self.artifact_commands.lock().unwrap();
std::mem::take(&mut *artifact_commands) std::mem::take(&mut *artifact_commands)
@ -265,6 +272,9 @@ impl crate::engine::EngineManager for EngineConnection {
}) })
})?; })?;
let mut responses = self.responses.lock().unwrap();
responses.insert(id, ws_result.clone());
Ok(ws_result) Ok(ws_result)
} }

View File

@ -67,6 +67,9 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
/// Get the batch of end commands to be sent to the engine. /// Get the batch of end commands to be sent to the engine.
fn batch_end(&self) -> Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>; fn batch_end(&self) -> Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>;
/// Get the command responses from the engine.
fn responses(&self) -> IndexMap<Uuid, WebSocketResponse>;
/// Take the artifact commands generated up to this point and clear them. /// Take the artifact commands generated up to this point and clear them.
fn take_artifact_commands(&self) -> Vec<ArtifactCommand>; fn take_artifact_commands(&self) -> Vec<ArtifactCommand>;

View File

@ -3,7 +3,7 @@ use thiserror::Error;
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity}; use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
use crate::{ use crate::{
execution::{ArtifactCommand, Operation}, execution::{ArtifactCommand, ArtifactGraph, Operation},
lsp::IntoDiagnostic, lsp::IntoDiagnostic,
source_range::{ModuleId, SourceRange}, source_range::{ModuleId, SourceRange},
}; };
@ -114,14 +114,21 @@ pub struct KclErrorWithOutputs {
pub error: KclError, pub error: KclError,
pub operations: Vec<Operation>, pub operations: Vec<Operation>,
pub artifact_commands: Vec<ArtifactCommand>, pub artifact_commands: Vec<ArtifactCommand>,
pub artifact_graph: ArtifactGraph,
} }
impl KclErrorWithOutputs { impl KclErrorWithOutputs {
pub fn new(error: KclError, operations: Vec<Operation>, artifact_commands: Vec<ArtifactCommand>) -> Self { pub fn new(
error: KclError,
operations: Vec<Operation>,
artifact_commands: Vec<ArtifactCommand>,
artifact_graph: ArtifactGraph,
) -> Self {
Self { Self {
error, error,
operations, operations,
artifact_commands, artifact_commands,
artifact_graph,
} }
} }
pub fn no_outputs(error: KclError) -> Self { pub fn no_outputs(error: KclError) -> Self {
@ -129,6 +136,7 @@ impl KclErrorWithOutputs {
error, error,
operations: Default::default(), operations: Default::default(),
artifact_commands: Default::default(), artifact_commands: Default::default(),
artifact_graph: Default::default(),
} }
} }
} }

View File

@ -1,15 +1,29 @@
use kittycad_modeling_cmds::ModelingCmd; use fnv::FnvHashMap;
use schemars::JsonSchema; use indexmap::IndexMap;
use serde::{Deserialize, Serialize}; use kittycad_modeling_cmds::{
self as kcmc,
id::ModelingCmdId,
ok_response::OkModelingCmdResponse,
shared::ExtrusionFaceCapType,
websocket::{BatchResponse, OkWebSocketResponseData, WebSocketResponse},
EnableSketchMode, ModelingCmd, SketchModeDisable,
};
use serde::{ser::SerializeSeq, Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::SourceRange; use crate::{
parsing::ast::types::{Node, Program},
KclError, SourceRange,
};
#[cfg(test)]
mod mermaid_tests;
/// A command that may create or update artifacts on the TS side. Because /// A command that may create or update artifacts on the TS side. Because
/// engine commands are batched, we don't have the response yet when these are /// engine commands are batched, we don't have the response yet when these are
/// created. /// created.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)] #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
#[ts(export)] #[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ArtifactCommand { pub struct ArtifactCommand {
/// Identifier of the command that can be matched with its response. /// Identifier of the command that can be matched with its response.
@ -22,8 +36,8 @@ pub struct ArtifactCommand {
pub command: ModelingCmd, pub command: ModelingCmd,
} }
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash, ts_rs::TS)]
#[ts(export)] #[ts(export_to = "Artifact.ts")]
pub struct ArtifactId(Uuid); pub struct ArtifactId(Uuid);
impl ArtifactId { impl ArtifactId {
@ -56,22 +70,835 @@ impl From<&ArtifactId> for Uuid {
} }
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] impl From<ModelingCmdId> for ArtifactId {
#[ts(export)] fn from(id: ModelingCmdId) -> Self {
#[serde(rename_all = "camelCase")] Self::new(*id.as_ref())
pub struct Artifact { }
pub id: ArtifactId,
#[serde(flatten)]
pub inner: ArtifactInner,
pub source_range: SourceRange,
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] impl From<&ModelingCmdId> for ArtifactId {
#[ts(export)] fn from(id: &ModelingCmdId) -> Self {
#[serde(tag = "type")] Self::new(*id.as_ref())
pub enum ArtifactInner { }
#[serde(rename_all = "camelCase")] }
StartSketchOnFace { face_id: Uuid },
#[serde(rename_all = "camelCase")] pub type DummyPathToNode = Vec<()>;
StartSketchOnPlane { plane_id: Uuid },
fn serialize_dummy_path_to_node<S>(_path_to_node: &DummyPathToNode, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
// Always output an empty array, for now.
let seq = serializer.serialize_seq(Some(0))?;
seq.end()
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")]
pub struct CodeRef {
pub range: SourceRange,
// TODO: We should implement this in Rust.
#[serde(default, serialize_with = "serialize_dummy_path_to_node")]
#[ts(type = "Array<[string | number, string]>")]
pub path_to_node: DummyPathToNode,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")]
pub struct Plane {
pub id: ArtifactId,
pub path_ids: Vec<ArtifactId>,
pub code_ref: CodeRef,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")]
pub struct Path {
pub id: ArtifactId,
pub plane_id: ArtifactId,
pub seg_ids: Vec<ArtifactId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sweep_id: Option<ArtifactId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub solid2d_id: Option<ArtifactId>,
pub code_ref: CodeRef,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")]
pub struct Segment {
pub id: ArtifactId,
pub path_id: ArtifactId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub surface_id: Option<ArtifactId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub edge_ids: Vec<ArtifactId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub edge_cut_id: Option<ArtifactId>,
pub code_ref: CodeRef,
}
/// A sweep is a more generic term for extrude, revolve, loft, and sweep.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")]
pub struct Sweep {
pub id: ArtifactId,
pub sub_type: SweepSubType,
pub path_id: ArtifactId,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub surface_ids: Vec<ArtifactId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub edge_ids: Vec<ArtifactId>,
pub code_ref: CodeRef,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")]
pub enum SweepSubType {
Extrusion,
Revolve,
Loft,
Sweep,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")]
pub struct Solid2d {
pub id: ArtifactId,
pub path_id: ArtifactId,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")]
pub struct Wall {
pub id: ArtifactId,
pub seg_id: ArtifactId,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub edge_cut_edge_ids: Vec<ArtifactId>,
pub sweep_id: ArtifactId,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub path_ids: Vec<ArtifactId>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")]
pub struct Cap {
pub id: ArtifactId,
pub sub_type: CapSubType,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub edge_cut_edge_ids: Vec<ArtifactId>,
pub sweep_id: ArtifactId,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub path_ids: Vec<ArtifactId>,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")]
pub enum CapSubType {
Start,
End,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")]
pub struct SweepEdge {
pub id: ArtifactId,
pub sub_type: SweepEdgeSubType,
pub seg_id: ArtifactId,
pub sweep_id: ArtifactId,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")]
pub enum SweepEdgeSubType {
Opposite,
Adjacent,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")]
pub struct EdgeCut {
pub id: ArtifactId,
pub sub_type: EdgeCutSubType,
pub consumed_edge_id: ArtifactId,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub edge_ids: Vec<ArtifactId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub surface_id: Option<ArtifactId>,
pub code_ref: CodeRef,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")]
pub enum EdgeCutSubType {
Fillet,
Chamfer,
}
impl From<kcmc::shared::CutType> for EdgeCutSubType {
fn from(cut_type: kcmc::shared::CutType) -> Self {
match cut_type {
kcmc::shared::CutType::Fillet => EdgeCutSubType::Fillet,
kcmc::shared::CutType::Chamfer => EdgeCutSubType::Chamfer,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")]
pub struct EdgeCutEdge {
pub id: ArtifactId,
pub edge_cut_id: ArtifactId,
pub surface_id: ArtifactId,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum Artifact {
Plane(Plane),
Path(Path),
Segment(Segment),
Solid2d(Solid2d),
#[serde(rename_all = "camelCase")]
StartSketchOnFace {
id: ArtifactId,
face_id: Uuid,
source_range: SourceRange,
},
#[serde(rename_all = "camelCase")]
StartSketchOnPlane {
id: ArtifactId,
plane_id: Uuid,
source_range: SourceRange,
},
Sweep(Sweep),
Wall(Wall),
Cap(Cap),
SweepEdge(SweepEdge),
EdgeCut(EdgeCut),
EdgeCutEdge(EdgeCutEdge),
}
impl Artifact {
pub(crate) fn id(&self) -> ArtifactId {
match self {
Artifact::Plane(a) => a.id,
Artifact::Path(a) => a.id,
Artifact::Segment(a) => a.id,
Artifact::Solid2d(a) => a.id,
Artifact::StartSketchOnFace { id, .. } => *id,
Artifact::StartSketchOnPlane { id, .. } => *id,
Artifact::Sweep(a) => a.id,
Artifact::Wall(a) => a.id,
Artifact::Cap(a) => a.id,
Artifact::SweepEdge(a) => a.id,
Artifact::EdgeCut(a) => a.id,
Artifact::EdgeCutEdge(a) => a.id,
}
}
#[expect(dead_code)]
pub(crate) fn code_ref(&self) -> Option<&CodeRef> {
match self {
Artifact::Plane(a) => Some(&a.code_ref),
Artifact::Path(a) => Some(&a.code_ref),
Artifact::Segment(a) => Some(&a.code_ref),
Artifact::Solid2d(_) => None,
// TODO: We should add code refs for these.
Artifact::StartSketchOnFace { .. } => None,
Artifact::StartSketchOnPlane { .. } => None,
Artifact::Sweep(a) => Some(&a.code_ref),
Artifact::Wall(_) => None,
Artifact::Cap(_) => None,
Artifact::SweepEdge(_) => None,
Artifact::EdgeCut(a) => Some(&a.code_ref),
Artifact::EdgeCutEdge(_) => None,
}
}
/// Merge the new artifact into self. If it can't because it's a different
/// type, return the new artifact which should be used as a replacement.
fn merge(&mut self, new: Artifact) -> Option<Artifact> {
match self {
Artifact::Plane(a) => a.merge(new),
Artifact::Path(a) => a.merge(new),
Artifact::Segment(a) => a.merge(new),
Artifact::Solid2d(_) => Some(new),
Artifact::StartSketchOnFace { .. } => Some(new),
Artifact::StartSketchOnPlane { .. } => Some(new),
Artifact::Sweep(a) => a.merge(new),
Artifact::Wall(a) => a.merge(new),
Artifact::Cap(a) => a.merge(new),
Artifact::SweepEdge(_) => Some(new),
Artifact::EdgeCut(a) => a.merge(new),
Artifact::EdgeCutEdge(_) => Some(new),
}
}
}
impl Plane {
fn merge(&mut self, new: Artifact) -> Option<Artifact> {
let Artifact::Plane(new) = new else {
return Some(new);
};
merge_ids(&mut self.path_ids, new.path_ids);
None
}
}
impl Path {
fn merge(&mut self, new: Artifact) -> Option<Artifact> {
let Artifact::Path(new) = new else {
return Some(new);
};
merge_opt_id(&mut self.sweep_id, new.sweep_id);
merge_ids(&mut self.seg_ids, new.seg_ids);
merge_opt_id(&mut self.solid2d_id, new.solid2d_id);
None
}
}
impl Segment {
fn merge(&mut self, new: Artifact) -> Option<Artifact> {
let Artifact::Segment(new) = new else {
return Some(new);
};
merge_opt_id(&mut self.surface_id, new.surface_id);
merge_ids(&mut self.edge_ids, new.edge_ids);
merge_opt_id(&mut self.edge_cut_id, new.edge_cut_id);
None
}
}
impl Sweep {
fn merge(&mut self, new: Artifact) -> Option<Artifact> {
let Artifact::Sweep(new) = new else {
return Some(new);
};
merge_ids(&mut self.surface_ids, new.surface_ids);
merge_ids(&mut self.edge_ids, new.edge_ids);
None
}
}
impl Wall {
fn merge(&mut self, new: Artifact) -> Option<Artifact> {
let Artifact::Wall(new) = new else {
return Some(new);
};
merge_ids(&mut self.edge_cut_edge_ids, new.edge_cut_edge_ids);
merge_ids(&mut self.path_ids, new.path_ids);
None
}
}
impl Cap {
fn merge(&mut self, new: Artifact) -> Option<Artifact> {
let Artifact::Cap(new) = new else {
return Some(new);
};
merge_ids(&mut self.edge_cut_edge_ids, new.edge_cut_edge_ids);
merge_ids(&mut self.path_ids, new.path_ids);
None
}
}
impl EdgeCut {
fn merge(&mut self, new: Artifact) -> Option<Artifact> {
let Artifact::EdgeCut(new) = new else {
return Some(new);
};
merge_opt_id(&mut self.surface_id, new.surface_id);
merge_ids(&mut self.edge_ids, new.edge_ids);
None
}
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")]
pub struct ArtifactGraph {
map: IndexMap<ArtifactId, Artifact>,
}
pub(super) fn build_artifact_graph(
artifact_commands: &[ArtifactCommand],
responses: &IndexMap<Uuid, WebSocketResponse>,
ast: &Node<Program>,
exec_artifacts: &IndexMap<ArtifactId, Artifact>,
) -> Result<ArtifactGraph, KclError> {
let mut map = IndexMap::new();
let mut current_plane_id = None;
for artifact_command in artifact_commands {
if let ModelingCmd::EnableSketchMode(EnableSketchMode { entity_id, .. }) = artifact_command.command {
current_plane_id = Some(entity_id);
}
if let ModelingCmd::SketchModeDisable(SketchModeDisable { .. }) = artifact_command.command {
current_plane_id = None;
}
let flattened_responses = flatten_modeling_command_responses(responses);
let artifact_updates = artifacts_to_update(
&map,
artifact_command,
&flattened_responses,
current_plane_id,
ast,
exec_artifacts,
)?;
for artifact in artifact_updates {
// Merge with existing artifacts.
merge_artifact_into_map(&mut map, artifact);
}
}
Ok(ArtifactGraph { map })
}
/// Flatten the responses into a map of command IDs to modeling command
/// responses. The raw responses from the engine contain batches.
fn flatten_modeling_command_responses(
responses: &IndexMap<Uuid, WebSocketResponse>,
) -> FnvHashMap<Uuid, OkModelingCmdResponse> {
let mut map = FnvHashMap::default();
for (cmd_id, ws_response) in responses {
let WebSocketResponse::Success(response) = ws_response else {
// Response not successful.
continue;
};
match &response.resp {
OkWebSocketResponseData::Modeling { modeling_response } => {
map.insert(*cmd_id, modeling_response.clone());
}
OkWebSocketResponseData::ModelingBatch { responses } =>
{
#[expect(
clippy::iter_over_hash_type,
reason = "Since we're moving entries to another unordered map, it's fine that the order is undefined"
)]
for (cmd_id, batch_response) in responses {
if let BatchResponse::Success {
response: modeling_response,
} = batch_response
{
map.insert(*cmd_id.as_ref(), modeling_response.clone());
}
}
}
OkWebSocketResponseData::IceServerInfo { .. }
| OkWebSocketResponseData::TrickleIce { .. }
| OkWebSocketResponseData::SdpAnswer { .. }
| OkWebSocketResponseData::Export { .. }
| OkWebSocketResponseData::MetricsRequest { .. }
| OkWebSocketResponseData::ModelingSessionData { .. }
| OkWebSocketResponseData::Pong { .. } => {}
}
}
map
}
fn merge_artifact_into_map(map: &mut IndexMap<ArtifactId, Artifact>, new_artifact: Artifact) {
let id = new_artifact.id();
let Some(old_artifact) = map.get_mut(&id) else {
// No old artifact exists. Insert the new one.
map.insert(id, new_artifact);
return;
};
if let Some(replacement) = old_artifact.merge(new_artifact) {
*old_artifact = replacement;
}
}
/// Merge the new IDs into the base vector, avoiding duplicates. This is O(nm)
/// runtime. Rationale is that most of the ID collections in the artifact graph
/// are pretty small, but we may want to change this in the future.
fn merge_ids(base: &mut Vec<ArtifactId>, new: Vec<ArtifactId>) {
let original_len = base.len();
for id in new {
// Don't bother inspecting new items that we just pushed.
let original_base = &base[..original_len];
if !original_base.contains(&id) {
base.push(id);
}
}
}
fn merge_opt_id(base: &mut Option<ArtifactId>, new: Option<ArtifactId>) {
// Always use the new one, even if it clears it.
*base = new;
}
fn artifacts_to_update(
artifacts: &IndexMap<ArtifactId, Artifact>,
artifact_command: &ArtifactCommand,
responses: &FnvHashMap<Uuid, OkModelingCmdResponse>,
current_plane_id: Option<Uuid>,
_ast: &Node<Program>,
_exec_artifacts: &IndexMap<ArtifactId, Artifact>,
) -> Result<Vec<Artifact>, KclError> {
// TODO: Build path-to-node from artifact_command source range. Right now,
// we're serializing an empty array, and the TS wrapper fills it in with the
// correct value.
let path_to_node = Vec::new();
let range = artifact_command.range;
let uuid = artifact_command.cmd_id;
let id = ArtifactId::new(uuid);
let Some(response) = responses.get(&uuid) else {
// Response not found or not successful.
return Ok(Vec::new());
};
let cmd = &artifact_command.command;
match cmd {
ModelingCmd::MakePlane(_) => {
if range.is_synthetic() {
return Ok(Vec::new());
}
// If we're calling `make_plane` and the code range doesn't end at
// `0` it's not a default plane, but a custom one from the
// offsetPlane standard library function.
return Ok(vec![Artifact::Plane(Plane {
id,
path_ids: Vec::new(),
code_ref: CodeRef { range, path_to_node },
})]);
}
ModelingCmd::EnableSketchMode(_) => {
let current_plane_id = current_plane_id.ok_or_else(|| {
KclError::internal(format!(
"Expected a current plane ID when processing EnableSketchMode command, but we have none: {id:?}"
))
})?;
let existing_plane = artifacts.get(&ArtifactId::new(current_plane_id));
match existing_plane {
Some(Artifact::Wall(wall)) => {
return Ok(vec![Artifact::Wall(Wall {
id: current_plane_id.into(),
seg_id: wall.seg_id,
edge_cut_edge_ids: wall.edge_cut_edge_ids.clone(),
sweep_id: wall.sweep_id,
path_ids: wall.path_ids.clone(),
})]);
}
Some(_) | None => {
let path_ids = match existing_plane {
Some(Artifact::Plane(Plane { path_ids, .. })) => path_ids.clone(),
_ => Vec::new(),
};
return Ok(vec![Artifact::Plane(Plane {
id: current_plane_id.into(),
path_ids,
code_ref: CodeRef { range, path_to_node },
})]);
}
}
}
ModelingCmd::StartPath(_) => {
let mut return_arr = Vec::new();
let current_plane_id = current_plane_id.ok_or_else(|| {
KclError::internal(format!(
"Expected a current plane ID when processing StartPath command, but we have none: {id:?}"
))
})?;
return_arr.push(Artifact::Path(Path {
id,
plane_id: current_plane_id.into(),
seg_ids: Vec::new(),
sweep_id: None,
solid2d_id: None,
code_ref: CodeRef { range, path_to_node },
}));
let plane = artifacts.get(&ArtifactId::new(current_plane_id));
if let Some(Artifact::Plane(plane)) = plane {
let code_ref = plane.code_ref.clone();
return_arr.push(Artifact::Plane(Plane {
id: current_plane_id.into(),
path_ids: vec![id],
code_ref,
}));
}
if let Some(Artifact::Wall(wall)) = plane {
return_arr.push(Artifact::Wall(Wall {
id: current_plane_id.into(),
seg_id: wall.seg_id,
edge_cut_edge_ids: wall.edge_cut_edge_ids.clone(),
sweep_id: wall.sweep_id,
path_ids: vec![id],
}));
}
return Ok(return_arr);
}
ModelingCmd::ClosePath(_) | ModelingCmd::ExtendPath(_) => {
let path_id = ArtifactId::new(match cmd {
ModelingCmd::ClosePath(c) => c.path_id,
ModelingCmd::ExtendPath(e) => e.path.into(),
_ => unreachable!(),
});
let mut return_arr = Vec::new();
return_arr.push(Artifact::Segment(Segment {
id,
path_id,
surface_id: None,
edge_ids: Vec::new(),
edge_cut_id: None,
code_ref: CodeRef { range, path_to_node },
}));
let path = artifacts.get(&path_id);
if let Some(Artifact::Path(path)) = path {
let mut new_path = path.clone();
new_path.seg_ids = vec![id];
return_arr.push(Artifact::Path(new_path));
}
if let OkModelingCmdResponse::ClosePath(close_path) = response {
return_arr.push(Artifact::Solid2d(Solid2d {
id: close_path.face_id.into(),
path_id,
}));
if let Some(Artifact::Path(path)) = path {
let mut new_path = path.clone();
new_path.solid2d_id = Some(close_path.face_id.into());
return_arr.push(Artifact::Path(new_path));
}
}
return Ok(return_arr);
}
ModelingCmd::Extrude(kcmc::Extrude { target, .. })
| ModelingCmd::Revolve(kcmc::Revolve { target, .. })
| ModelingCmd::Sweep(kcmc::Sweep { target, .. }) => {
let sub_type = match cmd {
ModelingCmd::Extrude(_) => SweepSubType::Extrusion,
ModelingCmd::Revolve(_) => SweepSubType::Revolve,
ModelingCmd::Sweep(_) => SweepSubType::Sweep,
_ => unreachable!(),
};
let mut return_arr = Vec::new();
let target = ArtifactId::from(target);
return_arr.push(Artifact::Sweep(Sweep {
id,
sub_type,
path_id: target,
surface_ids: Vec::new(),
edge_ids: Vec::new(),
code_ref: CodeRef { range, path_to_node },
}));
let path = artifacts.get(&target);
if let Some(Artifact::Path(path)) = path {
let mut new_path = path.clone();
new_path.sweep_id = Some(id);
return_arr.push(Artifact::Path(new_path));
}
return Ok(return_arr);
}
ModelingCmd::Loft(loft_cmd) => {
let OkModelingCmdResponse::Loft(_) = response else {
return Ok(Vec::new());
};
let mut return_arr = Vec::new();
return_arr.push(Artifact::Sweep(Sweep {
id,
sub_type: SweepSubType::Loft,
// TODO: Using the first one. Make sure to revisit this
// choice, don't think it matters for now.
path_id: ArtifactId::new(*loft_cmd.section_ids.first().ok_or_else(|| {
KclError::internal(format!(
"Expected at least one section ID in Loft command: {id:?}; cmd={cmd:?}"
))
})?),
surface_ids: Vec::new(),
edge_ids: Vec::new(),
code_ref: CodeRef { range, path_to_node },
}));
for section_id in &loft_cmd.section_ids {
let path = artifacts.get(&ArtifactId::new(*section_id));
if let Some(Artifact::Path(path)) = path {
let mut new_path = path.clone();
new_path.sweep_id = Some(id);
return_arr.push(Artifact::Path(new_path));
}
}
return Ok(return_arr);
}
ModelingCmd::Solid3dGetExtrusionFaceInfo(_) => {
let OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(face_info) = response else {
return Ok(Vec::new());
};
let mut return_arr = Vec::new();
let mut last_path = None;
for face in &face_info.faces {
if face.cap != ExtrusionFaceCapType::None {
continue;
}
let Some(curve_id) = face.curve_id.map(ArtifactId::new) else {
continue;
};
let Some(face_id) = face.face_id.map(ArtifactId::new) else {
continue;
};
let Some(Artifact::Segment(seg)) = artifacts.get(&curve_id) else {
continue;
};
let Some(Artifact::Path(path)) = artifacts.get(&seg.path_id) else {
continue;
};
last_path = Some(path);
let path_sweep_id = path.sweep_id.ok_or_else(|| {
KclError::internal(format!(
"Expected a sweep ID on the path when processing Solid3dGetExtrusionFaceInfo command, but we have none: {id:?}, {path:?}"
))
})?;
return_arr.push(Artifact::Wall(Wall {
id: face_id,
seg_id: curve_id,
edge_cut_edge_ids: Vec::new(),
sweep_id: path_sweep_id,
path_ids: vec![],
}));
let mut new_seg = seg.clone();
new_seg.surface_id = Some(face_id);
return_arr.push(Artifact::Segment(new_seg));
if let Some(Artifact::Sweep(sweep)) = path.sweep_id.and_then(|id| artifacts.get(&id)) {
let mut new_sweep = sweep.clone();
new_sweep.surface_ids = vec![face_id];
return_arr.push(Artifact::Sweep(new_sweep));
}
}
if let Some(path) = last_path {
for face in &face_info.faces {
let sub_type = match face.cap {
ExtrusionFaceCapType::Top => CapSubType::End,
ExtrusionFaceCapType::Bottom => CapSubType::Start,
ExtrusionFaceCapType::None | ExtrusionFaceCapType::Both => continue,
};
let Some(face_id) = face.face_id.map(ArtifactId::new) else {
continue;
};
let path_sweep_id = path.sweep_id.ok_or_else(|| {
KclError::internal(format!(
"Expected a sweep ID on the path when processing Solid3dGetExtrusionFaceInfo command, but we have none: {id:?}, {path:?}"
))
})?;
return_arr.push(Artifact::Cap(Cap {
id: face_id,
sub_type,
edge_cut_edge_ids: Vec::new(),
sweep_id: path_sweep_id,
path_ids: Vec::new(),
}));
let Some(Artifact::Sweep(sweep)) = artifacts.get(&path_sweep_id) else {
continue;
};
let mut new_sweep = sweep.clone();
new_sweep.surface_ids = vec![face_id];
return_arr.push(Artifact::Sweep(new_sweep));
}
}
return Ok(return_arr);
}
ModelingCmd::Solid3dGetNextAdjacentEdge(kcmc::Solid3dGetNextAdjacentEdge { face_id, edge_id, .. })
| ModelingCmd::Solid3dGetOppositeEdge(kcmc::Solid3dGetOppositeEdge { face_id, edge_id, .. }) => {
let sub_type = match cmd {
ModelingCmd::Solid3dGetNextAdjacentEdge(_) => SweepEdgeSubType::Adjacent,
ModelingCmd::Solid3dGetOppositeEdge(_) => SweepEdgeSubType::Opposite,
_ => unreachable!(),
};
let face_id = ArtifactId::new(*face_id);
let edge_id = ArtifactId::new(*edge_id);
let Some(Artifact::Wall(wall)) = artifacts.get(&face_id) else {
return Ok(Vec::new());
};
let Some(Artifact::Sweep(sweep)) = artifacts.get(&wall.sweep_id) else {
return Ok(Vec::new());
};
let Some(Artifact::Path(_)) = artifacts.get(&sweep.path_id) else {
return Ok(Vec::new());
};
let Some(Artifact::Segment(segment)) = artifacts.get(&edge_id) else {
return Ok(Vec::new());
};
let response_edge_id = match response {
OkModelingCmdResponse::Solid3dGetNextAdjacentEdge(r) => {
let Some(edge_id) = r.edge else {
return Err(KclError::internal(format!(
"Expected Solid3dGetNextAdjacentEdge response to have an edge ID, but found none: id={id:?}, {response:?}"
)));
};
edge_id.into()
}
OkModelingCmdResponse::Solid3dGetOppositeEdge(r) => r.edge.into(),
_ => {
return Err(KclError::internal(format!(
"Expected Solid3dGetNextAdjacentEdge or Solid3dGetOppositeEdge response, but got: id={id:?}, {response:?}"
)));
}
};
let mut return_arr = Vec::new();
return_arr.push(Artifact::SweepEdge(SweepEdge {
id: response_edge_id,
sub_type,
seg_id: edge_id,
sweep_id: sweep.id,
}));
let mut new_segment = segment.clone();
new_segment.edge_ids = vec![response_edge_id];
return_arr.push(Artifact::Segment(new_segment));
let mut new_sweep = sweep.clone();
new_sweep.edge_ids = vec![response_edge_id];
return_arr.push(Artifact::Sweep(new_sweep));
return Ok(return_arr);
}
ModelingCmd::Solid3dFilletEdge(cmd) => {
let mut return_arr = Vec::new();
return_arr.push(Artifact::EdgeCut(EdgeCut {
id,
sub_type: cmd.cut_type.into(),
consumed_edge_id: cmd.edge_id.into(),
edge_ids: Vec::new(),
surface_id: None,
code_ref: CodeRef { range, path_to_node },
}));
let consumed_edge = artifacts.get(&ArtifactId::new(cmd.edge_id));
if let Some(Artifact::Segment(consumed_edge)) = consumed_edge {
let mut new_segment = consumed_edge.clone();
new_segment.edge_cut_id = Some(id);
return_arr.push(Artifact::Segment(new_segment));
} else {
// TODO: Handle other types like SweepEdge.
}
return Ok(return_arr);
}
_ => {}
}
Ok(Vec::new())
} }

View File

@ -0,0 +1,527 @@
//! Tests for the artifact graph that convert it to Mermaid diagrams.
use std::fmt::Write;
use super::*;
type NodeId = u32;
type Edges = IndexMap<(NodeId, NodeId), EdgeInfo>;
#[derive(Debug, Clone, PartialEq, Eq)]
struct EdgeInfo {
direction: EdgeDirection,
flow: EdgeFlow,
kind: EdgeKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum EdgeDirection {
Forward,
Backward,
Bidirectional,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum EdgeFlow {
SourceToTarget,
TargetToSource,
}
impl EdgeFlow {
#[must_use]
fn reverse(&self) -> EdgeFlow {
match self {
EdgeFlow::SourceToTarget => EdgeFlow::TargetToSource,
EdgeFlow::TargetToSource => EdgeFlow::SourceToTarget,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum EdgeKind {
PathToSweep,
Other,
}
impl EdgeDirection {
#[must_use]
fn merge(&self, other: EdgeDirection) -> EdgeDirection {
match self {
EdgeDirection::Forward => match other {
EdgeDirection::Forward => EdgeDirection::Forward,
EdgeDirection::Backward => EdgeDirection::Bidirectional,
EdgeDirection::Bidirectional => EdgeDirection::Bidirectional,
},
EdgeDirection::Backward => match other {
EdgeDirection::Forward => EdgeDirection::Bidirectional,
EdgeDirection::Backward => EdgeDirection::Backward,
EdgeDirection::Bidirectional => EdgeDirection::Bidirectional,
},
EdgeDirection::Bidirectional => EdgeDirection::Bidirectional,
}
}
}
impl Artifact {
/// The IDs pointing back to prior nodes in a depth-first traversal of
/// the graph. This should be disjoint with `child_ids`.
pub(crate) fn back_edges(&self) -> Vec<ArtifactId> {
match self {
Artifact::Plane(_) => Vec::new(),
Artifact::Path(a) => vec![a.plane_id],
Artifact::Segment(a) => vec![a.path_id],
Artifact::Solid2d(a) => vec![a.path_id],
Artifact::StartSketchOnFace { face_id, .. } => vec![face_id.into()],
Artifact::StartSketchOnPlane { plane_id, .. } => vec![plane_id.into()],
Artifact::Sweep(a) => vec![a.path_id],
Artifact::Wall(a) => vec![a.seg_id, a.sweep_id],
Artifact::Cap(a) => vec![a.sweep_id],
Artifact::SweepEdge(a) => vec![a.seg_id, a.sweep_id],
Artifact::EdgeCut(a) => vec![a.consumed_edge_id],
Artifact::EdgeCutEdge(a) => vec![a.edge_cut_id],
}
}
/// The child IDs of this artifact, used to do a depth-first traversal of
/// the graph.
pub(crate) fn child_ids(&self) -> Vec<ArtifactId> {
match self {
Artifact::Plane(a) => a.path_ids.clone(),
Artifact::Path(a) => {
// Note: Don't include these since they're parents: plane_id.
let mut ids = a.seg_ids.clone();
if let Some(sweep_id) = a.sweep_id {
ids.push(sweep_id);
}
if let Some(solid2d_id) = a.solid2d_id {
ids.push(solid2d_id);
}
ids
}
Artifact::Segment(a) => {
// Note: Don't include these since they're parents: path_id.
let mut ids = Vec::new();
if let Some(surface_id) = a.surface_id {
ids.push(surface_id);
}
ids.extend(&a.edge_ids);
if let Some(edge_cut_id) = a.edge_cut_id {
ids.push(edge_cut_id);
}
ids
}
Artifact::Solid2d(_) => {
// Note: Don't include these since they're parents: path_id.
Vec::new()
}
Artifact::StartSketchOnFace { .. } => Vec::new(),
Artifact::StartSketchOnPlane { .. } => Vec::new(),
Artifact::Sweep(a) => {
// Note: Don't include these since they're parents: path_id.
let mut ids = Vec::new();
ids.extend(&a.surface_ids);
ids.extend(&a.edge_ids);
ids
}
Artifact::Wall(a) => {
// Note: Don't include these since they're parents: seg_id,
// sweep_id.
let mut ids = Vec::new();
ids.extend(&a.edge_cut_edge_ids);
ids.extend(&a.path_ids);
ids
}
Artifact::Cap(a) => {
// Note: Don't include these since they're parents: sweep_id.
let mut ids = Vec::new();
ids.extend(&a.edge_cut_edge_ids);
ids.extend(&a.path_ids);
ids
}
Artifact::SweepEdge(_) => {
// Note: Don't include these since they're parents: seg_id,
// sweep_id.
Vec::new()
}
Artifact::EdgeCut(a) => {
// Note: Don't include these since they're parents:
// consumed_edge_id.
let mut ids = Vec::new();
ids.extend(&a.edge_ids);
if let Some(surface_id) = a.surface_id {
ids.push(surface_id);
}
ids
}
Artifact::EdgeCutEdge(a) => {
// Note: Don't include these since they're parents: edge_cut_id.
vec![a.surface_id]
}
}
}
}
impl ArtifactGraph {
/// Output the Mermaid flowchart for the artifact graph.
pub(crate) fn to_mermaid_flowchart(&self) -> Result<String, std::fmt::Error> {
let mut output = String::new();
output.push_str("```mermaid\n");
output.push_str("flowchart LR\n");
let mut next_id = 1_u32;
let mut stable_id_map = FnvHashMap::default();
for id in self.map.keys() {
stable_id_map.insert(*id, next_id);
next_id = next_id.checked_add(1).unwrap();
}
// Output all nodes first since edge order can change how Mermaid
// lays out nodes. This is also where we output more details about
// the nodes, like their labels.
self.flowchart_nodes(&mut output, &stable_id_map, " ")?;
self.flowchart_edges(&mut output, &stable_id_map, " ")?;
output.push_str("```\n");
Ok(output)
}
/// Output the Mermaid flowchart nodes, one for each artifact.
fn flowchart_nodes<W: Write>(
&self,
output: &mut W,
stable_id_map: &FnvHashMap<ArtifactId, NodeId>,
prefix: &str,
) -> std::fmt::Result {
// Artifact ID of the path is the key. The value is a list of
// artifact IDs in that group.
let mut groups = IndexMap::new();
let mut ungrouped = Vec::new();
for artifact in self.map.values() {
let id = artifact.id();
let grouped = match artifact {
Artifact::Plane(_) => false,
Artifact::Path(_) => {
groups.entry(id).or_insert_with(Vec::new).push(id);
true
}
Artifact::Segment(segment) => {
let path_id = segment.path_id;
groups.entry(path_id).or_insert_with(Vec::new).push(id);
true
}
Artifact::Solid2d(solid2d) => {
let path_id = solid2d.path_id;
groups.entry(path_id).or_insert_with(Vec::new).push(id);
true
}
Artifact::StartSketchOnFace { .. }
| Artifact::StartSketchOnPlane { .. }
| Artifact::Sweep(_)
| Artifact::Wall(_)
| Artifact::Cap(_)
| Artifact::SweepEdge(_)
| Artifact::EdgeCut(_)
| Artifact::EdgeCutEdge(_) => false,
};
if !grouped {
ungrouped.push(id);
}
}
for (group_id, artifact_ids) in groups {
let group_id = *stable_id_map.get(&group_id).unwrap();
writeln!(output, "{prefix}subgraph path{group_id} [Path]")?;
let indented = format!("{} ", prefix);
for artifact_id in artifact_ids {
let artifact = self.map.get(&artifact_id).unwrap();
let id = *stable_id_map.get(&artifact_id).unwrap();
self.flowchart_node(output, artifact, id, &indented)?;
}
writeln!(output, "{prefix}end")?;
}
for artifact_id in ungrouped {
let artifact = self.map.get(&artifact_id).unwrap();
let id = *stable_id_map.get(&artifact_id).unwrap();
self.flowchart_node(output, artifact, id, prefix)?;
}
Ok(())
}
fn flowchart_node<W: Write>(
&self,
output: &mut W,
artifact: &Artifact,
id: NodeId,
prefix: &str,
) -> std::fmt::Result {
// For now, only showing the source range.
fn code_ref_display(code_ref: &CodeRef) -> [usize; 3] {
range_display(code_ref.range)
}
fn range_display(range: SourceRange) -> [usize; 3] {
[range.start(), range.end(), range.module_id().as_usize()]
}
match artifact {
Artifact::Plane(plane) => {
writeln!(
output,
"{prefix}{}[\"Plane<br>{:?}\"]",
id,
code_ref_display(&plane.code_ref)
)?;
}
Artifact::Path(path) => {
writeln!(
output,
"{prefix}{}[\"Path<br>{:?}\"]",
id,
code_ref_display(&path.code_ref)
)?;
}
Artifact::Segment(segment) => {
writeln!(
output,
"{prefix}{}[\"Segment<br>{:?}\"]",
id,
code_ref_display(&segment.code_ref)
)?;
}
Artifact::Solid2d(_solid2d) => {
writeln!(output, "{prefix}{}[Solid2d]", id)?;
}
Artifact::StartSketchOnFace { source_range, .. } => {
writeln!(
output,
"{prefix}{}[\"StartSketchOnFace<br>{:?}\"]",
id,
range_display(*source_range)
)?;
}
Artifact::StartSketchOnPlane { source_range, .. } => {
writeln!(
output,
"{prefix}{}[\"StartSketchOnPlane<br>{:?}\"]",
id,
range_display(*source_range)
)?;
}
Artifact::Sweep(sweep) => {
writeln!(
output,
"{prefix}{}[\"Sweep {:?}<br>{:?}\"]",
id,
sweep.sub_type,
code_ref_display(&sweep.code_ref)
)?;
}
Artifact::Wall(_wall) => {
writeln!(output, "{prefix}{}[Wall]", id)?;
}
Artifact::Cap(cap) => {
writeln!(output, "{prefix}{}[\"Cap {:?}\"]", id, cap.sub_type)?;
}
Artifact::SweepEdge(sweep_edge) => {
writeln!(output, "{prefix}{}[\"SweepEdge {:?}\"]", id, sweep_edge.sub_type)?;
}
Artifact::EdgeCut(edge_cut) => {
writeln!(
output,
"{prefix}{}[\"EdgeCut {:?}<br>{:?}\"]",
id,
edge_cut.sub_type,
code_ref_display(&edge_cut.code_ref)
)?;
}
Artifact::EdgeCutEdge(_edge_cut_edge) => {
writeln!(output, "{prefix}{}[EdgeCutEdge]", id)?;
}
}
Ok(())
}
fn flowchart_edges<W: Write>(
&self,
output: &mut W,
stable_id_map: &FnvHashMap<ArtifactId, NodeId>,
prefix: &str,
) -> Result<(), std::fmt::Error> {
// Mermaid will display two edges in either direction, even using
// the `---` edge type. So we need to deduplicate them.
fn add_unique_edge(edges: &mut Edges, source_id: NodeId, target_id: NodeId, flow: EdgeFlow, kind: EdgeKind) {
if source_id == target_id {
// Self edge. Skip it.
return;
}
// The key is the node IDs in canonical order.
let a = source_id.min(target_id);
let b = source_id.max(target_id);
let new_direction = if a == source_id {
EdgeDirection::Forward
} else {
EdgeDirection::Backward
};
let initial_flow = if a == source_id { flow } else { flow.reverse() };
let edge = edges.entry((a, b)).or_insert(EdgeInfo {
direction: new_direction,
flow: initial_flow,
kind,
});
// Merge with existing edge.
edge.direction = edge.direction.merge(new_direction);
}
// Collect all edges to deduplicate them.
let mut edges = IndexMap::default();
for artifact in self.map.values() {
let source_id = *stable_id_map.get(&artifact.id()).unwrap();
// In Mermaid, the textual order defines the rank, even though the
// edge arrow can go in either direction.
//
// Back edges: parent <- self
// Child edges: self -> child
for (target_id, flow) in artifact
.back_edges()
.into_iter()
.zip(std::iter::repeat(EdgeFlow::TargetToSource))
.chain(
artifact
.child_ids()
.into_iter()
.zip(std::iter::repeat(EdgeFlow::SourceToTarget)),
)
{
let Some(target) = self.map.get(&target_id) else {
continue;
};
let edge_kind = match (artifact, target) {
(Artifact::Path(_), Artifact::Sweep(_)) | (Artifact::Sweep(_), Artifact::Path(_)) => {
EdgeKind::PathToSweep
}
_ => EdgeKind::Other,
};
let target_id = *stable_id_map.get(&target_id).unwrap();
add_unique_edge(&mut edges, source_id, target_id, flow, edge_kind);
}
}
// Output the edges.
for ((source_id, target_id), edge) in edges {
let extra = match edge.kind {
// Extra length. This is needed to make the graph layout more
// legible. Without it, the sweep will be at the same rank as
// the path's segments, and the sweep's edges overlap with the
// segment edges a lot.
EdgeKind::PathToSweep => "-",
EdgeKind::Other => "",
};
match edge.flow {
EdgeFlow::SourceToTarget => match edge.direction {
EdgeDirection::Forward => {
writeln!(output, "{prefix}{source_id} x{}--> {}", extra, target_id)?;
}
EdgeDirection::Backward => {
writeln!(output, "{prefix}{source_id} <{}--x {}", extra, target_id)?;
}
EdgeDirection::Bidirectional => {
writeln!(output, "{prefix}{source_id} {}--- {}", extra, target_id)?;
}
},
EdgeFlow::TargetToSource => match edge.direction {
EdgeDirection::Forward => {
writeln!(output, "{prefix}{target_id} x{}--> {}", extra, source_id)?;
}
EdgeDirection::Backward => {
writeln!(output, "{prefix}{target_id} <{}--x {}", extra, source_id)?;
}
EdgeDirection::Bidirectional => {
writeln!(output, "{prefix}{target_id} {}--- {}", extra, source_id)?;
}
},
}
}
Ok(())
}
/// Output the Mermaid mind map for the artifact graph.
///
/// This is sometimes easier to read than the flowchart. But since it
/// does a depth-first traversal starting from all the planes, it may
/// not include all the artifacts. It also doesn't show edge direction.
/// It's useful for a high-level overview of the graph, not for
/// including all the information.
pub(crate) fn to_mermaid_mind_map(&self) -> Result<String, std::fmt::Error> {
let mut output = String::new();
output.push_str("```mermaid\n");
output.push_str("mindmap\n");
output.push_str(" root\n");
for (_, artifact) in &self.map {
// Only the planes are roots.
let Artifact::Plane(_) = artifact else {
continue;
};
self.mind_map_artifact(&mut output, artifact, " ")?;
}
output.push_str("```\n");
Ok(output)
}
fn mind_map_artifact<W: Write>(&self, output: &mut W, artifact: &Artifact, prefix: &str) -> std::fmt::Result {
match artifact {
Artifact::Plane(_plane) => {
writeln!(output, "{prefix}Plane")?;
}
Artifact::Path(_path) => {
writeln!(output, "{prefix}Path")?;
}
Artifact::Segment(_segment) => {
writeln!(output, "{prefix}Segment")?;
}
Artifact::Solid2d(_solid2d) => {
writeln!(output, "{prefix}Solid2d")?;
}
Artifact::StartSketchOnFace { .. } => {
writeln!(output, "{prefix}StartSketchOnFace")?;
}
Artifact::StartSketchOnPlane { .. } => {
writeln!(output, "{prefix}StartSketchOnPlane")?;
}
Artifact::Sweep(sweep) => {
writeln!(output, "{prefix}Sweep {:?}", sweep.sub_type)?;
}
Artifact::Wall(_wall) => {
writeln!(output, "{prefix}Wall")?;
}
Artifact::Cap(cap) => {
writeln!(output, "{prefix}Cap {:?}", cap.sub_type)?;
}
Artifact::SweepEdge(sweep_edge) => {
writeln!(output, "{prefix}SweepEdge {:?}", sweep_edge.sub_type,)?;
}
Artifact::EdgeCut(edge_cut) => {
writeln!(output, "{prefix}EdgeCut {:?}", edge_cut.sub_type)?;
}
Artifact::EdgeCutEdge(_edge_cut_edge) => {
writeln!(output, "{prefix}EdgeCutEdge")?;
}
}
for child_id in artifact.child_ids() {
let Some(child_artifact) = self.map.get(&child_id) else {
continue;
};
self.mind_map_artifact(output, child_artifact, &format!("{} ", prefix))?;
}
Ok(())
}
}

View File

@ -8,7 +8,7 @@ use crate::{
}; };
/// Information for the caching an AST and smartly re-executing it if we can. /// Information for the caching an AST and smartly re-executing it if we can.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CacheInformation { pub struct CacheInformation {
/// The old information. /// The old information.
pub old: Option<OldAstState>, pub old: Option<OldAstState>,
@ -17,7 +17,7 @@ pub struct CacheInformation {
} }
/// The old ast and program memory. /// The old ast and program memory.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct OldAstState { pub struct OldAstState {
/// The ast. /// The ast.
pub ast: Node<Program>, pub ast: Node<Program>,

View File

@ -3,6 +3,7 @@
use std::{path::PathBuf, sync::Arc}; use std::{path::PathBuf, sync::Arc};
use anyhow::Result; use anyhow::Result;
use artifact::build_artifact_graph;
use async_recursion::async_recursion; use async_recursion::async_recursion;
use indexmap::IndexMap; use indexmap::IndexMap;
use kcmc::{ use kcmc::{
@ -11,8 +12,8 @@ use kcmc::{
websocket::{ModelingSessionData, OkWebSocketResponseData}, websocket::{ModelingSessionData, OkWebSocketResponseData},
ImageFormat, ModelingCmd, ImageFormat, ModelingCmd,
}; };
use kittycad_modeling_cmds as kcmc;
use kittycad_modeling_cmds::length_unit::LengthUnit; use kittycad_modeling_cmds::length_unit::LengthUnit;
use kittycad_modeling_cmds::{self as kcmc, websocket::WebSocketResponse};
use parse_display::{Display, FromStr}; use parse_display::{Display, FromStr};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -49,18 +50,18 @@ use crate::{
}; };
// Re-exports. // Re-exports.
pub use artifact::{Artifact, ArtifactCommand, ArtifactId, ArtifactInner}; pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, ArtifactId};
pub use cad_op::Operation; pub use cad_op::Operation;
/// State for executing a program. /// State for executing a program.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] #[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ExecState { pub struct ExecState {
pub global: GlobalState, pub global: GlobalState,
pub mod_local: ModuleState, pub mod_local: ModuleState,
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] #[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct GlobalState { pub struct GlobalState {
/// The stable artifact ID generator. /// The stable artifact ID generator.
@ -75,6 +76,13 @@ pub struct GlobalState {
/// These are accumulated in the [`ExecutorContext`] but moved here for /// These are accumulated in the [`ExecutorContext`] but moved here for
/// convenience of the execution cache. /// convenience of the execution cache.
pub artifact_commands: Vec<ArtifactCommand>, pub artifact_commands: Vec<ArtifactCommand>,
/// Responses from the engine for `artifact_commands`. We need to cache
/// this so that we can build the artifact graph. These are accumulated in
/// the [`ExecutorContext`] but moved here for convenience of the execution
/// cache.
pub artifact_responses: IndexMap<Uuid, WebSocketResponse>,
/// Output artifact graph.
pub artifact_graph: ArtifactGraph,
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
@ -113,6 +121,8 @@ pub struct ExecOutcome {
pub artifacts: IndexMap<ArtifactId, Artifact>, pub artifacts: IndexMap<ArtifactId, Artifact>,
/// Output commands to allow building the artifact graph by the caller. /// Output commands to allow building the artifact graph by the caller.
pub artifact_commands: Vec<ArtifactCommand>, pub artifact_commands: Vec<ArtifactCommand>,
/// Output artifact graph.
pub artifact_graph: ArtifactGraph,
} }
impl ExecState { impl ExecState {
@ -149,6 +159,7 @@ impl ExecState {
operations: self.mod_local.operations, operations: self.mod_local.operations,
artifacts: self.global.artifacts, artifacts: self.global.artifacts,
artifact_commands: self.global.artifact_commands, artifact_commands: self.global.artifact_commands,
artifact_graph: self.global.artifact_graph,
} }
} }
@ -165,7 +176,7 @@ impl ExecState {
} }
pub fn add_artifact(&mut self, artifact: Artifact) { pub fn add_artifact(&mut self, artifact: Artifact) {
let id = artifact.id; let id = artifact.id();
self.global.artifacts.insert(id, artifact); self.global.artifacts.insert(id, artifact);
} }
@ -216,6 +227,8 @@ impl GlobalState {
module_infos: Default::default(), module_infos: Default::default(),
artifacts: Default::default(), artifacts: Default::default(),
artifact_commands: Default::default(), artifact_commands: Default::default(),
artifact_responses: Default::default(),
artifact_graph: Default::default(),
}; };
// TODO(#4434): Use the top-level file's path. // TODO(#4434): Use the top-level file's path.
@ -2239,22 +2252,56 @@ impl ExecutorContext {
.await .await
.map_err(KclErrorWithOutputs::no_outputs)?; .map_err(KclErrorWithOutputs::no_outputs)?;
self.inner_execute(&cache_result.program, exec_state, crate::execution::BodyType::Root) self.execute_and_build_graph(&cache_result.program, exec_state)
.await .await
.map_err(|e| { .map_err(|e| {
KclErrorWithOutputs::new( KclErrorWithOutputs::new(
e, e,
exec_state.mod_local.operations.clone(), exec_state.mod_local.operations.clone(),
self.engine.take_artifact_commands(), exec_state.global.artifact_commands.clone(),
exec_state.global.artifact_graph.clone(),
) )
})?; })?;
// Move the artifact commands to simplify cache management.
let session_data = self.engine.get_session_data();
Ok(session_data)
}
/// Execute an AST's program and build auxiliary outputs like the artifact
/// graph.
async fn execute_and_build_graph<'a>(
&self,
program: NodeRef<'a, crate::parsing::ast::types::Program>,
exec_state: &mut ExecState,
) -> Result<Option<KclValue>, KclError> {
// Don't early return! We need to build other outputs regardless of
// whether execution failed.
let exec_result = self
.inner_execute(program, exec_state, crate::execution::BodyType::Root)
.await;
// Move the artifact commands and responses to simplify cache management
// and error creation.
exec_state exec_state
.global .global
.artifact_commands .artifact_commands
.extend(self.engine.take_artifact_commands()); .extend(self.engine.take_artifact_commands());
let session_data = self.engine.get_session_data(); exec_state.global.artifact_responses.extend(self.engine.responses());
Ok(session_data) // Build the artifact graph.
match build_artifact_graph(
&exec_state.global.artifact_commands,
&exec_state.global.artifact_responses,
program,
&exec_state.global.artifacts,
) {
Ok(artifact_graph) => {
exec_state.global.artifact_graph = artifact_graph;
exec_result
}
Err(err) => {
// Prefer the exec error.
exec_result.and(Err(err))
}
}
} }
/// Execute an AST's program. /// Execute an AST's program.

View File

@ -91,12 +91,12 @@ async fn execute(test_name: &str, render_to_png: bool) {
) )
.await; .await;
match exec_res { match exec_res {
Ok((program_memory, ops, artifact_commands, png)) => { Ok((exec_state, png)) => {
if render_to_png { if render_to_png {
twenty_twenty::assert_image(format!("tests/{test_name}/rendered_model.png"), &png, 0.99); twenty_twenty::assert_image(format!("tests/{test_name}/rendered_model.png"), &png, 0.99);
} }
assert_snapshot(test_name, "Program memory after executing", || { assert_snapshot(test_name, "Program memory after executing", || {
insta::assert_json_snapshot!("program_memory", program_memory, { insta::assert_json_snapshot!("program_memory", exec_state.mod_local.memory, {
".environments[].**[].from[]" => rounded_redaction(4), ".environments[].**[].from[]" => rounded_redaction(4),
".environments[].**[].to[]" => rounded_redaction(4), ".environments[].**[].to[]" => rounded_redaction(4),
".environments[].**[].x[]" => rounded_redaction(4), ".environments[].**[].x[]" => rounded_redaction(4),
@ -105,15 +105,35 @@ async fn execute(test_name: &str, render_to_png: bool) {
}); });
}); });
assert_snapshot(test_name, "Operations executed", || { assert_snapshot(test_name, "Operations executed", || {
insta::assert_json_snapshot!("ops", ops); insta::assert_json_snapshot!("ops", exec_state.mod_local.operations);
}); });
assert_snapshot(test_name, "Artifact commands", || { assert_snapshot(test_name, "Artifact commands", || {
insta::assert_json_snapshot!("artifact_commands", artifact_commands, { insta::assert_json_snapshot!("artifact_commands", exec_state.global.artifact_commands, {
"[].command.segment.*.x" => rounded_redaction(4), "[].command.segment.*.x" => rounded_redaction(4),
"[].command.segment.*.y" => rounded_redaction(4), "[].command.segment.*.y" => rounded_redaction(4),
"[].command.segment.*.z" => rounded_redaction(4), "[].command.segment.*.z" => rounded_redaction(4),
}); });
}); });
assert_snapshot(test_name, "Artifact graph flowchart", || {
let flowchart = exec_state
.global
.artifact_graph
.to_mermaid_flowchart()
.unwrap_or_else(|e| format!("Failed to convert artifact graph to mind map: {e}"));
// Change the snapshot suffix so that it is rendered as a
// Markdown file in GitHub.
insta::assert_binary_snapshot!("artifact_graph_flowchart.md", flowchart.as_bytes().to_owned());
});
assert_snapshot(test_name, "Artifact graph mind map", || {
let mind_map = exec_state
.global
.artifact_graph
.to_mermaid_mind_map()
.unwrap_or_else(|e| format!("Failed to convert artifact graph to mind map: {e}"));
// Change the snapshot suffix so that it is rendered as a
// Markdown file in GitHub.
insta::assert_binary_snapshot!("artifact_graph_mind_map.md", mind_map.as_bytes().to_owned());
});
} }
Err(e) => { Err(e) => {
match e.error { match e.error {
@ -177,6 +197,90 @@ mod cube {
super::execute(TEST_NAME, true).await super::execute(TEST_NAME, true).await
} }
} }
mod artifact_graph_example_code1 {
const TEST_NAME: &str = "artifact_graph_example_code1";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}
mod artifact_graph_example_code_no_3d {
const TEST_NAME: &str = "artifact_graph_example_code_no_3d";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}
mod artifact_graph_example_code_offset_planes {
const TEST_NAME: &str = "artifact_graph_example_code_offset_planes";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}
mod artifact_graph_sketch_on_face_etc {
const TEST_NAME: &str = "artifact_graph_sketch_on_face_etc";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}
mod helix_ccw { mod helix_ccw {
const TEST_NAME: &str = "helix_ccw"; const TEST_NAME: &str = "helix_ccw";

View File

@ -23,6 +23,13 @@ impl ModuleId {
} }
} }
/// The first two items are the start and end points (byte offsets from the start of the file).
/// The third item is whether the source range belongs to the 'main' file, i.e., the file currently
/// being rendered/displayed in the editor.
//
// Don't use a doc comment for the below since the above goes in the website docs.
// @see isTopLevelModule() in wasm.ts.
// TODO we need to handle modules better in the frontend.
#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema, Hash, Eq)] #[derive(Debug, Default, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema, Hash, Eq)]
#[ts(export, type = "[number, number, number]")] #[ts(export, type = "[number, number, number]")]
pub struct SourceRange([usize; 3]); pub struct SourceRange([usize; 3]);
@ -58,6 +65,12 @@ impl SourceRange {
Self::default() Self::default()
} }
/// True if this is a source range that doesn't correspond to any source
/// code.
pub fn is_synthetic(&self) -> bool {
self.start() == 0 && self.end() == 0
}
/// Get the start of the range. /// Get the start of the range.
pub fn start(&self) -> usize { pub fn start(&self) -> usize {
self.0[0] self.0[0]

View File

@ -11,7 +11,7 @@ use parse_display::{Display, FromStr};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::execution::{Artifact, ArtifactId, ArtifactInner}; use crate::execution::{Artifact, ArtifactId};
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{ execution::{
@ -1079,9 +1079,9 @@ async fn inner_start_sketch_on(
SketchData::Plane(plane) => { SketchData::Plane(plane) => {
// Create artifact used only by the UI, not the engine. // Create artifact used only by the UI, not the engine.
let id = exec_state.next_uuid(); let id = exec_state.next_uuid();
exec_state.add_artifact(Artifact { exec_state.add_artifact(Artifact::StartSketchOnPlane {
id: ArtifactId::from(id), id: ArtifactId::from(id),
inner: ArtifactInner::StartSketchOnPlane { plane_id: plane.id }, plane_id: plane.id,
source_range: args.source_range, source_range: args.source_range,
}); });
@ -1098,9 +1098,9 @@ async fn inner_start_sketch_on(
// Create artifact used only by the UI, not the engine. // Create artifact used only by the UI, not the engine.
let id = exec_state.next_uuid(); let id = exec_state.next_uuid();
exec_state.add_artifact(Artifact { exec_state.add_artifact(Artifact::StartSketchOnFace {
id: ArtifactId::from(id), id: ArtifactId::from(id),
inner: ArtifactInner::StartSketchOnFace { face_id: face.id }, face_id: face.id,
source_range: args.source_range, source_range: args.source_range,
}); });

View File

@ -4,7 +4,7 @@ use std::path::PathBuf;
use crate::{ use crate::{
errors::ExecErrorWithState, errors::ExecErrorWithState,
execution::{new_zoo_client, ArtifactCommand, ExecutorContext, ExecutorSettings, Operation, ProgramMemory}, execution::{new_zoo_client, ExecutorContext, ExecutorSettings},
settings::types::UnitLength, settings::types::UnitLength,
ConnectionError, ExecError, ExecState, KclErrorWithOutputs, Program, ConnectionError, ExecError, ExecState, KclErrorWithOutputs, Program,
}; };
@ -39,16 +39,9 @@ pub async fn execute_and_snapshot_ast(
ast: Program, ast: Program,
units: UnitLength, units: UnitLength,
project_directory: Option<PathBuf>, project_directory: Option<PathBuf>,
) -> Result<(ProgramMemory, Vec<Operation>, Vec<ArtifactCommand>, image::DynamicImage), ExecErrorWithState> { ) -> Result<(ExecState, image::DynamicImage), ExecErrorWithState> {
let ctx = new_context(units, true, project_directory).await?; let ctx = new_context(units, true, project_directory).await?;
let res = do_execute_and_snapshot(&ctx, ast).await.map(|(state, snap)| { let res = do_execute_and_snapshot(&ctx, ast).await;
(
state.mod_local.memory,
state.mod_local.operations,
state.global.artifact_commands,
snap,
)
});
ctx.close().await; ctx.close().await;
res res
} }
@ -71,7 +64,7 @@ pub async fn execute_and_snapshot_no_auth(
async fn do_execute_and_snapshot( async fn do_execute_and_snapshot(
ctx: &ExecutorContext, ctx: &ExecutorContext,
program: Program, program: Program,
) -> Result<(crate::execution::ExecState, image::DynamicImage), ExecErrorWithState> { ) -> Result<(ExecState, image::DynamicImage), ExecErrorWithState> {
let mut exec_state = ExecState::new(&ctx.settings); let mut exec_state = ExecState::new(&ctx.settings);
let snapshot_png_bytes = ctx let snapshot_png_bytes = ctx
.execute_and_prepare_snapshot(&program, &mut exec_state) .execute_and_prepare_snapshot(&program, &mut exec_state)

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph flowchart add_lots.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,3 @@
```mermaid
flowchart LR
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph mind map add_lots.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,4 @@
```mermaid
mindmap
root
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph flowchart angled_line.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,82 @@
```mermaid
flowchart LR
subgraph path2 [Path]
2["Path<br>[35, 67, 0]"]
3["Segment<br>[73, 94, 0]"]
4["Segment<br>[100, 130, 0]"]
5["Segment<br>[136, 159, 0]"]
6["Segment<br>[165, 202, 0]"]
7["Segment<br>[208, 232, 0]"]
8["Segment<br>[238, 246, 0]"]
9[Solid2d]
end
1["Plane<br>[10, 29, 0]"]
10["Sweep Extrusion<br>[252, 265, 0]"]
11[Wall]
12[Wall]
13[Wall]
14[Wall]
15[Wall]
16[Wall]
17["Cap Start"]
18["Cap End"]
19["SweepEdge Opposite"]
20["SweepEdge Adjacent"]
21["SweepEdge Opposite"]
22["SweepEdge Adjacent"]
23["SweepEdge Opposite"]
24["SweepEdge Adjacent"]
25["SweepEdge Opposite"]
26["SweepEdge Adjacent"]
27["SweepEdge Opposite"]
28["SweepEdge Adjacent"]
29["SweepEdge Opposite"]
30["SweepEdge Adjacent"]
1 --- 2
2 --- 3
2 --- 4
2 --- 5
2 --- 6
2 --- 7
2 --- 8
2 ---- 10
2 --- 9
3 --- 16
3 --- 29
3 --- 30
4 --- 15
4 --- 27
4 --- 28
5 --- 14
5 --- 25
5 --- 26
6 --- 13
6 --- 23
6 --- 24
7 --- 12
7 --- 21
7 --- 22
8 --- 11
8 --- 19
8 --- 20
10 --- 11
10 --- 12
10 --- 13
10 --- 14
10 --- 15
10 --- 16
10 --- 17
10 --- 18
10 --- 19
10 --- 20
10 --- 21
10 --- 22
10 --- 23
10 --- 24
10 --- 25
10 --- 26
10 --- 27
10 --- 28
10 --- 29
10 --- 30
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph mind map angled_line.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,52 @@
```mermaid
mindmap
root
Plane
Path
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Sweep Extrusion
Wall
Wall
Wall
Wall
Wall
Wall
Cap Start
Cap End
SweepEdge Opposite
SweepEdge Adjacent
SweepEdge Opposite
SweepEdge Adjacent
SweepEdge Opposite
SweepEdge Adjacent
SweepEdge Opposite
SweepEdge Adjacent
SweepEdge Opposite
SweepEdge Adjacent
SweepEdge Opposite
SweepEdge Adjacent
Solid2d
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph flowchart array_elem_pop.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,3 @@
```mermaid
flowchart LR
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph mind map array_elem_pop.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,4 @@
```mermaid
mindmap
root
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph flowchart array_elem_push.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,3 @@
```mermaid
flowchart LR
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph mind map array_elem_push.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,4 @@
```mermaid
mindmap
root
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph flowchart array_range_expr.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,3 @@
```mermaid
flowchart LR
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph mind map array_range_expr.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,4 @@
```mermaid
mindmap
root
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph flowchart array_range_negative_expr.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,3 @@
```mermaid
flowchart LR
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph mind map array_range_negative_expr.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,4 @@
```mermaid
mindmap
root
```

View File

@ -0,0 +1,937 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact commands artifact_graph_example_code1.kcl
snapshot_kind: text
---
[
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": 1.0,
"y": 0.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 1.0,
"z": 0.0
},
"size": 100.0,
"clobber": false,
"hide": true
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "plane_set_color",
"plane_id": "[uuid]",
"color": {
"r": 0.7,
"g": 0.28,
"b": 0.28,
"a": 0.4
}
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": 0.0,
"y": 1.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 0.0,
"z": 1.0
},
"size": 100.0,
"clobber": false,
"hide": true
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "plane_set_color",
"plane_id": "[uuid]",
"color": {
"r": 0.28,
"g": 0.7,
"b": 0.28,
"a": 0.4
}
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": 1.0,
"y": 0.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 0.0,
"z": 1.0
},
"size": 100.0,
"clobber": false,
"hide": true
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "plane_set_color",
"plane_id": "[uuid]",
"color": {
"r": 0.28,
"g": 0.28,
"b": 0.7,
"a": 0.4
}
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": -1.0,
"y": 0.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 1.0,
"z": 0.0
},
"size": 100.0,
"clobber": false,
"hide": true
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": 0.0,
"y": -1.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 0.0,
"z": 1.0
},
"size": 100.0,
"clobber": false,
"hide": true
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": -1.0,
"y": 0.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 0.0,
"z": 1.0
},
"size": 100.0,
"clobber": false,
"hide": true
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "edge_lines_visible",
"hidden": false
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "set_scene_units",
"unit": "mm"
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "object_visible",
"object_id": "[uuid]",
"hidden": true
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "object_visible",
"object_id": "[uuid]",
"hidden": true
}
},
{
"cmdId": "[uuid]",
"range": [
12,
31,
0
],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": 1.0,
"y": 0.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 1.0,
"z": 0.0
},
"size": 60.0,
"clobber": false,
"hide": true
}
},
{
"cmdId": "[uuid]",
"range": [
37,
64,
0
],
"command": {
"type": "enable_sketch_mode",
"entity_id": "[uuid]",
"ortho": false,
"animated": false,
"adjust_camera": false,
"planar_normal": {
"x": 0.0,
"y": 0.0,
"z": 1.0
}
}
},
{
"cmdId": "[uuid]",
"range": [
37,
64,
0
],
"command": {
"type": "start_path"
}
},
{
"cmdId": "[uuid]",
"range": [
37,
64,
0
],
"command": {
"type": "move_path_pen",
"path": "[uuid]",
"to": {
"x": -5.0,
"y": -5.0,
"z": 0.0
}
}
},
{
"cmdId": "[uuid]",
"range": [
70,
86,
0
],
"command": {
"type": "extend_path",
"path": "[uuid]",
"segment": {
"type": "line",
"end": {
"x": 0.0,
"y": 10.0,
"z": 0.0
},
"relative": true
}
}
},
{
"cmdId": "[uuid]",
"range": [
92,
119,
0
],
"command": {
"type": "extend_path",
"path": "[uuid]",
"segment": {
"type": "line",
"end": {
"x": 10.55,
"y": 0.0,
"z": 0.0
},
"relative": true
}
}
},
{
"cmdId": "[uuid]",
"range": [
125,
150,
0
],
"command": {
"type": "extend_path",
"path": "[uuid]",
"segment": {
"type": "line",
"end": {
"x": 0.0,
"y": -10.0,
"z": 0.0
},
"relative": true
}
}
},
{
"cmdId": "[uuid]",
"range": [
156,
203,
0
],
"command": {
"type": "extend_path",
"path": "[uuid]",
"segment": {
"type": "line",
"end": {
"x": -5.0,
"y": -5.0,
"z": 0.0
},
"relative": false
}
}
},
{
"cmdId": "[uuid]",
"range": [
209,
217,
0
],
"command": {
"type": "close_path",
"path_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [
209,
217,
0
],
"command": {
"type": "sketch_mode_disable"
}
},
{
"cmdId": "[uuid]",
"range": [
231,
254,
0
],
"command": {
"type": "enable_sketch_mode",
"entity_id": "[uuid]",
"ortho": false,
"animated": false,
"adjust_camera": false,
"planar_normal": {
"x": 0.0,
"y": 0.0,
"z": 1.0
}
}
},
{
"cmdId": "[uuid]",
"range": [
231,
254,
0
],
"command": {
"type": "extrude",
"target": "[uuid]",
"distance": -10.0,
"faces": null
}
},
{
"cmdId": "[uuid]",
"range": [
231,
254,
0
],
"command": {
"type": "sketch_mode_disable"
}
},
{
"cmdId": "[uuid]",
"range": [
231,
254,
0
],
"command": {
"type": "object_bring_to_front",
"object_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [
231,
254,
0
],
"command": {
"type": "solid3d_get_extrusion_face_info",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [
231,
254,
0
],
"command": {
"type": "solid3d_get_opposite_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [
231,
254,
0
],
"command": {
"type": "solid3d_get_next_adjacent_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [
231,
254,
0
],
"command": {
"type": "solid3d_get_opposite_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [
231,
254,
0
],
"command": {
"type": "solid3d_get_next_adjacent_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [
231,
254,
0
],
"command": {
"type": "solid3d_get_opposite_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [
231,
254,
0
],
"command": {
"type": "solid3d_get_next_adjacent_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [
231,
254,
0
],
"command": {
"type": "solid3d_get_opposite_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [
231,
254,
0
],
"command": {
"type": "solid3d_get_next_adjacent_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [
260,
301,
0
],
"command": {
"type": "solid3d_fillet_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"radius": 5.0,
"tolerance": 0.0000001,
"cut_type": "fillet",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [
352,
379,
0
],
"command": {
"type": "enable_sketch_mode",
"entity_id": "[uuid]",
"ortho": false,
"animated": false,
"adjust_camera": false,
"planar_normal": null
}
},
{
"cmdId": "[uuid]",
"range": [
352,
379,
0
],
"command": {
"type": "start_path"
}
},
{
"cmdId": "[uuid]",
"range": [
352,
379,
0
],
"command": {
"type": "move_path_pen",
"path": "[uuid]",
"to": {
"x": -2.0,
"y": -6.0,
"z": 0.0
}
}
},
{
"cmdId": "[uuid]",
"range": [
385,
400,
0
],
"command": {
"type": "extend_path",
"path": "[uuid]",
"segment": {
"type": "line",
"end": {
"x": 2.0,
"y": 3.0,
"z": 0.0
},
"relative": true
}
}
},
{
"cmdId": "[uuid]",
"range": [
406,
422,
0
],
"command": {
"type": "extend_path",
"path": "[uuid]",
"segment": {
"type": "line",
"end": {
"x": 2.0,
"y": -3.0,
"z": 0.0
},
"relative": true
}
}
},
{
"cmdId": "[uuid]",
"range": [
428,
475,
0
],
"command": {
"type": "extend_path",
"path": "[uuid]",
"segment": {
"type": "line",
"end": {
"x": -2.0,
"y": -6.0,
"z": 0.0
},
"relative": false
}
}
},
{
"cmdId": "[uuid]",
"range": [
481,
489,
0
],
"command": {
"type": "close_path",
"path_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [
503,
524,
0
],
"command": {
"type": "enable_sketch_mode",
"entity_id": "[uuid]",
"ortho": false,
"animated": false,
"adjust_camera": false,
"planar_normal": null
}
},
{
"cmdId": "[uuid]",
"range": [
503,
524,
0
],
"command": {
"type": "extrude",
"target": "[uuid]",
"distance": 5.0,
"faces": null
}
},
{
"cmdId": "[uuid]",
"range": [
503,
524,
0
],
"command": {
"type": "sketch_mode_disable"
}
},
{
"cmdId": "[uuid]",
"range": [
503,
524,
0
],
"command": {
"type": "object_bring_to_front",
"object_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [
503,
524,
0
],
"command": {
"type": "solid3d_get_extrusion_face_info",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [
503,
524,
0
],
"command": {
"type": "solid3d_get_opposite_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [
503,
524,
0
],
"command": {
"type": "solid3d_get_next_adjacent_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [
503,
524,
0
],
"command": {
"type": "solid3d_get_opposite_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [
503,
524,
0
],
"command": {
"type": "solid3d_get_next_adjacent_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [
503,
524,
0
],
"command": {
"type": "solid3d_get_opposite_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [
503,
524,
0
],
"command": {
"type": "solid3d_get_next_adjacent_edge",
"object_id": "[uuid]",
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
}
]

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph flowchart artifact_graph_example_code1.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,109 @@
```mermaid
flowchart LR
subgraph path2 [Path]
2["Path<br>[37, 64, 0]"]
3["Segment<br>[70, 86, 0]"]
4["Segment<br>[92, 119, 0]"]
5["Segment<br>[125, 150, 0]"]
6["Segment<br>[156, 203, 0]"]
7["Segment<br>[209, 217, 0]"]
8[Solid2d]
end
subgraph path25 [Path]
25["Path<br>[352, 379, 0]"]
26["Segment<br>[385, 400, 0]"]
27["Segment<br>[406, 422, 0]"]
28["Segment<br>[428, 475, 0]"]
29["Segment<br>[481, 489, 0]"]
30[Solid2d]
end
1["Plane<br>[12, 31, 0]"]
9["Sweep Extrusion<br>[231, 254, 0]"]
10[Wall]
11[Wall]
12[Wall]
13[Wall]
14["Cap Start"]
15["Cap End"]
16["SweepEdge Opposite"]
17["SweepEdge Adjacent"]
18["SweepEdge Opposite"]
19["SweepEdge Adjacent"]
20["SweepEdge Opposite"]
21["SweepEdge Adjacent"]
22["SweepEdge Opposite"]
23["SweepEdge Adjacent"]
24["EdgeCut Fillet<br>[260, 301, 0]"]
31["Sweep Extrusion<br>[503, 524, 0]"]
32[Wall]
33[Wall]
34[Wall]
35["Cap End"]
36["SweepEdge Opposite"]
37["SweepEdge Adjacent"]
38["SweepEdge Opposite"]
39["SweepEdge Adjacent"]
40["SweepEdge Opposite"]
41["SweepEdge Adjacent"]
1 --- 2
2 --- 3
2 --- 4
2 --- 5
2 --- 6
2 --- 7
2 ---- 9
2 --- 8
3 --- 13
3 --- 22
3 --- 23
4 --- 12
4 --- 20
4 --- 21
4 --- 24
5 --- 11
5 --- 18
5 --- 19
6 --- 10
6 --- 16
6 --- 17
9 --- 10
9 --- 11
9 --- 12
9 --- 13
9 --- 14
9 --- 15
9 --- 16
9 --- 17
9 --- 18
9 --- 19
9 --- 20
9 --- 21
9 --- 22
9 --- 23
11 --- 25
25 --- 26
25 --- 27
25 --- 28
25 --- 29
25 ---- 31
25 --- 30
26 --- 34
26 --- 40
26 --- 41
27 --- 33
27 --- 38
27 --- 39
28 --- 32
28 --- 36
28 --- 37
31 --- 32
31 --- 33
31 --- 34
31 --- 35
31 --- 36
31 --- 37
31 --- 38
31 --- 39
31 --- 40
31 --- 41
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph mind map artifact_graph_example_code1.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,92 @@
```mermaid
mindmap
root
Plane
Path
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
EdgeCut Fillet
Segment
Wall
Path
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Segment
Sweep Extrusion
Wall
Wall
Wall
Cap End
SweepEdge Opposite
SweepEdge Adjacent
SweepEdge Opposite
SweepEdge Adjacent
SweepEdge Opposite
SweepEdge Adjacent
Solid2d
SweepEdge Opposite
SweepEdge Adjacent
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Segment
Sweep Extrusion
Wall
Wall
Path
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Segment
Sweep Extrusion
Wall
Wall
Wall
Cap End
SweepEdge Opposite
SweepEdge Adjacent
SweepEdge Opposite
SweepEdge Adjacent
SweepEdge Opposite
SweepEdge Adjacent
Solid2d
Wall
Wall
Cap Start
Cap End
SweepEdge Opposite
SweepEdge Adjacent
SweepEdge Opposite
SweepEdge Adjacent
SweepEdge Opposite
SweepEdge Adjacent
SweepEdge Opposite
SweepEdge Adjacent
Solid2d
```

View File

@ -0,0 +1,837 @@
---
source: kcl/src/simulation_tests.rs
description: Result of parsing artifact_graph_example_code1.kcl
snapshot_kind: text
---
{
"Ok": {
"body": [
{
"declaration": {
"end": 217,
"id": {
"end": 9,
"name": "sketch001",
"start": 0,
"type": "Identifier"
},
"init": {
"body": [
{
"arguments": [
{
"end": 30,
"raw": "'XY'",
"start": 26,
"type": "Literal",
"type": "Literal",
"value": "XY"
}
],
"callee": {
"end": 25,
"name": "startSketchOn",
"start": 12,
"type": "Identifier"
},
"end": 31,
"start": 12,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"elements": [
{
"argument": {
"end": 55,
"raw": "5",
"start": 54,
"type": "Literal",
"type": "Literal",
"value": 5.0
},
"end": 55,
"operator": "-",
"start": 53,
"type": "UnaryExpression",
"type": "UnaryExpression"
},
{
"argument": {
"end": 59,
"raw": "5",
"start": 58,
"type": "Literal",
"type": "Literal",
"value": 5.0
},
"end": 59,
"operator": "-",
"start": 57,
"type": "UnaryExpression",
"type": "UnaryExpression"
}
],
"end": 60,
"start": 52,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
{
"end": 63,
"start": 62,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 51,
"name": "startProfileAt",
"start": 37,
"type": "Identifier"
},
"end": 64,
"start": 37,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"elements": [
{
"end": 77,
"raw": "0",
"start": 76,
"type": "Literal",
"type": "Literal",
"value": 0.0
},
{
"end": 81,
"raw": "10",
"start": 79,
"type": "Literal",
"type": "Literal",
"value": 10.0
}
],
"end": 82,
"start": 75,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
{
"end": 85,
"start": 84,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 74,
"name": "line",
"start": 70,
"type": "Identifier"
},
"end": 86,
"start": 70,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"elements": [
{
"end": 103,
"raw": "10.55",
"start": 98,
"type": "Literal",
"type": "Literal",
"value": 10.55
},
{
"end": 106,
"raw": "0",
"start": 105,
"type": "Literal",
"type": "Literal",
"value": 0.0
}
],
"end": 107,
"start": 97,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
{
"end": 110,
"start": 109,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
},
{
"end": 118,
"start": 112,
"type": "TagDeclarator",
"type": "TagDeclarator",
"value": "seg01"
}
],
"callee": {
"end": 96,
"name": "line",
"start": 92,
"type": "Identifier"
},
"end": 119,
"start": 92,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"elements": [
{
"end": 132,
"raw": "0",
"start": 131,
"type": "Literal",
"type": "Literal",
"value": 0.0
},
{
"argument": {
"end": 137,
"raw": "10",
"start": 135,
"type": "Literal",
"type": "Literal",
"value": 10.0
},
"end": 137,
"operator": "-",
"start": 134,
"type": "UnaryExpression",
"type": "UnaryExpression"
}
],
"end": 138,
"start": 130,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
{
"end": 141,
"start": 140,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
},
{
"end": 149,
"start": 143,
"type": "TagDeclarator",
"type": "TagDeclarator",
"value": "seg02"
}
],
"callee": {
"end": 129,
"name": "line",
"start": 125,
"type": "Identifier"
},
"end": 150,
"start": 125,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"elements": [
{
"arguments": [
{
"end": 179,
"start": 178,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 177,
"name": "profileStartX",
"start": 164,
"type": "Identifier"
},
"end": 180,
"start": 164,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"end": 197,
"start": 196,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 195,
"name": "profileStartY",
"start": 182,
"type": "Identifier"
},
"end": 198,
"start": 182,
"type": "CallExpression",
"type": "CallExpression"
}
],
"end": 199,
"start": 163,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
{
"end": 202,
"start": 201,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 162,
"name": "lineTo",
"start": 156,
"type": "Identifier"
},
"end": 203,
"start": 156,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"end": 216,
"start": 215,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 214,
"name": "close",
"start": 209,
"type": "Identifier"
},
"end": 217,
"start": 209,
"type": "CallExpression",
"type": "CallExpression"
}
],
"end": 217,
"start": 12,
"type": "PipeExpression",
"type": "PipeExpression"
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 217,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 301,
"id": {
"end": 228,
"name": "extrude001",
"start": 218,
"type": "Identifier"
},
"init": {
"body": [
{
"arguments": [
{
"argument": {
"end": 242,
"raw": "10",
"start": 240,
"type": "Literal",
"type": "Literal",
"value": 10.0
},
"end": 242,
"operator": "-",
"start": 239,
"type": "UnaryExpression",
"type": "UnaryExpression"
},
{
"end": 253,
"name": "sketch001",
"start": 244,
"type": "Identifier",
"type": "Identifier"
}
],
"callee": {
"end": 238,
"name": "extrude",
"start": 231,
"type": "Identifier"
},
"end": 254,
"start": 231,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"end": 297,
"properties": [
{
"end": 279,
"key": {
"end": 275,
"name": "radius",
"start": 269,
"type": "Identifier"
},
"start": 269,
"type": "ObjectProperty",
"value": {
"end": 279,
"raw": "5",
"start": 278,
"type": "Literal",
"type": "Literal",
"value": 5.0
}
},
{
"end": 295,
"key": {
"end": 285,
"name": "tags",
"start": 281,
"type": "Identifier"
},
"start": 281,
"type": "ObjectProperty",
"value": {
"elements": [
{
"end": 294,
"name": "seg01",
"start": 289,
"type": "Identifier",
"type": "Identifier"
}
],
"end": 295,
"start": 288,
"type": "ArrayExpression",
"type": "ArrayExpression"
}
}
],
"start": 267,
"type": "ObjectExpression",
"type": "ObjectExpression"
},
{
"end": 300,
"start": 299,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 266,
"name": "fillet",
"start": 260,
"type": "Identifier"
},
"end": 301,
"start": 260,
"type": "CallExpression",
"type": "CallExpression"
}
],
"end": 301,
"start": 231,
"type": "PipeExpression",
"type": "PipeExpression"
},
"start": 218,
"type": "VariableDeclarator"
},
"end": 301,
"kind": "const",
"start": 218,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 489,
"id": {
"end": 311,
"name": "sketch002",
"start": 302,
"type": "Identifier"
},
"init": {
"body": [
{
"arguments": [
{
"end": 338,
"name": "extrude001",
"start": 328,
"type": "Identifier",
"type": "Identifier"
},
{
"end": 345,
"name": "seg02",
"start": 340,
"type": "Identifier",
"type": "Identifier"
}
],
"callee": {
"end": 327,
"name": "startSketchOn",
"start": 314,
"type": "Identifier"
},
"end": 346,
"start": 314,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"elements": [
{
"argument": {
"end": 370,
"raw": "2",
"start": 369,
"type": "Literal",
"type": "Literal",
"value": 2.0
},
"end": 370,
"operator": "-",
"start": 368,
"type": "UnaryExpression",
"type": "UnaryExpression"
},
{
"argument": {
"end": 374,
"raw": "6",
"start": 373,
"type": "Literal",
"type": "Literal",
"value": 6.0
},
"end": 374,
"operator": "-",
"start": 372,
"type": "UnaryExpression",
"type": "UnaryExpression"
}
],
"end": 375,
"start": 367,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
{
"end": 378,
"start": 377,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 366,
"name": "startProfileAt",
"start": 352,
"type": "Identifier"
},
"end": 379,
"start": 352,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"elements": [
{
"end": 392,
"raw": "2",
"start": 391,
"type": "Literal",
"type": "Literal",
"value": 2.0
},
{
"end": 395,
"raw": "3",
"start": 394,
"type": "Literal",
"type": "Literal",
"value": 3.0
}
],
"end": 396,
"start": 390,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
{
"end": 399,
"start": 398,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 389,
"name": "line",
"start": 385,
"type": "Identifier"
},
"end": 400,
"start": 385,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"elements": [
{
"end": 413,
"raw": "2",
"start": 412,
"type": "Literal",
"type": "Literal",
"value": 2.0
},
{
"argument": {
"end": 417,
"raw": "3",
"start": 416,
"type": "Literal",
"type": "Literal",
"value": 3.0
},
"end": 417,
"operator": "-",
"start": 415,
"type": "UnaryExpression",
"type": "UnaryExpression"
}
],
"end": 418,
"start": 411,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
{
"end": 421,
"start": 420,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 410,
"name": "line",
"start": 406,
"type": "Identifier"
},
"end": 422,
"start": 406,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"elements": [
{
"arguments": [
{
"end": 451,
"start": 450,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 449,
"name": "profileStartX",
"start": 436,
"type": "Identifier"
},
"end": 452,
"start": 436,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"end": 469,
"start": 468,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 467,
"name": "profileStartY",
"start": 454,
"type": "Identifier"
},
"end": 470,
"start": 454,
"type": "CallExpression",
"type": "CallExpression"
}
],
"end": 471,
"start": 435,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
{
"end": 474,
"start": 473,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 434,
"name": "lineTo",
"start": 428,
"type": "Identifier"
},
"end": 475,
"start": 428,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"end": 488,
"start": 487,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 486,
"name": "close",
"start": 481,
"type": "Identifier"
},
"end": 489,
"start": 481,
"type": "CallExpression",
"type": "CallExpression"
}
],
"end": 489,
"start": 314,
"type": "PipeExpression",
"type": "PipeExpression"
},
"start": 302,
"type": "VariableDeclarator"
},
"end": 489,
"kind": "const",
"start": 302,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 524,
"id": {
"end": 500,
"name": "extrude002",
"start": 490,
"type": "Identifier"
},
"init": {
"arguments": [
{
"end": 512,
"raw": "5",
"start": 511,
"type": "Literal",
"type": "Literal",
"value": 5.0
},
{
"end": 523,
"name": "sketch002",
"start": 514,
"type": "Identifier",
"type": "Identifier"
}
],
"callee": {
"end": 510,
"name": "extrude",
"start": 503,
"type": "Identifier"
},
"end": 524,
"start": 503,
"type": "CallExpression",
"type": "CallExpression"
},
"start": 490,
"type": "VariableDeclarator"
},
"end": 524,
"kind": "const",
"start": 490,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 525,
"start": 0
}
}

View File

@ -0,0 +1,16 @@
sketch001 = startSketchOn('XY')
|> startProfileAt([-5, -5], %)
|> line([0, 10], %)
|> line([10.55, 0], %, $seg01)
|> line([0, -10], %, $seg02)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-10, sketch001)
|> fillet({ radius = 5, tags = [seg01] }, %)
sketch002 = startSketchOn(extrude001, seg02)
|> startProfileAt([-2, -6], %)
|> line([2, 3], %)
|> line([2, -3], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude002 = extrude(5, sketch002)

Some files were not shown because too many files have changed in this diff Show More