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]
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

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.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 () => {

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
*/

View File

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

View File

@ -63,6 +63,10 @@ export class ToolbarFixture {
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
}
get logoLink() {
return this.page.getByTestId('app-logo')
}
startSketchPlaneSelection = async () =>
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.getByText('broken-code')).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 () => {
// Go back home.
@ -253,7 +253,7 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('empty')).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 () => {
// 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(
@ -1391,7 +1511,7 @@ extrude001 = extrude(200, sketch001)`)
await page.getByTestId('app-logo').click()
await expect(
page.getByRole('button', { name: 'New project' })
page.getByRole('button', { name: 'Create project' })
).toBeVisible()
for (let i = 1; i <= 10; i++) {
@ -1465,7 +1585,7 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).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 () => {
@ -1494,7 +1614,7 @@ test(
await expect(page.getByRole('link', { name: 'bracket' })).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
}) {
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('button', { name: 'Continue' }).click()

View File

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

View File

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

View File

@ -59,6 +59,7 @@ import {
sourceRangeFromRust,
resultIsOk,
SourceRange,
topLevelRange,
} from 'lang/wasm'
import { calculate_circle_from_3_points } from '../wasm-lib/pkg/wasm_lib'
import {
@ -628,7 +629,7 @@ export class SceneEntities {
const startRange = _node1.node.start
const endRange = _node1.node.end
const sourceRange: SourceRange = [startRange, endRange, true]
const sourceRange = topLevelRange(startRange, endRange)
const selection: Selections = computeSelectionFromSourceRangeAndAST(
sourceRange,
maybeModdedAst
@ -2012,7 +2013,7 @@ export class SceneEntities {
kclManager.programMemory,
{
type: 'sourceRange',
sourceRange: [node.start, node.end, true],
sourceRange: topLevelRange(node.start, node.end),
},
getChangeSketchInput()
)
@ -2263,7 +2264,7 @@ export class SceneEntities {
)
if (trap(_node, { suppress: true })) return
const node = _node.node
editorManager.setHighlightRange([[node.start, node.end, true]])
editorManager.setHighlightRange([topLevelRange(node.start, node.end)])
const yellow = 0xffff00
colorSegment(selected, yellow)
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 { codeToIdSelections } from 'lib/selections'
import { codeRefFromRange } from 'lang/std/artifactGraph'
import { defaultSourceRange } from 'lang/wasm'
import { defaultSourceRange, SourceRange, topLevelRange } from 'lang/wasm'
export function AstExplorer() {
const { context } = useModelingContext()
@ -118,19 +118,19 @@ function DisplayObj({
hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : ''
}`}
onMouseEnter={(e) => {
editorManager.setHighlightRange([[obj?.start || 0, obj.end, true]])
editorManager.setHighlightRange([
topLevelRange(obj?.start || 0, obj.end),
])
e.stopPropagation()
}}
onMouseMove={(e) => {
e.stopPropagation()
editorManager.setHighlightRange([[obj?.start || 0, obj.end, true]])
editorManager.setHighlightRange([
topLevelRange(obj?.start || 0, obj.end),
])
}}
onClick={(e) => {
const range: [number, number, boolean] = [
obj?.start || 0,
obj.end || 0,
true,
]
const range = topLevelRange(obj?.start || 0, obj.end || 0)
const idInfo = codeToIdSelections([
{ codeRef: codeRefFromRange(range, kclManager.ast) },
])[0]

View File

@ -17,7 +17,7 @@ import { StateFrom } from 'xstate'
const semanticEntityNames: {
[key: string]: Array<Artifact['type'] | 'defaultPlane'>
} = {
face: ['wall', 'cap', 'solid2D'],
face: ['wall', 'cap', 'solid2d'],
edge: ['segment', 'sweepEdge', 'edgeCutEdge'],
point: [],
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"
/>
<Combobox.Input
data-testid="cmd-bar-search"
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"
onKeyDown={(event) => {
@ -85,6 +86,7 @@ function CommandComboBox({
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"
disabled={optionIsDisabled(option)}
data-testid={`cmd-bar-option`}
>
{'icon' in option && option.icon && (
<CustomIcon name={option.icon} className="w-5 h-5" />

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { toolTips } from 'lang/langHelpers'
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 {
PathToNodeMap,
@ -41,7 +41,7 @@ export function removeConstrainingValuesInfo({
graphSelections: nodes.map(
(node): Selection => ({
codeRef: codeRefFromRange(
[node.start, node.end, true],
topLevelRange(node.start, node.end),
kclManager.ast
),
})

View File

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

View File

@ -1,4 +1,5 @@
import { kclErrorsToDiagnostics, KCLError } from './errors'
import { defaultArtifactGraph, topLevelRange } from 'lang/wasm'
describe('test kclErrToDiagnostic', () => {
it('converts KCL errors to CodeMirror diagnostics', () => {
@ -8,18 +9,20 @@ describe('test kclErrToDiagnostic', () => {
message: '',
kind: 'semantic',
msg: 'Semantic error',
sourceRange: [0, 1, true],
sourceRange: topLevelRange(0, 1),
operations: [],
artifactCommands: [],
artifactGraph: defaultArtifactGraph(),
},
{
name: '',
message: '',
kind: 'type',
msg: 'Type error',
sourceRange: [4, 5, true],
sourceRange: topLevelRange(4, 5),
operations: [],
artifactCommands: [],
artifactGraph: defaultArtifactGraph(),
},
]
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 { Text } from '@codemirror/state'
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'
type ExtractKind<T> = T extends { kind: infer K } ? K : never
@ -15,13 +21,15 @@ export class KCLError extends Error {
msg: string
operations: Operation[]
artifactCommands: ArtifactCommand[]
artifactGraph: ArtifactGraph
constructor(
kind: ExtractKind<RustKclError> | 'name',
msg: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph
) {
super()
this.kind = kind
@ -29,6 +37,7 @@ export class KCLError extends Error {
this.sourceRange = sourceRange
this.operations = operations
this.artifactCommands = artifactCommands
this.artifactGraph = artifactGraph
Object.setPrototypeOf(this, KCLError.prototype)
}
}
@ -38,9 +47,17 @@ export class KCLLexicalError extends KCLError {
msg: string,
sourceRange: SourceRange,
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)
}
}
@ -50,9 +67,17 @@ export class KCLInternalError extends KCLError {
msg: string,
sourceRange: SourceRange,
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)
}
}
@ -62,9 +87,17 @@ export class KCLSyntaxError extends KCLError {
msg: string,
sourceRange: SourceRange,
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)
}
}
@ -74,9 +107,17 @@ export class KCLSemanticError extends KCLError {
msg: string,
sourceRange: SourceRange,
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)
}
}
@ -86,9 +127,10 @@ export class KCLTypeError extends KCLError {
msg: string,
sourceRange: SourceRange,
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)
}
}
@ -98,9 +140,17 @@ export class KCLUnimplementedError extends KCLError {
msg: string,
sourceRange: SourceRange,
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)
}
}
@ -110,9 +160,17 @@ export class KCLUnexpectedError extends KCLError {
msg: string,
sourceRange: SourceRange,
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)
}
}
@ -122,14 +180,16 @@ export class KCLValueAlreadyDefined extends KCLError {
key: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph
) {
super(
'name',
`Key ${key} was already defined elsewhere`,
sourceRange,
operations,
artifactCommands
artifactCommands,
artifactGraph
)
Object.setPrototypeOf(this, KCLValueAlreadyDefined.prototype)
}
@ -140,14 +200,16 @@ export class KCLUndefinedValueError extends KCLError {
key: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
artifactCommands: ArtifactCommand[],
artifactGraph: ArtifactGraph
) {
super(
'name',
`Key ${key} has not been defined`,
sourceRange,
operations,
artifactCommands
artifactCommands,
artifactGraph
)
Object.setPrototypeOf(this, KCLUndefinedValueError.prototype)
}
@ -167,9 +229,10 @@ export function lspDiagnosticsToKclErrors(
new KCLError(
'unexpected',
message,
[posToOffset(doc, range.start)!, posToOffset(doc, range.end)!, true],
[posToOffset(doc, range.start)!, posToOffset(doc, range.end)!, 0],
[],
[]
[],
defaultArtifactGraph()
)
)
.sort((a, b) => {
@ -193,7 +256,7 @@ export function kclErrorsToDiagnostics(
errors: KCLError[]
): CodeMirrorDiagnostic[] {
return errors
?.filter((err) => err.sourceRange[2])
?.filter((err) => isTopLevelModule(err.sourceRange))
.map((err) => {
return {
from: err.sourceRange[0],
@ -208,7 +271,7 @@ export function complilationErrorsToDiagnostics(
errors: CompilationError[]
): CodeMirrorDiagnostic[] {
return errors
?.filter((err) => err.sourceRange[2] === 0)
?.filter((err) => isTopLevelModule(err.sourceRange))
.map((err) => {
let severity: any = 'error'
if (err.severity === 'Warning') {

View File

@ -6,6 +6,8 @@ import {
Sketch,
initPromise,
sketchFromKclValue,
defaultArtifactGraph,
topLevelRange,
} from './wasm'
import { enginelessExecutor } from '../lib/testHelpers'
import { KCLError } from './errors'
@ -480,9 +482,10 @@ const theExtrude = startSketchOn('XY')
new KCLError(
'undefined_value',
'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 { Identifier, assertParse, initPromise, Parameter } from './wasm'
import {
Identifier,
assertParse,
initPromise,
Parameter,
SourceRange,
topLevelRange,
} from './wasm'
import { err } from 'lib/trap'
beforeAll(async () => {
@ -17,11 +24,10 @@ const sk3 = startSketchAt([0, 0])
`
const subStr = 'lineTo([3, 4], %, $yo)'
const lineToSubstringIndex = code.indexOf(subStr)
const sourceRange: [number, number, boolean] = [
const sourceRange = topLevelRange(
lineToSubstringIndex,
lineToSubstringIndex + subStr.length,
true,
]
lineToSubstringIndex + subStr.length
)
const ast = assertParse(code)
const nodePath = getNodePathFromSourceRange(ast, sourceRange)
@ -29,7 +35,7 @@ const sk3 = startSketchAt([0, 0])
if (err(_node)) throw _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')
})
it('gets path right for function definition params', () => {
@ -45,11 +51,7 @@ const sk3 = startSketchAt([0, 0])
const b1 = cube([0,0], 10)`
const subStr = 'pos, scale'
const subStrIndex = code.indexOf(subStr)
const sourceRange: [number, number, boolean] = [
subStrIndex,
subStrIndex + 'pos'.length,
true,
]
const sourceRange = topLevelRange(subStrIndex, subStrIndex + 'pos'.length)
const ast = assertParse(code)
const nodePath = getNodePathFromSourceRange(ast, sourceRange)
@ -81,11 +83,7 @@ const b1 = cube([0,0], 10)`
const b1 = cube([0,0], 10)`
const subStr = 'scale, 0'
const subStrIndex = code.indexOf(subStr)
const sourceRange: [number, number, boolean] = [
subStrIndex,
subStrIndex + 'scale'.length,
true,
]
const sourceRange = topLevelRange(subStrIndex, subStrIndex + 'scale'.length)
const ast = assertParse(code)
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 {
createLiteral,
createIdentifier,
@ -148,11 +155,7 @@ function giveSketchFnCallTagTestHelper(
// making it more of an integration test, but easier to read the test intention is the goal
const ast = assertParse(code)
const start = code.indexOf(searchStr)
const range: [number, number, boolean] = [
start,
start + searchStr.length,
true,
]
const range = topLevelRange(start, start + searchStr.length)
const sketchRes = giveSketchFnCallTag(ast, range)
if (err(sketchRes)) throw sketchRes
const { modifiedAst, tag, isTagExisting } = sketchRes
@ -230,7 +233,7 @@ yo2 = hmm([identifierGuy + 5])`
const { modifiedAst } = moveValueIntoNewVariable(
ast,
execState.memory,
[startIndex, startIndex, true],
topLevelRange(startIndex, startIndex),
'newVar'
)
const newCode = recast(modifiedAst)
@ -244,7 +247,7 @@ yo2 = hmm([identifierGuy + 5])`
const { modifiedAst } = moveValueIntoNewVariable(
ast,
execState.memory,
[startIndex, startIndex, true],
topLevelRange(startIndex, startIndex),
'newVar'
)
const newCode = recast(modifiedAst)
@ -258,7 +261,7 @@ yo2 = hmm([identifierGuy + 5])`
const { modifiedAst } = moveValueIntoNewVariable(
ast,
execState.memory,
[startIndex, startIndex, true],
topLevelRange(startIndex, startIndex),
'newVar'
)
const newCode = recast(modifiedAst)
@ -272,7 +275,7 @@ yo2 = hmm([identifierGuy + 5])`
const { modifiedAst } = moveValueIntoNewVariable(
ast,
execState.memory,
[startIndex, startIndex, true],
topLevelRange(startIndex, startIndex),
'newVar'
)
const newCode = recast(modifiedAst)
@ -286,7 +289,7 @@ yo2 = hmm([identifierGuy + 5])`
const { modifiedAst } = moveValueIntoNewVariable(
ast,
execState.memory,
[startIndex, startIndex, true],
topLevelRange(startIndex, startIndex),
'newVar'
)
const newCode = recast(modifiedAst)
@ -306,18 +309,16 @@ describe('testing sketchOnExtrudedFace', () => {
const ast = assertParse(code)
const segmentSnippet = `line([9.7, 9.19], %)`
const segmentRange: [number, number, boolean] = [
const segmentRange = topLevelRange(
code.indexOf(segmentSnippet),
code.indexOf(segmentSnippet) + segmentSnippet.length,
true,
]
code.indexOf(segmentSnippet) + segmentSnippet.length
)
const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange)
const extrudeSnippet = `extrude(5 + 7, %)`
const extrudeRange: [number, number, boolean] = [
const extrudeRange = topLevelRange(
code.indexOf(extrudeSnippet),
code.indexOf(extrudeSnippet) + extrudeSnippet.length,
true,
]
code.indexOf(extrudeSnippet) + extrudeSnippet.length
)
const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange)
const extruded = sketchOnExtrudedFace(
@ -346,18 +347,16 @@ sketch001 = startSketchOn(part001, seg01)`)
|> extrude(5 + 7, %)`
const ast = assertParse(code)
const segmentSnippet = `close(%)`
const segmentRange: [number, number, boolean] = [
const segmentRange = topLevelRange(
code.indexOf(segmentSnippet),
code.indexOf(segmentSnippet) + segmentSnippet.length,
true,
]
code.indexOf(segmentSnippet) + segmentSnippet.length
)
const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange)
const extrudeSnippet = `extrude(5 + 7, %)`
const extrudeRange: [number, number, boolean] = [
const extrudeRange = topLevelRange(
code.indexOf(extrudeSnippet),
code.indexOf(extrudeSnippet) + extrudeSnippet.length,
true,
]
code.indexOf(extrudeSnippet) + extrudeSnippet.length
)
const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange)
const extruded = sketchOnExtrudedFace(
@ -386,18 +385,16 @@ sketch001 = startSketchOn(part001, seg01)`)
|> extrude(5 + 7, %)`
const ast = assertParse(code)
const sketchSnippet = `startProfileAt([3.58, 2.06], %)`
const sketchRange: [number, number, boolean] = [
const sketchRange = topLevelRange(
code.indexOf(sketchSnippet),
code.indexOf(sketchSnippet) + sketchSnippet.length,
true,
]
code.indexOf(sketchSnippet) + sketchSnippet.length
)
const sketchPathToNode = getNodePathFromSourceRange(ast, sketchRange)
const extrudeSnippet = `extrude(5 + 7, %)`
const extrudeRange: [number, number, boolean] = [
const extrudeRange = topLevelRange(
code.indexOf(extrudeSnippet),
code.indexOf(extrudeSnippet) + extrudeSnippet.length,
true,
]
code.indexOf(extrudeSnippet) + extrudeSnippet.length
)
const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange)
const extruded = sketchOnExtrudedFace(
@ -435,18 +432,16 @@ sketch001 = startSketchOn(part001, 'END')`)
part001 = extrude(5 + 7, sketch001)`
const ast = assertParse(code)
const segmentSnippet = `line([4.99, -0.46], %)`
const segmentRange: [number, number, boolean] = [
const segmentRange = topLevelRange(
code.indexOf(segmentSnippet),
code.indexOf(segmentSnippet) + segmentSnippet.length,
true,
]
code.indexOf(segmentSnippet) + segmentSnippet.length
)
const segmentPathToNode = getNodePathFromSourceRange(ast, segmentRange)
const extrudeSnippet = `extrude(5 + 7, sketch001)`
const extrudeRange: [number, number, boolean] = [
const extrudeRange = topLevelRange(
code.indexOf(extrudeSnippet),
code.indexOf(extrudeSnippet) + extrudeSnippet.length,
true,
]
code.indexOf(extrudeSnippet) + extrudeSnippet.length
)
const extrudePathToNode = getNodePathFromSourceRange(ast, extrudeRange)
const updatedAst = sketchOnExtrudedFace(
@ -471,11 +466,10 @@ describe('Testing deleteSegmentFromPipeExpression', () => {
const ast = assertParse(code)
const execState = await enginelessExecutor(ast)
const lineOfInterest = 'line([306.21, 198.85], %, $a)'
const range: [number, number, boolean] = [
const range = topLevelRange(
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
true,
]
code.indexOf(lineOfInterest) + lineOfInterest.length
)
const pathToNode = getNodePathFromSourceRange(ast, range)
const modifiedAst = deleteSegmentFromPipeExpression(
[],
@ -549,11 +543,10 @@ ${!replace1 ? ` |> ${line}\n` : ''} |> angledLine([-65, ${
const ast = assertParse(code)
const execState = await enginelessExecutor(ast)
const lineOfInterest = line
const range: [number, number, boolean] = [
const range = topLevelRange(
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
true,
]
code.indexOf(lineOfInterest) + lineOfInterest.length
)
const pathToNode = getNodePathFromSourceRange(ast, range)
const dependentSegments = findUsesOfTagInPipe(ast, pathToNode)
const modifiedAst = deleteSegmentFromPipeExpression(
@ -638,11 +631,10 @@ describe('Testing removeSingleConstraintInfo', () => {
const execState = await enginelessExecutor(ast)
const lineOfInterest = expectedFinish.split('(')[0] + '('
const range: [number, number, boolean] = [
const range = topLevelRange(
code.indexOf(lineOfInterest) + 1,
code.indexOf(lineOfInterest) + lineOfInterest.length,
true,
]
code.indexOf(lineOfInterest) + lineOfInterest.length
)
const pathToNode = getNodePathFromSourceRange(ast, range)
let argPosition: SimplifiedArgDetails
if (key === 'arrayIndex' && typeof value === 'number') {
@ -692,11 +684,10 @@ describe('Testing removeSingleConstraintInfo', () => {
const execState = await enginelessExecutor(ast)
const lineOfInterest = expectedFinish.split('(')[0] + '('
const range: [number, number, boolean] = [
const range = topLevelRange(
code.indexOf(lineOfInterest) + 1,
code.indexOf(lineOfInterest) + lineOfInterest.length,
true,
]
code.indexOf(lineOfInterest) + lineOfInterest.length
)
let argPosition: SimplifiedArgDetails
if (key === 'arrayIndex' && typeof value === 'number') {
argPosition = {
@ -889,11 +880,10 @@ sketch002 = startSketchOn({
const execState = await enginelessExecutor(ast)
// deleteFromSelection
const range: [number, number, boolean] = [
const range = topLevelRange(
codeBefore.indexOf(lineOfInterest),
codeBefore.indexOf(lineOfInterest) + lineOfInterest.length,
true,
]
codeBefore.indexOf(lineOfInterest) + lineOfInterest.length
)
const artifact = { type } as Artifact
const newAst = await deleteFromSelection(
ast,

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import { ToolTip } from 'lang/langHelpers'
import { Selection, Selections } from 'lib/selections'
import {
ArrayExpression,
ArtifactGraph,
BinaryExpression,
CallExpression,
Expr,
@ -16,8 +17,8 @@ import {
sketchFromKclValue,
sketchFromKclValueOptional,
SourceRange,
sourceRangeFromRust,
SyntaxType,
topLevelRange,
VariableDeclaration,
VariableDeclarator,
} from './wasm'
@ -32,7 +33,7 @@ import {
import { err, Reason } from 'lib/trap'
import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { ArtifactGraph, codeRefFromRange } from './std/artifactGraph'
import { codeRefFromRange } from './std/artifactGraph'
/**
* 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 {
isParallelAndConstrained,
selection: {
codeRef: codeRefFromRange(sourceRangeFromRust(prevSourceRange), ast),
codeRef: codeRefFromRange(prevSourceRange, ast),
artifact: artifactGraph.get(prevSegment.__geoMeta.id),
},
}
@ -937,7 +938,7 @@ export function findUsesOfTagInPipe(
const tagArgValue =
tagArg.type === 'TagDeclarator' ? String(tagArg.value) : tagArg.name
if (tagArgValue === tag)
dependentRanges.push([node.start, node.end, true])
dependentRanges.push(topLevelRange(node.start, node.end))
},
})
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 {
ArtifactCommand,
ExecState,
Artifact,
ArtifactGraph,
ArtifactId,
PathToNode,
Program,
SourceRange,
sourceRangeFromRust,
PathArtifact,
PlaneArtifact,
WallArtifact,
SegmentArtifact,
Solid2dArtifact as Solid2D,
SweepArtifact,
SweepEdge,
CapArtifact,
EdgeCut,
} from 'lang/wasm'
import { Models } from '@kittycad/lib'
import { getNodePathFromSourceRange } from 'lang/queryAst'
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 {
id: ArtifactId
@ -22,30 +30,12 @@ export interface CodeRef {
pathToNode: PathToNode
}
export interface PlaneArtifact extends BaseArtifact {
type: 'plane'
pathIds: Array<ArtifactId>
codeRef: CodeRef
}
export interface PlaneArtifactRich extends BaseArtifact {
type: 'plane'
paths: Array<PathArtifact>
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 {
type: 'path'
/** 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 */
segments: Array<SegmentArtifact>
/** A path may not result in a sweep artifact */
sweep?: SweepArtifact
sweep: SweepArtifact | null
codeRef: CodeRef
}
export interface SegmentArtifact extends BaseArtifact {
type: 'segment'
pathId: ArtifactId
surfaceId?: ArtifactId
edgeIds: Array<ArtifactId>
edgeCutId?: ArtifactId
codeRef: CodeRef
}
interface SegmentArtifactRich extends BaseArtifact {
type: 'segment'
path: PathArtifact
@ -74,15 +56,6 @@ interface SegmentArtifactRich extends BaseArtifact {
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 {
type: 'sweep'
subType: 'extrusion' | 'revolve' | 'loft' | 'sweep'
@ -92,58 +65,6 @@ interface SweepArtifactRich extends BaseArtifact {
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']
type OkWebSocketResponseData = Models['OkWebSocketResponseData_type']
@ -152,437 +73,6 @@ export interface ResponseMap {
[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 */
export function filterArtifacts<T extends Artifact['type'][]>(
{
@ -676,7 +166,7 @@ export function expandPath(
},
artifactGraph
)
: undefined
: null
const plane = getArtifactOfTypes(
{ key: path.planeId, types: ['plane', 'wall'] },
artifactGraph
@ -778,11 +268,11 @@ export function getCapCodeRef(
}
export function getSolid2dCodeRef(
solid2D: solid2D,
solid2d: Solid2D,
artifactGraph: ArtifactGraph
): CodeRef | Error {
const path = getArtifactOfTypes(
{ key: solid2D.pathId, types: ['path'] },
{ key: solid2d.pathId, types: ['path'] },
artifactGraph
)
if (err(path)) return path
@ -881,7 +371,7 @@ export function getCodeRefsByArtifactId(
artifactGraph: ArtifactGraph
): Array<CodeRef> | null {
const artifact = artifactGraph.get(id)
if (artifact?.type === 'solid2D') {
if (artifact?.type === 'solid2d') {
const codeRef = getSolid2dCodeRef(artifact, artifactGraph)
if (err(codeRef)) return null
return [codeRef]

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import {
Path,
PathToNode,
Expr,
topLevelRange,
} from '../wasm'
import { err } from 'lib/trap'
@ -31,7 +32,7 @@ export function getSketchSegmentFromPathToNode(
const node = nodeMeta.node
if (!node || typeof node.start !== 'number' || !node.end)
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)
}
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 {
getConstraintType,
getTransformInfos,
@ -125,7 +132,7 @@ describe('testing transformAstForSketchLines for equal length constraint', () =>
)
}
const start = codeBeforeLine + line.indexOf('|> ' + 5)
const range: [number, number, boolean] = [start, start, true]
const range = topLevelRange(start, start)
return {
codeRef: codeRefFromRange(range, ast),
}
@ -297,7 +304,7 @@ part001 = startSketchOn('XY')
const comment = ln.split('//')[1]
const start = inputScript.indexOf('//' + comment) - 7
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 start = inputScript.indexOf('//' + comment) - 7
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 start = inputScript.indexOf('//' + comment) - 7
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 start = inputScript.indexOf('//' + comment) - 7
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 expectedConstraintLevel = getConstraintLevelFromSourceRange(
[offsetIndex, offsetIndex, true],
topLevelRange(offsetIndex, offsetIndex),
ast
)
if (err(expectedConstraintLevel)) {

View File

@ -5,8 +5,9 @@ import {
Literal,
ArrayExpression,
BinaryExpression,
ArtifactGraph,
} from './wasm'
import { ArtifactGraph, filterArtifacts } from 'lang/std/artifactGraph'
import { filterArtifacts } from 'lang/std/artifactGraph'
import { isOverlap } from 'lib/utils'
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 { Node } from 'wasm-lib/kcl/bindings/Node'
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 { Operation } from 'wasm-lib/kcl/bindings/Operation'
import { KclErrorWithOutputs } from 'wasm-lib/kcl/bindings/KclErrorWithOutputs'
import { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
import { ArtifactId } from 'wasm-lib/kcl/bindings/ArtifactId'
import { ArtifactCommand } from 'wasm-lib/kcl/bindings/ArtifactCommand'
import { Artifact as RustArtifact } from 'wasm-lib/kcl/bindings/Artifact'
import { ArtifactId } from 'wasm-lib/kcl/bindings/Artifact'
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 { ArtifactCommand } from 'wasm-lib/kcl/bindings/ArtifactCommand'
export type { ArtifactId } from 'wasm-lib/kcl/bindings/ArtifactId'
export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
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 { Program } from '../wasm-lib/kcl/bindings/Program'
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 { LiteralValue } from '../wasm-lib/kcl/bindings/LiteralValue'
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 =
| '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 { 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
* 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
* the frontend).
*/
export function sourceRangeFromRust(s: RustSourceRange): SourceRange {
return [s[0], s[1], s[2] === 0]
export function sourceRangeFromRust(s: SourceRange): SourceRange {
return [s[0], s[1], s[2]]
}
/**
* Create a default SourceRange for testing or as a placeholder.
*/
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 {
return [0, 0, 0]
export function topLevelRange(start: number, end: number): SourceRange {
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 = () => {
@ -234,7 +248,8 @@ export const parse = (code: string | Error): ParseResult | Error => {
parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0]),
[],
[]
[],
defaultArtifactGraph()
)
}
}
@ -258,8 +273,9 @@ export const isPathToNodeNumber = (
export interface ExecState {
memory: ProgramMemory
operations: Operation[]
artifacts: { [key in ArtifactId]?: Artifact }
artifacts: { [key in ArtifactId]?: RustArtifact }
artifactCommands: ArtifactCommand[]
artifactGraph: ArtifactGraph
}
/**
@ -272,18 +288,53 @@ export function emptyExecState(): ExecState {
operations: [],
artifacts: {},
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 {
memory: ProgramMemory.fromRaw(execOutcome.memory),
operations: execOutcome.operations,
artifacts: execOutcome.artifacts,
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 {
[key: string]: KclValue | undefined
}
@ -543,7 +594,7 @@ export const executor = async (
engineCommandManager,
fileSystemManager
)
return execStateFromRust(execOutcome)
return execStateFromRust(execOutcome, node)
} catch (e: any) {
console.log(e)
const parsed: KclErrorWithOutputs = JSON.parse(e.toString())
@ -552,7 +603,8 @@ export const executor = async (
parsed.error.msg,
sourceRangeFromRust(parsed.error.sourceRanges[0]),
parsed.operations,
parsed.artifactCommands
parsed.artifactCommands,
rustArtifactGraphToMap(parsed.artifactGraph)
)
return Promise.reject(kclError)
@ -613,7 +665,8 @@ export const modifyAstForSketch = async (
parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0]),
[],
[]
[],
defaultArtifactGraph()
)
console.log(kclError)
@ -683,7 +736,8 @@ export function programMemoryInit(): ProgramMemory | Error {
parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0]),
[],
[]
[],
defaultArtifactGraph()
)
}
}

View File

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

View File

@ -116,16 +116,16 @@ export const loftValidator = async ({
}
const { selection } = data
if (selection.graphSelections.some((s) => s.artifact?.type !== 'solid2D')) {
return 'Unable to loft, some selection are not solid2Ds'
if (selection.graphSelections.some((s) => s.artifact?.type !== 'solid2d')) {
return 'Unable to loft, some selection are not solid2ds'
}
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) {
return 'Unable to loft, selection contains less than two solid2Ds'
return 'Unable to loft, selection contains less than two solid2ds'
}
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 indices = matches
.filter(Boolean)
.map((match) => match![1])
.map(Number)
.map((match) => (match !== null ? match[1] : '-1'))
.map((maybeMatchIndex) => {
return parseInt(maybeMatchIndex || '0', 10)
})
const maxIndex = Math.max(...indices, -1)
return maxIndex + 1
}
@ -83,6 +85,33 @@ export function doesProjectNameNeedInterpolated(projectName: string) {
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) {
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 { Operation } from 'wasm-lib/kcl/bindings/Operation'
@ -8,7 +8,7 @@ function stdlib(name: string): Operation {
name,
unlabeledArg: null,
labeledArgs: {},
sourceRange: defaultRustSourceRange(),
sourceRange: defaultSourceRange(),
isError: false,
}
}
@ -17,10 +17,10 @@ function userCall(name: string): Operation {
return {
type: 'UserDefinedFunctionCall',
name,
functionSourceRange: defaultRustSourceRange(),
functionSourceRange: defaultSourceRange(),
unlabeledArg: null,
labeledArgs: {},
sourceRange: defaultRustSourceRange(),
sourceRange: defaultSourceRange(),
}
}
function userReturn(): Operation {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -113,7 +113,6 @@ base64 = "0.22.1"
criterion = { version = "0.5.1", features = ["async_tokio"] }
expectorate = "1.1.0"
handlebars = "6.3.0"
iai = "0.1"
image = { version = "0.25.5", default-features = false, features = ["png"] }
insta = { version = "1.41.1", features = ["json", "filters", "redactions"] }
itertools = "0.13.0"
@ -129,10 +128,6 @@ workspace = true
name = "compiler_benchmark_criterion"
harness = false
[[bench]]
name = "compiler_benchmark_iai"
harness = false
[[bench]]
name = "digest_benchmark"
harness = false
@ -142,15 +137,7 @@ name = "lsp_semantic_tokens_benchmark_criterion"
harness = false
required-features = ["lsp-test-util"]
[[bench]]
name = "lsp_semantic_tokens_benchmark_iai"
harness = false
required-features = ["lsp-test-util"]
[[bench]]
name = "executor_benchmark_criterion"
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()
}
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> {
let mut artifact_commands = self.artifact_commands.lock().unwrap();
std::mem::take(&mut *artifact_commands)

View File

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

View File

@ -52,6 +52,7 @@ pub struct EngineConnection {
manager: Arc<EngineCommandManager>,
batch: Arc<Mutex<Vec<(WebSocketRequest, SourceRange)>>>,
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>,
responses: Arc<Mutex<IndexMap<Uuid, WebSocketResponse>>>,
artifact_commands: Arc<Mutex<Vec<ArtifactCommand>>>,
execution_kind: Arc<Mutex<ExecutionKind>>,
}
@ -66,6 +67,7 @@ impl EngineConnection {
manager: Arc::new(manager),
batch: Arc::new(Mutex::new(Vec::new())),
batch_end: Arc::new(Mutex::new(IndexMap::new())),
responses: Arc::new(Mutex::new(IndexMap::new())),
artifact_commands: Arc::new(Mutex::new(Vec::new())),
execution_kind: Default::default(),
})
@ -106,6 +108,11 @@ impl crate::engine::EngineManager for EngineConnection {
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> {
let mut artifact_commands = self.artifact_commands.lock().unwrap();
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)
}

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.
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.
fn take_artifact_commands(&self) -> Vec<ArtifactCommand>;

View File

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

View File

@ -1,15 +1,29 @@
use kittycad_modeling_cmds::ModelingCmd;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use fnv::FnvHashMap;
use indexmap::IndexMap;
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 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
/// engine commands are batched, we don't have the response yet when these are
/// created.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
#[ts(export)]
#[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")]
pub struct ArtifactCommand {
/// Identifier of the command that can be matched with its response.
@ -22,8 +36,8 @@ pub struct ArtifactCommand {
pub command: ModelingCmd,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
pub struct ArtifactId(Uuid);
impl ArtifactId {
@ -56,22 +70,835 @@ impl From<&ArtifactId> for Uuid {
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct Artifact {
pub id: ArtifactId,
#[serde(flatten)]
pub inner: ArtifactInner,
pub source_range: SourceRange,
impl From<ModelingCmdId> for ArtifactId {
fn from(id: ModelingCmdId) -> Self {
Self::new(*id.as_ref())
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum ArtifactInner {
#[serde(rename_all = "camelCase")]
StartSketchOnFace { face_id: Uuid },
#[serde(rename_all = "camelCase")]
StartSketchOnPlane { plane_id: Uuid },
impl From<&ModelingCmdId> for ArtifactId {
fn from(id: &ModelingCmdId) -> Self {
Self::new(*id.as_ref())
}
}
pub type DummyPathToNode = Vec<()>;
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.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CacheInformation {
/// The old information.
pub old: Option<OldAstState>,
@ -17,7 +17,7 @@ pub struct CacheInformation {
}
/// The old ast and program memory.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct OldAstState {
/// The ast.
pub ast: Node<Program>,

View File

@ -3,6 +3,7 @@
use std::{path::PathBuf, sync::Arc};
use anyhow::Result;
use artifact::build_artifact_graph;
use async_recursion::async_recursion;
use indexmap::IndexMap;
use kcmc::{
@ -11,8 +12,8 @@ use kcmc::{
websocket::{ModelingSessionData, OkWebSocketResponseData},
ImageFormat, ModelingCmd,
};
use kittycad_modeling_cmds as kcmc;
use kittycad_modeling_cmds::length_unit::LengthUnit;
use kittycad_modeling_cmds::{self as kcmc, websocket::WebSocketResponse};
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@ -49,18 +50,18 @@ use crate::{
};
// Re-exports.
pub use artifact::{Artifact, ArtifactCommand, ArtifactId, ArtifactInner};
pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, ArtifactId};
pub use cad_op::Operation;
/// State for executing a program.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecState {
pub global: GlobalState,
pub mod_local: ModuleState,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GlobalState {
/// The stable artifact ID generator.
@ -75,6 +76,13 @@ pub struct GlobalState {
/// These are accumulated in the [`ExecutorContext`] but moved here for
/// convenience of the execution cache.
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)]
@ -113,6 +121,8 @@ pub struct ExecOutcome {
pub artifacts: IndexMap<ArtifactId, Artifact>,
/// Output commands to allow building the artifact graph by the caller.
pub artifact_commands: Vec<ArtifactCommand>,
/// Output artifact graph.
pub artifact_graph: ArtifactGraph,
}
impl ExecState {
@ -149,6 +159,7 @@ impl ExecState {
operations: self.mod_local.operations,
artifacts: self.global.artifacts,
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) {
let id = artifact.id;
let id = artifact.id();
self.global.artifacts.insert(id, artifact);
}
@ -216,6 +227,8 @@ impl GlobalState {
module_infos: Default::default(),
artifacts: Default::default(),
artifact_commands: Default::default(),
artifact_responses: Default::default(),
artifact_graph: Default::default(),
};
// TODO(#4434): Use the top-level file's path.
@ -2239,22 +2252,56 @@ impl ExecutorContext {
.await
.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
.map_err(|e| {
KclErrorWithOutputs::new(
e,
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
.global
.artifact_commands
.extend(self.engine.take_artifact_commands());
let session_data = self.engine.get_session_data();
Ok(session_data)
exec_state.global.artifact_responses.extend(self.engine.responses());
// 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.

View File

@ -91,12 +91,12 @@ async fn execute(test_name: &str, render_to_png: bool) {
)
.await;
match exec_res {
Ok((program_memory, ops, artifact_commands, png)) => {
Ok((exec_state, png)) => {
if render_to_png {
twenty_twenty::assert_image(format!("tests/{test_name}/rendered_model.png"), &png, 0.99);
}
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[].**[].to[]" => 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", || {
insta::assert_json_snapshot!("ops", ops);
insta::assert_json_snapshot!("ops", exec_state.mod_local.operations);
});
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.*.y" => 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) => {
match e.error {
@ -177,6 +197,90 @@ mod cube {
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 {
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)]
#[ts(export, type = "[number, number, number]")]
pub struct SourceRange([usize; 3]);
@ -58,6 +65,12 @@ impl SourceRange {
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.
pub fn start(&self) -> usize {
self.0[0]

View File

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

View File

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