Compare commits

...

7 Commits

Author SHA1 Message Date
9a537da183 Show toolbar tooltips on hover only, hide when dropdowns are open (#5109)
* Show toolbar tooltips on hover only, hide when dropdowns are open

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Re-run CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-18 05:22:22 -05:00
df81b76b8b Bug fix follow-up for create project (#5105)
* fix dumb mistake in command flow for #5083

* Add e2e test for creating projects with the default interpolated name

* Drop that number to 12 ain't got all day

* Why do I have a kcl-samples submodule hanging around?

* Empty commit to remove the submodule
2025-01-17 23:10:28 +00:00
ac3f7ab712 Rust: Remove iai benchmark tests (#5102)
We don't get much value from these, we can always run criterion or valgrind locally.

If we want to measure instruction counts, we should be using codspeed.io instead because
they support visualizing and tracking over time.

If we want to track performance over time we should be using Kevin's perf monitor machine.
2025-01-17 15:42:51 -06:00
dac91d3b79 Add uniqueness check to "Create project" command (#5100)
* Add failing playwright test

* Make create generate a unique name if the given one collides

* Add a new consolidated getUniqueProjectName function with tests

* Use getUniqueProjectName

* Replace "New project" button text with "Create project"
cc @pierremtb

* Extend the e2e test to show the incrementing behavior
cc @lf94
2025-01-17 19:46:52 +00:00
0698432abf Rust artifact graph (#5068)
* Start porting artifact graph creation to Rust

* Add most of artifact graph creation

* Add handling loft command from recent PR

* Refactor artifact merge code so that it errors when a new artifact type is added

* Add sweep subtype

* Finish implementation of build artifact graph

* Fix wasm.ts to use new combined generated ts-rs file

* Fix Rust lints

* Fix lints

* Fix up replacement code

* Add artifact graph to WASM outcome

* Add artifact graph to simulation test output

* Add new artifact graph output snapshots

* Fix wall field and reduce unreachable code

* Change field order for subtype

* Change subtype to be determined from the request, like the TS

* Fix plane sweep_id

* Condense code

* Change ID types to be properly optional

* Change to favor the new ID, the same as TS

* Fix to make error impossible

* Rename artifact type tag values to match TS

* Fix name of field on Cap

* Update outputs

* Change to use Rust source range

* Update output snapshots

* Add conversion to mermaid mind map and add to snapshot tests

* Add new mermaid mind map output

* Add flowchart

* Remove raw artifact graph from tests

* Remove JSON artifact graph output

* Update output file with header

* Update output after adding flowchart

* Fix flowchart to not have duplicate edges, one in each direction

* Fix not not output duplicate edges in flowcharts

* Change flowchart edge style to be more obvious when a direction is missing

* Update output after deduplication of edges

* Fix not not skip sketch-on-face artifacts

* Add docs

* Fix edge iteration order to be stable

* Update output after fixing order

* Port TS artifactGraph.test.ts tests to simulation tests

* Add grouping segments and solid2ds with their path

* Update output flowcharts since grouping paths

* Remove TS artifactGraph tests

* Remove unused d3 dependencies

* Fix to track loft ID on paths

* Add command ID to error messages

* Move artifact graph test code to a separate file since it's a large file

* Reduce function visibility

* Remove TS artifact graph code

* Fix spelling error with serde

* Add TODO for edge cut consumed ID

* Add comment about mermaid edge rank

* Fix mermaid flowchart edge cuts to appear as children of their edges

* Update output since fixing flowchart order

* Fix to always build the artifact graph even when there's a KCL error

* Add artifact graph to error output

* Change optional ID merge to match TS

* Remove redundant SourceRange definition

* Remove Rust-flavored default source range function

* Add helper for source range creation

* Update doc comment for the website

* Update docs after doc comment change

* Fix to save engine responses in execution cache

* Remove unused import

* Fix to not call WASM function before beforeAll callback is run

* Remove more unused imports
2025-01-17 14:34:36 -05:00
0592d3b5da Selection dry-run validation for Shell (#4775)
* WIP: mess with shell selection validation
Will eventually fix #4711

* Update from main

* WIP: not working yet

* Working loft dry run validator

* Clean up shell (still not working)

* Bump kittycad-modeling-cmds

* Clean up

* Add logging

* Add proper object_id and face_id mapping, still not working for shell

* Fix faceId

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* Working validation after engine merge; Clean up

* Fix codespell

* Add pw test

* More clean up

* Back to basics

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* Clean up

* Fix tests

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* Remove kcl-samples

---------

Co-authored-by: Adam Chalmers <adam.chalmers@zoo.dev>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-17 10:16:18 -05:00
max
31e4d60045 Refactor Fillet Playwright Test to the new Fixture-based Approach (#4909)
* add locator

* new test

* remove old test

* separation of setup steps

* improved click reliability

* fix ubuntu

* ubuntu fix 2

* ubuntu fix 3

* flaky cmdbar ubuntu

* ubuntu fix second yellow

* ubuntu update

* enable test for windows

* step(Initial test setup)

* extra await, just in case

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* screenshot

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-17 10:21:55 +01:00
351 changed files with 47955 additions and 2835 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

@ -45,46 +45,6 @@ test.describe('Command bar tests', () => {
)
})
// TODO: fix this test after the electron migration
test.fixme('Fillet from command bar', async ({ page, homePage }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn('XY')
|> startProfileAt([-5, -5], %)
|> line([0, 10], %)
|> line([10, 0], %)
|> line([0, -10], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-10, sketch001)`
)
})
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const selectSegment = () => page.getByText(`line([0, -10], %)`).click()
await selectSegment()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Fillet' }).click()
await page.waitForTimeout(100)
await page.keyboard.press('Enter') // skip selection
await page.waitForTimeout(100)
await page.keyboard.press('Enter') // accept default radius
await page.waitForTimeout(100)
await page.keyboard.press('Enter') // submit
await page.waitForTimeout(100)
await expect(page.locator('.cm-activeLine')).toContainText(
`fillet({ radius = ${KCL_DEFAULT_LENGTH}, tags = [seg01] }, %)`
)
})
test('Command bar can change a setting, and switch back and forth between arguments', async ({
page,
homePage,

View File

@ -135,4 +135,20 @@ export class CmdBarFixture {
await promptEditCommand.first().click()
}
}
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()
}
}

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

@ -15,6 +15,7 @@ export class ToolbarFixture {
extrudeButton!: Locator
loftButton!: Locator
sweepButton!: Locator
filletButton!: Locator
chamferButton!: Locator
shellButton!: Locator
offsetPlaneButton!: Locator
@ -43,6 +44,7 @@ export class ToolbarFixture {
this.extrudeButton = page.getByTestId('extrude')
this.loftButton = page.getByTestId('loft')
this.sweepButton = page.getByTestId('sweep')
this.filletButton = page.getByTestId('fillet3d')
this.chamferButton = page.getByTestId('chamfer3d')
this.shellButton = page.getByTestId('shell')
this.offsetPlaneButton = page.getByTestId('plane-offset')
@ -61,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

@ -1020,6 +1020,221 @@ sketch002 = startSketchOn('XZ')
})
})
test(`Fillet point-and-click`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
// Code samples
const initialCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-12, -6], %)
|> line([0, 12], %)
|> line([24, 0], %)
|> line([0, -12], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-12, sketch001)
`
const firstFilletDeclaration = 'fillet({ radius = 5, tags = [seg01] }, %)'
const secondFilletDeclaration =
'fillet({ radius = 5, tags = [getOppositeEdge(seg01)] }, %)'
// Locators
const firstEdgeLocation = { x: 600, y: 193 }
const secondEdgeLocation = { x: 600, y: 383 }
const bodyLocation = { x: 630, y: 290 }
const [clickOnFirstEdge] = scene.makeMouseHelpers(
firstEdgeLocation.x,
firstEdgeLocation.y
)
const [clickOnSecondEdge] = scene.makeMouseHelpers(
secondEdgeLocation.x,
secondEdgeLocation.y
)
// Colors
const edgeColorWhite: [number, number, number] = [248, 248, 248]
const edgeColorYellow: [number, number, number] = [251, 251, 40] // Mac:B=67 Ubuntu:B=12
const bodyColor: [number, number, number] = [155, 155, 155]
const filletColor: [number, number, number] = [127, 127, 127]
const backgroundColor: [number, number, number] = [30, 30, 30]
const lowTolerance = 20
const highTolerance = 40
// Setup
await test.step(`Initial test setup`, async () => {
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
// verify modeling scene is loaded
await scene.expectPixelColor(
backgroundColor,
secondEdgeLocation,
lowTolerance
)
// wait for stream to load
await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance)
})
// Test 1: Command bar flow with preselected edges
await test.step(`Select first edge`, async () => {
await scene.expectPixelColor(
edgeColorWhite,
firstEdgeLocation,
lowTolerance
)
await clickOnFirstEdge()
await scene.expectPixelColor(
edgeColorYellow,
firstEdgeLocation,
highTolerance // Ubuntu color mismatch can require high tolerance
)
})
await test.step(`Apply fillet to the preselected edge`, async () => {
await page.waitForTimeout(100)
await toolbar.filletButton.click()
await cmdBar.expectState({
commandName: 'Fillet',
highlightedHeaderArg: 'selection',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Radius: '',
},
stage: 'arguments',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
commandName: 'Fillet',
highlightedHeaderArg: 'radius',
currentArgKey: 'radius',
currentArgValue: '5',
headerArguments: {
Selection: '1 face',
Radius: '',
},
stage: 'arguments',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
commandName: 'Fillet',
headerArguments: {
Selection: '1 face',
Radius: '5',
},
stage: 'review',
})
await cmdBar.progressCmdBar()
})
await test.step(`Confirm code is added to the editor`, async () => {
await editor.expectEditor.toContain(firstFilletDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: ['|>fillet({radius=5,tags=[seg01]},%)'],
highlightedCode: '',
})
})
await test.step(`Confirm scene has changed`, async () => {
await scene.expectPixelColor(filletColor, firstEdgeLocation, lowTolerance)
})
// Test 2: Command bar flow without preselected edges
await test.step(`Open fillet UI without selecting edges`, async () => {
await page.waitForTimeout(100)
await toolbar.filletButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Radius: '',
},
highlightedHeaderArg: 'selection',
commandName: 'Fillet',
})
})
await test.step(`Select second edge`, async () => {
await scene.expectPixelColor(
edgeColorWhite,
secondEdgeLocation,
lowTolerance
)
await clickOnSecondEdge()
await scene.expectPixelColor(
edgeColorYellow,
secondEdgeLocation,
highTolerance // Ubuntu color mismatch can require high tolerance
)
})
await test.step(`Apply fillet to the second edge`, async () => {
await cmdBar.expectState({
commandName: 'Fillet',
highlightedHeaderArg: 'selection',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Radius: '',
},
stage: 'arguments',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
commandName: 'Fillet',
highlightedHeaderArg: 'radius',
currentArgKey: 'radius',
currentArgValue: '5',
headerArguments: {
Selection: '1 sweepEdge',
Radius: '',
},
stage: 'arguments',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
commandName: 'Fillet',
headerArguments: {
Selection: '1 sweepEdge',
Radius: '5',
},
stage: 'review',
})
await cmdBar.progressCmdBar()
})
await test.step(`Confirm code is added to the editor`, async () => {
await editor.expectEditor.toContain(secondFilletDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: ['radius=5,'],
highlightedCode: '',
})
})
await test.step(`Confirm scene has changed`, async () => {
await scene.expectPixelColor(
backgroundColor,
secondEdgeLocation,
lowTolerance
)
})
})
test(`Chamfer point-and-click`, async ({
context,
page,
@ -1029,9 +1244,6 @@ test(`Chamfer point-and-click`, async ({
toolbar,
cmdBar,
}) => {
// TODO: fix this test on windows after the electron migration
test.skip(process.platform === 'win32', 'Skip on windows')
// Code samples
const initialCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-12, -6], %)
@ -1069,13 +1281,13 @@ extrude001 = extrude(-12, sketch001)
const highTolerance = 40
// Setup
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await test.step(`Initial test setup`, async () => {
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await test.step(`Verify scene is loaded`, async () => {
// verify modeling scene is loaded
await scene.expectPixelColor(
backgroundColor,
@ -1103,6 +1315,7 @@ extrude001 = extrude(-12, sketch001)
})
await test.step(`Apply chamfer to the preselected edge`, async () => {
await page.waitForTimeout(100)
await toolbar.chamferButton.click()
await cmdBar.expectState({
commandName: 'Chamfer',
@ -1154,6 +1367,7 @@ extrude001 = extrude(-12, sketch001)
// Test 2: Command bar flow without preselected edges
await test.step(`Open chamfer UI without selecting edges`, async () => {
await page.waitForTimeout(100)
await toolbar.chamferButton.click()
await cmdBar.expectState({
stage: 'arguments',
@ -1289,6 +1503,7 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
await clickOnCap()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
@ -1309,6 +1524,7 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => {
await toolbar.shellButton.click()
await cmdBar.progressCmdBar()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
@ -1390,6 +1606,7 @@ extrude001 = extrude(40, sketch001)
await page.waitForTimeout(500)
await page.keyboard.up('Shift')
await cmdBar.progressCmdBar()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
@ -1513,3 +1730,61 @@ shellSketchOnFacesCases.forEach((initialCode, index) => {
})
})
})
test(`Shell dry-run validation rejects sweeps`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn('YZ')
|> circle({
center = [0, 0],
radius = 500
}, %)
sketch002 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> xLine(-2000, %)
sweep001 = sweep({ path = sketch002 }, sketch001)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// One dumb hardcoded screen pixel value
const testPoint = { x: 500, y: 250 }
const [clickOnSweep] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
await test.step(`Confirm sweep exists`, async () => {
await toolbar.closePane('code')
await scene.expectPixelColor([231, 231, 231], testPoint, 15)
})
await test.step(`Go through the Shell flow and fail validation with a toast`, async () => {
await toolbar.shellButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Thickness: '',
},
highlightedHeaderArg: 'selection',
commandName: 'Shell',
})
await clickOnSweep()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await expect(
page.getByText('Unable to shell with the provided selection')
).toBeVisible()
await page.waitForTimeout(1000)
})
})

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()
})
}
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 44 KiB

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

@ -210,6 +210,7 @@ export function Toolbar({
<ToolbarItemTooltip
itemConfig={maybeIconConfig[0]}
configCallbackProps={configCallbackProps}
className="ui-open:!hidden"
/>
</ActionButtonDropdown>
)
@ -277,9 +278,11 @@ export function Toolbar({
const ToolbarItemTooltip = memo(function ToolbarItemContents({
itemConfig,
configCallbackProps,
className,
}: {
itemConfig: ToolbarItemResolved
configCallbackProps: ToolbarItemCallbackProps
className?: string
}) {
const { state } = useModelingContext()
@ -305,8 +308,9 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
? ({ '-webkit-app-region': 'no-drag' } as React.CSSProperties)
: {}
}
hoverOnly
position="bottom"
wrapperClassName="!p-4 !pointer-events-auto"
wrapperClassName={'!p-4 !pointer-events-auto ' + className}
contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch"
>
<div className="rounded-top flex items-center gap-2 pt-3 pb-2 px-2 bg-chalkboard-20/50 dark:bg-chalkboard-80/50">

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

@ -134,6 +134,7 @@ function CommandArgOptionInput({
</label>
<Combobox.Input
id="option-input"
data-testid="cmd-bar-arg-value"
ref={inputRef}
onChange={(event) =>
!event.target.disabled && setQuery(event.target.value)

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,
} from 'lib/desktopFS'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import useStateMachineCommands from 'hooks/useStateMachineCommands'
@ -195,16 +196,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) => [
code.indexOf(selectionSnippet),
code.indexOf(selectionSnippet) + selectionSnippet.length,
true,
]
const segmentRanges: Array<SourceRange> = selectionSnippets.map(
(selectionSnippet) =>
topLevelRange(
code.indexOf(selectionSnippet),
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

@ -9,7 +9,11 @@ import { Selections } from 'lib/selections'
import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap'
import { modelingMachine, SketchTool } from 'machines/modelingMachine'
import { loftValidator, revolveAxisValidator } from './validators'
import {
loftValidator,
revolveAxisValidator,
shellValidator,
} from './validators'
type OutputFormat = Models['OutputFormat_type']
type OutputTypeKey = OutputFormat['type']
@ -276,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,
@ -308,7 +312,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
args: {
profile: {
inputType: 'selection',
selectionTypes: ['solid2D'],
selectionTypes: ['solid2d'],
required: true,
skip: true,
multiple: false,
@ -333,7 +337,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
args: {
selection: {
inputType: 'selection',
selectionTypes: ['solid2D'],
selectionTypes: ['solid2d'],
multiple: true,
required: true,
skip: false,
@ -351,12 +355,13 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
selectionTypes: ['cap', 'wall'],
multiple: true,
required: true,
skip: false,
validation: shellValidator,
},
thickness: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_LENGTH,
required: true,
// TODO: add dry-run validation on thickness param
},
},
},
@ -368,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,
@ -573,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 () => {
@ -153,3 +153,57 @@ export const loftValidator = async ({
return 'Unable to loft with selected sketches'
}
}
export const shellValidator = async ({
data,
}: {
data: { selection: Selections }
}): Promise<boolean | string> => {
if (!isSelections(data.selection)) {
return 'Unable to shell, selections are missing'
}
// No validation on the faces, filtering is done upstream and we have the dry run validation just below
const face_ids = data.selection.graphSelections.flatMap((s) =>
s.artifact ? s.artifact.id : []
)
// We don't have the concept of solid3ds in TS yet.
// So we're listing out the sweeps as if they were solids and taking the first one, just like in Rust for Shell:
// https://github.com/KittyCAD/modeling-app/blob/e61fff115b9fa94aaace6307b1842cc15d41655e/src/wasm-lib/kcl/src/std/shell.rs#L237-L238
// TODO: This is one cheap way to make sketch-on-face supported now but will likely fail multiple solids
const object_id = engineCommandManager.artifactGraph
.values()
.find((v) => v.type === 'sweep')?.pathId
if (!object_id) {
return "Unable to shell, couldn't find the solid"
}
const shellCommand = async () => {
// TODO: figure out something better than an arbitrarily small value
const DEFAULT_THICKNESS: Models['LengthUnit_type'] = 1e-9
const DEFAULT_HOLLOW = false
const cmdArgs = {
face_ids,
object_id,
hollow: DEFAULT_HOLLOW,
shell_thickness: DEFAULT_THICKNESS,
}
return await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'solid3d_shell_face',
...cmdArgs,
},
})
}
const attemptShell = await dryRunWrapper(shellCommand)
if (attemptShell?.success) {
return true
}
return 'Unable to shell with the provided selection'
}

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

@ -148,7 +148,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
---

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