Compare commits
31 Commits
pierremtb/
...
v0.30.0
Author | SHA1 | Date | |
---|---|---|---|
00e97257ae | |||
aeb656d176 | |||
ac49ebd6e0 | |||
b40f03ad25 | |||
a8ad86e645 | |||
87f50cd5e9 | |||
0400e6228e | |||
26f150fd6c | |||
3049f405f5 | |||
53d40301dc | |||
671c01e36f | |||
e80151979b | |||
668e2afb99 | |||
548c664db0 | |||
d3a3f4410c | |||
22eb343171 | |||
f2cfa4d5cf | |||
3f1f40eeba | |||
ff2d161606 | |||
210c78029d | |||
e27840219b | |||
c943a3f192 | |||
6aa588f09f | |||
59a6333aad | |||
403f1507ae | |||
eac7b83504 | |||
667500d1b9 | |||
b15aac9f48 | |||
54153aa646 | |||
943cf21d34 | |||
5a6728c45a |
3
.github/workflows/build-apps.yml
vendored
3
.github/workflows/build-apps.yml
vendored
@ -165,7 +165,6 @@ jobs:
|
|||||||
- name: Build the app (release)
|
- name: Build the app (release)
|
||||||
if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }}
|
if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }}
|
||||||
env:
|
env:
|
||||||
PUBLISH_FOR_PULL_REQUEST: true
|
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
@ -173,7 +172,6 @@ jobs:
|
|||||||
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
|
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||||
CSC_FOR_PULL_REQUEST: true
|
|
||||||
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
|
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
|
||||||
run: yarn electron-builder --config --publish always
|
run: yarn electron-builder --config --publish always
|
||||||
|
|
||||||
@ -229,7 +227,6 @@ jobs:
|
|||||||
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
|
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||||
CSC_FOR_PULL_REQUEST: true
|
|
||||||
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
|
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
|
||||||
run: yarn electron-builder --config --publish always
|
run: yarn electron-builder --config --publish always
|
||||||
|
|
||||||
|
@ -22,3 +22,5 @@ once fixed in engine will just start working here with no language changes.
|
|||||||
|
|
||||||
- **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple
|
- **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple
|
||||||
chamfer cases work currently.
|
chamfer cases work currently.
|
||||||
|
|
||||||
|
- **Appearance**: Changing the appearance on a loft does not work.
|
||||||
|
210
docs/kcl/appearance.md
Normal file
210
docs/kcl/appearance.md
Normal file
File diff suppressed because one or more lines are too long
@ -19,6 +19,7 @@ layout: manual
|
|||||||
* [`angledLineThatIntersects`](kcl/angledLineThatIntersects)
|
* [`angledLineThatIntersects`](kcl/angledLineThatIntersects)
|
||||||
* [`angledLineToX`](kcl/angledLineToX)
|
* [`angledLineToX`](kcl/angledLineToX)
|
||||||
* [`angledLineToY`](kcl/angledLineToY)
|
* [`angledLineToY`](kcl/angledLineToY)
|
||||||
|
* [`appearance`](kcl/appearance)
|
||||||
* [`arc`](kcl/arc)
|
* [`arc`](kcl/arc)
|
||||||
* [`arcTo`](kcl/arcTo)
|
* [`arcTo`](kcl/arcTo)
|
||||||
* [`asin`](kcl/asin)
|
* [`asin`](kcl/asin)
|
||||||
|
@ -45,7 +45,7 @@ circles = map([1..3], drawCircle)
|
|||||||
```js
|
```js
|
||||||
r = 10 // radius
|
r = 10 // radius
|
||||||
// Call `map`, using an anonymous function instead of a named one.
|
// Call `map`, using an anonymous function instead of a named one.
|
||||||
circles = map([1..3], (id) {
|
circles = map([1..3], fn(id) {
|
||||||
return startSketchOn("XY")
|
return startSketchOn("XY")
|
||||||
|> circle({ center = [id * 2 * r, 0], radius = r }, %)
|
|> circle({ center = [id * 2 * r, 0], radius = r }, %)
|
||||||
})
|
})
|
||||||
|
@ -61,7 +61,7 @@ assertEqual(sum([1, 2, 3]), 6, 0.00001, "1 + 2 + 3 summed is 6")
|
|||||||
// an anonymous `add` function as its parameter, instead of declaring a
|
// an anonymous `add` function as its parameter, instead of declaring a
|
||||||
// named function outside.
|
// named function outside.
|
||||||
arr = [1, 2, 3]
|
arr = [1, 2, 3]
|
||||||
sum = reduce(arr, 0, (i, result_so_far) {
|
sum = reduce(arr, 0, fn(i, result_so_far) {
|
||||||
return i + result_so_far
|
return i + result_so_far
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -84,7 +84,7 @@ fn decagon(radius) {
|
|||||||
// Use a `reduce` to draw the remaining decagon sides.
|
// Use a `reduce` to draw the remaining decagon sides.
|
||||||
// For each number in the array 1..10, run the given function,
|
// For each number in the array 1..10, run the given function,
|
||||||
// which takes a partially-sketched decagon and adds one more edge to it.
|
// which takes a partially-sketched decagon and adds one more edge to it.
|
||||||
fullDecagon = reduce([1..10], startOfDecagonSketch, (i, partialDecagon) {
|
fullDecagon = reduce([1..10], startOfDecagonSketch, fn(i, partialDecagon) {
|
||||||
// Draw one edge of the decagon.
|
// Draw one edge of the decagon.
|
||||||
x = cos(stepAngle * i) * radius
|
x = cos(stepAngle * i) * radius
|
||||||
y = sin(stepAngle * i) * radius
|
y = sin(stepAngle * i) * radius
|
||||||
|
2889
docs/kcl/std.json
2889
docs/kcl/std.json
File diff suppressed because it is too large
Load Diff
23
docs/kcl/types/AppearanceData.md
Normal file
23
docs/kcl/types/AppearanceData.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
title: "AppearanceData"
|
||||||
|
excerpt: "Data for appearance."
|
||||||
|
layout: manual
|
||||||
|
---
|
||||||
|
|
||||||
|
Data for appearance.
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Type | Description | Required |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| `color` |`string`| Color of the new material, a hex string like "#ff0000". | No |
|
||||||
|
| `metalness` |`number` (**maximum:** 100.0)| Metalness of the new material, a percentage like 95.7. | No |
|
||||||
|
| `roughness` |`number` (**maximum:** 100.0)| Roughness of the new material, a percentage like 95.7. | No |
|
||||||
|
|
||||||
|
|
@ -7,6 +7,7 @@ export class ToolbarFixture {
|
|||||||
|
|
||||||
extrudeButton!: Locator
|
extrudeButton!: Locator
|
||||||
loftButton!: Locator
|
loftButton!: Locator
|
||||||
|
shellButton!: Locator
|
||||||
offsetPlaneButton!: Locator
|
offsetPlaneButton!: Locator
|
||||||
startSketchBtn!: Locator
|
startSketchBtn!: Locator
|
||||||
lineBtn!: Locator
|
lineBtn!: Locator
|
||||||
@ -28,6 +29,7 @@ export class ToolbarFixture {
|
|||||||
this.page = page
|
this.page = page
|
||||||
this.extrudeButton = page.getByTestId('extrude')
|
this.extrudeButton = page.getByTestId('extrude')
|
||||||
this.loftButton = page.getByTestId('loft')
|
this.loftButton = page.getByTestId('loft')
|
||||||
|
this.shellButton = page.getByTestId('shell')
|
||||||
this.offsetPlaneButton = page.getByTestId('plane-offset')
|
this.offsetPlaneButton = page.getByTestId('plane-offset')
|
||||||
this.startSketchBtn = page.getByTestId('sketch')
|
this.startSketchBtn = page.getByTestId('sketch')
|
||||||
this.lineBtn = page.getByTestId('line')
|
this.lineBtn = page.getByTestId('line')
|
||||||
|
@ -768,3 +768,168 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const shellPointAndClickCapCases = [
|
||||||
|
{ shouldPreselect: true },
|
||||||
|
{ shouldPreselect: false },
|
||||||
|
]
|
||||||
|
shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
|
||||||
|
test(`Shell point-and-click cap (preselected sketches: ${shouldPreselect})`, async ({
|
||||||
|
app,
|
||||||
|
scene,
|
||||||
|
editor,
|
||||||
|
toolbar,
|
||||||
|
cmdBar,
|
||||||
|
}) => {
|
||||||
|
const initialCode = `sketch001 = startSketchOn('XZ')
|
||||||
|
|> circle({ center = [0, 0], radius = 30 }, %)
|
||||||
|
extrude001 = extrude(30, sketch001)
|
||||||
|
`
|
||||||
|
await app.initialise(initialCode)
|
||||||
|
|
||||||
|
// One dumb hardcoded screen pixel value
|
||||||
|
const testPoint = { x: 575, y: 200 }
|
||||||
|
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||||
|
const shellDeclaration =
|
||||||
|
"shell001 = shell({ faces = ['end'], thickness = 5 }, extrude001)"
|
||||||
|
|
||||||
|
await test.step(`Look for the grey of the shape`, async () => {
|
||||||
|
await scene.expectPixelColor([127, 127, 127], testPoint, 15)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!shouldPreselect) {
|
||||||
|
await test.step(`Go through the command bar flow without preselected faces`, async () => {
|
||||||
|
await toolbar.shellButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'selection',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '',
|
||||||
|
Thickness: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'selection',
|
||||||
|
commandName: 'Shell',
|
||||||
|
})
|
||||||
|
await clickOnCap()
|
||||||
|
await app.page.waitForTimeout(500)
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '1 cap',
|
||||||
|
Thickness: '5',
|
||||||
|
},
|
||||||
|
commandName: 'Shell',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await test.step(`Preselect the cap`, async () => {
|
||||||
|
await clickOnCap()
|
||||||
|
await app.page.waitForTimeout(500)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => {
|
||||||
|
await toolbar.shellButton.click()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '1 cap',
|
||||||
|
Thickness: '5',
|
||||||
|
},
|
||||||
|
commandName: 'Shell',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
||||||
|
await editor.expectEditor.toContain(shellDeclaration)
|
||||||
|
await editor.expectState({
|
||||||
|
diagnostics: [],
|
||||||
|
activeLines: [shellDeclaration],
|
||||||
|
highlightedCode: '',
|
||||||
|
})
|
||||||
|
await scene.expectPixelColor([146, 146, 146], testPoint, 15)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Shell point-and-click wall', async ({
|
||||||
|
app,
|
||||||
|
page,
|
||||||
|
scene,
|
||||||
|
editor,
|
||||||
|
toolbar,
|
||||||
|
cmdBar,
|
||||||
|
}) => {
|
||||||
|
const initialCode = `sketch001 = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-20, 20], %)
|
||||||
|
|> xLine(40, %)
|
||||||
|
|> yLine(-60, %)
|
||||||
|
|> xLine(-40, %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)
|
||||||
|
extrude001 = extrude(40, sketch001)
|
||||||
|
`
|
||||||
|
await app.initialise(initialCode)
|
||||||
|
|
||||||
|
// One dumb hardcoded screen pixel value
|
||||||
|
const testPoint = { x: 580, y: 180 }
|
||||||
|
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||||
|
const [clickOnWall] = scene.makeMouseHelpers(testPoint.x, testPoint.y + 70)
|
||||||
|
const mutatedCode = 'xLine(-40, %, $seg01)'
|
||||||
|
const shellDeclaration =
|
||||||
|
"shell001 = shell({ faces = ['end', seg01], thickness = 5}, extrude001)"
|
||||||
|
const formattedOutLastLine = '}, extrude001)'
|
||||||
|
|
||||||
|
await test.step(`Look for the grey of the shape`, async () => {
|
||||||
|
await scene.expectPixelColor([99, 99, 99], testPoint, 15)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Go through the command bar flow, selecting a wall and keeping default thickness`, async () => {
|
||||||
|
await toolbar.shellButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'selection',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '',
|
||||||
|
Thickness: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'selection',
|
||||||
|
commandName: 'Shell',
|
||||||
|
})
|
||||||
|
await clickOnCap()
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
await clickOnWall()
|
||||||
|
await app.page.waitForTimeout(500)
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '1 cap, 1 face',
|
||||||
|
Thickness: '5',
|
||||||
|
},
|
||||||
|
commandName: 'Shell',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
||||||
|
await editor.expectEditor.toContain(mutatedCode)
|
||||||
|
await editor.expectEditor.toContain(shellDeclaration)
|
||||||
|
await editor.expectState({
|
||||||
|
diagnostics: [],
|
||||||
|
activeLines: [formattedOutLastLine],
|
||||||
|
highlightedCode: '',
|
||||||
|
})
|
||||||
|
await scene.expectPixelColor([49, 49, 49], testPoint, 15)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
@ -950,7 +950,75 @@ test(
|
|||||||
|
|
||||||
test.describe('Grid visibility', { tag: '@snapshot' }, () => {
|
test.describe('Grid visibility', { tag: '@snapshot' }, () => {
|
||||||
// FIXME: Skip on macos its being weird.
|
// FIXME: Skip on macos its being weird.
|
||||||
test.skip(process.platform === 'darwin', 'Skip on macos')
|
// test.skip(process.platform === 'darwin', 'Skip on macos')
|
||||||
|
|
||||||
|
test('Grid turned off to on via command bar', async ({ page }) => {
|
||||||
|
const u = await getUtils(page)
|
||||||
|
const stream = page.getByTestId('stream')
|
||||||
|
const mask = [
|
||||||
|
page.locator('#app-header'),
|
||||||
|
page.locator('#sidebar-top-ribbon'),
|
||||||
|
page.locator('#sidebar-bottom-ribbon'),
|
||||||
|
]
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
await u.openDebugPanel()
|
||||||
|
// wait for execution done
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-message-type="execution-done"]')
|
||||||
|
).toHaveCount(1)
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
await u.closeKclCodePanel()
|
||||||
|
// TODO: Find a way to truly know that the objects have finished
|
||||||
|
// rendering, because an execution-done message is not sufficient.
|
||||||
|
await page.waitForTimeout(1000)
|
||||||
|
|
||||||
|
// Open the command bar.
|
||||||
|
await page
|
||||||
|
.getByRole('button', { name: 'Commands', exact: false })
|
||||||
|
.or(page.getByRole('button', { name: '⌘K' }))
|
||||||
|
.click()
|
||||||
|
const commandName = 'show scale grid'
|
||||||
|
const commandOption = page.getByRole('option', {
|
||||||
|
name: commandName,
|
||||||
|
exact: false,
|
||||||
|
})
|
||||||
|
const cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||||
|
// This selector changes after we set the setting
|
||||||
|
await cmdSearchBar.fill(commandName)
|
||||||
|
await expect(commandOption).toBeVisible()
|
||||||
|
await commandOption.click()
|
||||||
|
|
||||||
|
const toggleInput = page.getByPlaceholder('Off')
|
||||||
|
await expect(toggleInput).toBeVisible()
|
||||||
|
await expect(toggleInput).toBeFocused()
|
||||||
|
|
||||||
|
// Select On
|
||||||
|
await page.keyboard.press('ArrowDown')
|
||||||
|
await expect(page.getByRole('option', { name: 'Off' })).toHaveAttribute(
|
||||||
|
'data-headlessui-state',
|
||||||
|
'active selected'
|
||||||
|
)
|
||||||
|
await page.keyboard.press('ArrowUp')
|
||||||
|
await expect(page.getByRole('option', { name: 'On' })).toHaveAttribute(
|
||||||
|
'data-headlessui-state',
|
||||||
|
'active'
|
||||||
|
)
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
|
||||||
|
// Check the toast appeared
|
||||||
|
await expect(
|
||||||
|
page.getByText(`Set show scale grid to "true" as a user default`)
|
||||||
|
).toBeVisible()
|
||||||
|
|
||||||
|
await expect(stream).toHaveScreenshot({
|
||||||
|
maxDiffPixels: 100,
|
||||||
|
mask,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test('Grid turned off', async ({ page }) => {
|
test('Grid turned off', async ({ page }) => {
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
|
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
Binary file not shown.
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Binary file not shown.
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
@ -26,7 +26,17 @@ test.describe('Testing constraints', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
// constants and locators
|
||||||
|
const lengthValue = {
|
||||||
|
old: '20',
|
||||||
|
new: '25',
|
||||||
|
}
|
||||||
|
const cmdBarKclInput = page
|
||||||
|
.getByTestId('cmd-bar-arg-value')
|
||||||
|
.getByRole('textbox')
|
||||||
|
const cmdBarSubmitButton = page.getByRole('button', {
|
||||||
|
name: 'arrow right Continue',
|
||||||
|
})
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
@ -36,26 +46,26 @@ test.describe('Testing constraints', () => {
|
|||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
// Click the line of code for line.
|
// Click the line of code for line.
|
||||||
await page.getByText(`line([0, 20], %)`).click() // TODO remove this and reinstate // await topHorzSegmentClick()
|
// TODO remove this and reinstate `await topHorzSegmentClick()`
|
||||||
|
await page.getByText(`line([0, ${lengthValue.old}], %)`).click()
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
// enter sketch again
|
// enter sketch again
|
||||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||||
await page.waitForTimeout(500) // wait for animation
|
await page.waitForTimeout(500) // wait for animation
|
||||||
|
|
||||||
const startXPx = 500
|
|
||||||
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
|
|
||||||
await page.keyboard.down('Shift')
|
|
||||||
await page.mouse.click(834, 244)
|
|
||||||
await page.keyboard.up('Shift')
|
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByRole('button', { name: 'dimension Length', exact: true })
|
.getByRole('button', { name: 'dimension Length', exact: true })
|
||||||
.click()
|
.click()
|
||||||
await page.getByText('Add constraining value').click()
|
await expect(cmdBarKclInput).toHaveText('20')
|
||||||
|
await cmdBarKclInput.fill(lengthValue.new)
|
||||||
|
await expect(
|
||||||
|
page.getByText(`Can't calculate`),
|
||||||
|
`Something went wrong with the KCL expression evaluation`
|
||||||
|
).not.toBeVisible()
|
||||||
|
await cmdBarSubmitButton.click()
|
||||||
|
|
||||||
await expect(page.locator('.cm-content')).toHaveText(
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
`length001 = 20sketch001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> angledLine([90, length001], %) |> xLine(-20, %)`
|
`length001 = ${lengthValue.new}sketch001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> angledLine([90, length001], %) |> xLine(-20, %)`
|
||||||
)
|
)
|
||||||
|
|
||||||
// Make sure we didn't pop out of sketch mode.
|
// Make sure we didn't pop out of sketch mode.
|
||||||
@ -66,7 +76,6 @@ test.describe('Testing constraints', () => {
|
|||||||
await page.waitForTimeout(500) // wait for animation
|
await page.waitForTimeout(500) // wait for animation
|
||||||
|
|
||||||
// Exit sketch
|
// Exit sketch
|
||||||
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
|
|
||||||
await page.keyboard.press('Escape')
|
await page.keyboard.press('Escape')
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Exit Sketch' })
|
page.getByRole('button', { name: 'Exit Sketch' })
|
||||||
@ -524,7 +533,7 @@ part002 = startSketchOn('XZ')
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
test.describe('Test Angle/Length constraint single selection', () => {
|
test.describe('Test Angle constraint single selection', () => {
|
||||||
const cases = [
|
const cases = [
|
||||||
{
|
{
|
||||||
testName: 'Angle - Add variable',
|
testName: 'Angle - Add variable',
|
||||||
@ -538,18 +547,6 @@ part002 = startSketchOn('XZ')
|
|||||||
constraint: 'angle',
|
constraint: 'angle',
|
||||||
value: '83, 78.33',
|
value: '83, 78.33',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
testName: 'Length - Add variable',
|
|
||||||
addVariable: true,
|
|
||||||
constraint: 'length',
|
|
||||||
value: '83, length001',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
testName: 'Length - No variable',
|
|
||||||
addVariable: false,
|
|
||||||
constraint: 'length',
|
|
||||||
value: '83, 78.33',
|
|
||||||
},
|
|
||||||
] as const
|
] as const
|
||||||
for (const { testName, addVariable, value, constraint } of cases) {
|
for (const { testName, addVariable, value, constraint } of cases) {
|
||||||
test(`${testName}`, async ({ page }) => {
|
test(`${testName}`, async ({ page }) => {
|
||||||
@ -608,6 +605,90 @@ part002 = startSketchOn('XZ')
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
test.describe('Test Length constraint single selection', () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
testName: 'Length - Add variable',
|
||||||
|
addVariable: true,
|
||||||
|
constraint: 'length',
|
||||||
|
value: '83, length001',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: 'Length - No variable',
|
||||||
|
addVariable: false,
|
||||||
|
constraint: 'length',
|
||||||
|
value: '83, 78.33',
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
for (const { testName, addVariable, value, constraint } of cases) {
|
||||||
|
test(`${testName}`, async ({ page }) => {
|
||||||
|
// constants and locators
|
||||||
|
const cmdBarKclInput = page
|
||||||
|
.getByTestId('cmd-bar-arg-value')
|
||||||
|
.getByRole('textbox')
|
||||||
|
const cmdBarKclVariableNameInput =
|
||||||
|
page.getByPlaceholder('Variable name')
|
||||||
|
const cmdBarSubmitButton = page.getByRole('button', {
|
||||||
|
name: 'arrow right Continue',
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.addInitScript(async () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`yo = 5
|
||||||
|
part001 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([-7.54, -26.74], %)
|
||||||
|
|> line([74.36, 130.4], %)
|
||||||
|
|> line([78.92, -120.11], %)
|
||||||
|
|> line([9.16, 77.79], %)
|
||||||
|
|> line([51.19, 48.97], %)
|
||||||
|
part002 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([299.05, 231.45], %)
|
||||||
|
|> xLine(-425.34, %, $seg_what)
|
||||||
|
|> yLine(-264.06, %)
|
||||||
|
|> xLine(segLen(seg_what), %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
const u = await getUtils(page)
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
await page.getByText('line([74.36, 130.4], %)').click()
|
||||||
|
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||||
|
|
||||||
|
const line3 = await u.getSegmentBodyCoords(
|
||||||
|
`[data-overlay-index="${2}"]`
|
||||||
|
)
|
||||||
|
|
||||||
|
await page.mouse.click(line3.x, line3.y)
|
||||||
|
await page
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'Length: open menu',
|
||||||
|
})
|
||||||
|
.click()
|
||||||
|
await page.getByTestId('dropdown-constraint-' + constraint).click()
|
||||||
|
|
||||||
|
if (!addVariable) {
|
||||||
|
await test.step(`Clear the variable input`, async () => {
|
||||||
|
await cmdBarKclVariableNameInput.clear()
|
||||||
|
await cmdBarKclVariableNameInput.press('Backspace')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await expect(cmdBarKclInput).toHaveText('78.33')
|
||||||
|
await cmdBarSubmitButton.click()
|
||||||
|
|
||||||
|
const changedCode = `|> angledLine([${value}], %)`
|
||||||
|
await expect(page.locator('.cm-content')).toContainText(changedCode)
|
||||||
|
// checking active assures the cursor is where it should be
|
||||||
|
await expect(page.locator('.cm-activeLine')).toHaveText(changedCode)
|
||||||
|
|
||||||
|
// checking the count of the overlays is a good proxy check that the client sketch scene is in a good state
|
||||||
|
await expect(page.getByTestId('segment-overlay')).toHaveCount(4)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
test.describe('Many segments - no modal constraints', () => {
|
test.describe('Many segments - no modal constraints', () => {
|
||||||
const cases = [
|
const cases = [
|
||||||
{
|
{
|
||||||
@ -868,6 +949,15 @@ part002 = startSketchOn('XZ')
|
|||||||
|> line([3.13, -2.4], %)`
|
|> line([3.13, -2.4], %)`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// constants and locators
|
||||||
|
const cmdBarKclInput = page
|
||||||
|
.getByTestId('cmd-bar-arg-value')
|
||||||
|
.getByRole('textbox')
|
||||||
|
const cmdBarSubmitButton = page.getByRole('button', {
|
||||||
|
name: 'arrow right Continue',
|
||||||
|
})
|
||||||
|
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
|
||||||
@ -928,8 +1018,8 @@ part002 = startSketchOn('XZ')
|
|||||||
// await page.getByRole('button', { name: 'length', exact: true }).click()
|
// await page.getByRole('button', { name: 'length', exact: true }).click()
|
||||||
await page.getByTestId('dropdown-constraint-length').click()
|
await page.getByTestId('dropdown-constraint-length').click()
|
||||||
|
|
||||||
await page.getByLabel('length Value').fill('10')
|
await cmdBarKclInput.fill('10')
|
||||||
await page.getByRole('button', { name: 'Add constraining value' }).click()
|
await cmdBarSubmitButton.click()
|
||||||
|
|
||||||
activeLinesContent = await page.locator('.cm-activeLine').all()
|
activeLinesContent = await page.locator('.cm-activeLine').all()
|
||||||
await expect(activeLinesContent[0]).toHaveText(`|> xLine(length001, %)`)
|
await expect(activeLinesContent[0]).toHaveText(`|> xLine(length001, %)`)
|
||||||
|
@ -91,7 +91,14 @@ test.describe('Testing segment overlays', () => {
|
|||||||
await page.getByTestId('constraint-symbol-popover').count()
|
await page.getByTestId('constraint-symbol-popover').count()
|
||||||
).toBeGreaterThan(0)
|
).toBeGreaterThan(0)
|
||||||
await unconstrainedLocator.click()
|
await unconstrainedLocator.click()
|
||||||
await page.getByText('Add variable').click()
|
await expect(
|
||||||
|
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
|
||||||
|
).toBeFocused()
|
||||||
|
await page
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'arrow right Continue',
|
||||||
|
})
|
||||||
|
.click()
|
||||||
await expect(page.locator('.cm-content')).toContainText(expectFinal)
|
await expect(page.locator('.cm-content')).toContainText(expectFinal)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,7 +158,14 @@ test.describe('Testing segment overlays', () => {
|
|||||||
await page.getByTestId('constraint-symbol-popover').count()
|
await page.getByTestId('constraint-symbol-popover').count()
|
||||||
).toBeGreaterThan(0)
|
).toBeGreaterThan(0)
|
||||||
await unconstrainedLocator.click()
|
await unconstrainedLocator.click()
|
||||||
await page.getByText('Add variable').click()
|
await expect(
|
||||||
|
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
|
||||||
|
).toBeFocused()
|
||||||
|
await page
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'arrow right Continue',
|
||||||
|
})
|
||||||
|
.click()
|
||||||
await expect(page.locator('.cm-content')).toContainText(
|
await expect(page.locator('.cm-content')).toContainText(
|
||||||
expectAfterUnconstrained
|
expectAfterUnconstrained
|
||||||
)
|
)
|
||||||
|
@ -1,20 +1,9 @@
|
|||||||
import type { ForgeConfig } from '@electron-forge/shared-types'
|
import type { ForgeConfig } from '@electron-forge/shared-types'
|
||||||
import { MakerSquirrel } from '@electron-forge/maker-squirrel'
|
|
||||||
import { MakerZIP } from '@electron-forge/maker-zip'
|
|
||||||
import { MakerDeb } from '@electron-forge/maker-deb'
|
|
||||||
import { MakerRpm } from '@electron-forge/maker-rpm'
|
|
||||||
import { VitePlugin } from '@electron-forge/plugin-vite'
|
import { VitePlugin } from '@electron-forge/plugin-vite'
|
||||||
import { MakerWix, MakerWixConfig } from '@electron-forge/maker-wix'
|
|
||||||
import { FusesPlugin } from '@electron-forge/plugin-fuses'
|
import { FusesPlugin } from '@electron-forge/plugin-fuses'
|
||||||
import { FuseV1Options, FuseVersion } from '@electron/fuses'
|
import { FuseV1Options, FuseVersion } from '@electron/fuses'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
interface ExtendedMakerWixConfig extends MakerWixConfig {
|
|
||||||
// see https://github.com/electron/forge/issues/3673
|
|
||||||
// this is an undocumented property of electron-wix-msi
|
|
||||||
associateExtensions?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const rootDir = process.cwd()
|
const rootDir = process.cwd()
|
||||||
|
|
||||||
const config: ForgeConfig = {
|
const config: ForgeConfig = {
|
||||||
@ -39,26 +28,7 @@ const config: ForgeConfig = {
|
|||||||
extendInfo: 'Info.plist', // Information for file associations.
|
extendInfo: 'Info.plist', // Information for file associations.
|
||||||
},
|
},
|
||||||
rebuildConfig: {},
|
rebuildConfig: {},
|
||||||
makers: [
|
makers: [],
|
||||||
new MakerSquirrel({
|
|
||||||
setupIcon: path.resolve(rootDir, 'assets', 'icon.ico'),
|
|
||||||
}),
|
|
||||||
new MakerWix({
|
|
||||||
icon: path.resolve(rootDir, 'assets', 'icon.ico'),
|
|
||||||
associateExtensions: 'kcl',
|
|
||||||
} as ExtendedMakerWixConfig),
|
|
||||||
new MakerZIP({}, ['darwin']),
|
|
||||||
new MakerRpm({
|
|
||||||
options: {
|
|
||||||
icon: path.resolve(rootDir, 'assets', 'icon.png'),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
new MakerDeb({
|
|
||||||
options: {
|
|
||||||
icon: path.resolve(rootDir, 'assets', 'icon.png'),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
plugins: [
|
plugins: [
|
||||||
new VitePlugin({
|
new VitePlugin({
|
||||||
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
|
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
|
||||||
|
26
package.json
26
package.json
@ -39,7 +39,6 @@
|
|||||||
"chokidar": "^4.0.1",
|
"chokidar": "^4.0.1",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"decamelize": "^6.0.0",
|
"decamelize": "^6.0.0",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
|
||||||
"electron-updater": "6.3.0",
|
"electron-updater": "6.3.0",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"html2canvas-pro": "^1.5.8",
|
"html2canvas-pro": "^1.5.8",
|
||||||
@ -69,7 +68,7 @@
|
|||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite --port=3000 --host=0.0.0.0",
|
||||||
"start:prod": "vite preview --port=3000",
|
"start:prod": "vite preview --port=3000",
|
||||||
"serve": "vite serve --port=3000",
|
"serve": "vite serve --port=3000",
|
||||||
"build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build",
|
"build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build",
|
||||||
@ -104,8 +103,6 @@
|
|||||||
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
|
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
|
||||||
"tron:start": "electron-forge start",
|
"tron:start": "electron-forge start",
|
||||||
"tron:package": "electron-forge package",
|
"tron:package": "electron-forge package",
|
||||||
"tron:make": "electron-forge make",
|
|
||||||
"tron:publish": "electron-forge publish",
|
|
||||||
"tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron",
|
"tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron",
|
||||||
"tronb:vite": "vite build -c vite.main.config.ts && vite build -c vite.preload.config.ts && vite build -c vite.renderer.config.ts",
|
"tronb:vite": "vite build -c vite.main.config.ts && vite build -c vite.preload.config.ts && vite build -c vite.renderer.config.ts",
|
||||||
"tronb:package": "electron-builder --config electron-builder.yml",
|
"tronb:package": "electron-builder --config electron-builder.yml",
|
||||||
@ -148,17 +145,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-env": "^7.25.4",
|
"@babel/preset-env": "^7.25.4",
|
||||||
"@electron-forge/cli": "^7.4.0",
|
"@electron-forge/cli": "7.4.0",
|
||||||
"@electron-forge/maker-deb": "^7.4.0",
|
"@electron-forge/plugin-fuses": "7.4.0",
|
||||||
"@electron-forge/maker-rpm": "^7.4.0",
|
"@electron-forge/plugin-vite": "7.4.0",
|
||||||
"@electron-forge/maker-squirrel": "^7.4.0",
|
"@electron/fuses": "1.8.0",
|
||||||
"@electron-forge/maker-wix": "^7.5.0",
|
|
||||||
"@electron-forge/maker-zip": "^7.5.0",
|
|
||||||
"@electron-forge/plugin-auto-unpack-natives": "^7.4.0",
|
|
||||||
"@electron-forge/plugin-fuses": "^7.4.0",
|
|
||||||
"@electron-forge/plugin-vite": "^7.4.0",
|
|
||||||
"@electron/fuses": "^1.8.0",
|
|
||||||
"@electron/rebuild": "^3.6.0",
|
|
||||||
"@iarna/toml": "^2.2.5",
|
"@iarna/toml": "^2.2.5",
|
||||||
"@lezer/generator": "^1.7.1",
|
"@lezer/generator": "^1.7.1",
|
||||||
"@nabla/vite-plugin-eslint": "^2.0.5",
|
"@nabla/vite-plugin-eslint": "^2.0.5",
|
||||||
@ -188,9 +178,9 @@
|
|||||||
"@xstate/cli": "^0.5.17",
|
"@xstate/cli": "^0.5.17",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"d3-force": "^3.0.0",
|
"d3-force": "^3.0.0",
|
||||||
"electron": "^32.1.2",
|
"electron": "32.1.2",
|
||||||
"electron-builder": "^24.13.3",
|
"electron-builder": "24.13.3",
|
||||||
"electron-notarize": "^1.2.2",
|
"electron-notarize": "1.2.2",
|
||||||
"eslint": "^8.0.1",
|
"eslint": "^8.0.1",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"eslint-plugin-css-modules": "^2.12.0",
|
"eslint-plugin-css-modules": "^2.12.0",
|
||||||
|
@ -505,7 +505,8 @@ const ConstraintSymbol = ({
|
|||||||
constrainInfo: ConstrainInfo
|
constrainInfo: ConstrainInfo
|
||||||
verticalPosition: 'top' | 'bottom'
|
verticalPosition: 'top' | 'bottom'
|
||||||
}) => {
|
}) => {
|
||||||
const { context, send } = useModelingContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
|
const { context } = useModelingContext()
|
||||||
const varNameMap: {
|
const varNameMap: {
|
||||||
[key in ConstrainInfo['type']]: {
|
[key in ConstrainInfo['type']]: {
|
||||||
varName: string
|
varName: string
|
||||||
@ -624,11 +625,18 @@ const ConstraintSymbol = ({
|
|||||||
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
|
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
|
||||||
onClick={toSync(async () => {
|
onClick={toSync(async () => {
|
||||||
if (!isConstrained) {
|
if (!isConstrained) {
|
||||||
send({
|
commandBarSend({
|
||||||
type: 'Convert to variable',
|
type: 'Find and select command',
|
||||||
data: {
|
data: {
|
||||||
pathToNode,
|
name: 'Constrain with named value',
|
||||||
variableName: varName,
|
groupId: 'modeling',
|
||||||
|
argDefaultValues: {
|
||||||
|
currentValue: {
|
||||||
|
pathToNode,
|
||||||
|
variableName: varName,
|
||||||
|
valueText: value,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else if (isConstrained) {
|
} else if (isConstrained) {
|
||||||
|
@ -8,11 +8,16 @@ import { getSystemTheme } from 'lib/theme'
|
|||||||
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
|
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
|
||||||
import { roundOff } from 'lib/utils'
|
import { roundOff } from 'lib/utils'
|
||||||
import { varMentions } from 'lib/varCompletionExtension'
|
import { varMentions } from 'lib/varCompletionExtension'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import styles from './CommandBarKclInput.module.css'
|
import styles from './CommandBarKclInput.module.css'
|
||||||
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
|
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
|
||||||
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
|
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
|
||||||
|
import { useSelector } from '@xstate/react'
|
||||||
|
|
||||||
|
const machineContextSelector = (snapshot?: {
|
||||||
|
context: Record<string, unknown>
|
||||||
|
}) => snapshot?.context
|
||||||
|
|
||||||
function CommandBarKclInput({
|
function CommandBarKclInput({
|
||||||
arg,
|
arg,
|
||||||
@ -31,12 +36,44 @@ function CommandBarKclInput({
|
|||||||
arg.name
|
arg.name
|
||||||
] as KclCommandValue | undefined
|
] as KclCommandValue | undefined
|
||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const defaultValue = (arg.defaultValue as string) || ''
|
const argMachineContext = useSelector(
|
||||||
|
arg.machineActor,
|
||||||
|
machineContextSelector
|
||||||
|
)
|
||||||
|
const defaultValue = useMemo(
|
||||||
|
() =>
|
||||||
|
arg.defaultValue
|
||||||
|
? arg.defaultValue instanceof Function
|
||||||
|
? arg.defaultValue(commandBarState.context, argMachineContext)
|
||||||
|
: arg.defaultValue
|
||||||
|
: '',
|
||||||
|
[arg.defaultValue, commandBarState.context, argMachineContext]
|
||||||
|
)
|
||||||
|
const initialVariableName = useMemo(() => {
|
||||||
|
// Use the configured variable name if it exists
|
||||||
|
if (arg.variableName !== undefined) {
|
||||||
|
return arg.variableName instanceof Function
|
||||||
|
? arg.variableName(commandBarState.context, argMachineContext)
|
||||||
|
: arg.variableName
|
||||||
|
}
|
||||||
|
// or derive it from the previously set value or the argument name
|
||||||
|
return previouslySetValue && 'variableName' in previouslySetValue
|
||||||
|
? previouslySetValue.variableName
|
||||||
|
: arg.name
|
||||||
|
}, [
|
||||||
|
arg.variableName,
|
||||||
|
commandBarState.context,
|
||||||
|
argMachineContext,
|
||||||
|
arg.name,
|
||||||
|
previouslySetValue,
|
||||||
|
])
|
||||||
const [value, setValue] = useState(
|
const [value, setValue] = useState(
|
||||||
previouslySetValue?.valueText || defaultValue || ''
|
previouslySetValue?.valueText || defaultValue || ''
|
||||||
)
|
)
|
||||||
const [createNewVariable, setCreateNewVariable] = useState(
|
const [createNewVariable, setCreateNewVariable] = useState(
|
||||||
previouslySetValue && 'variableName' in previouslySetValue
|
(previouslySetValue && 'variableName' in previouslySetValue) ||
|
||||||
|
arg.createVariableByDefault ||
|
||||||
|
false
|
||||||
)
|
)
|
||||||
const [canSubmit, setCanSubmit] = useState(true)
|
const [canSubmit, setCanSubmit] = useState(true)
|
||||||
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
||||||
@ -52,10 +89,7 @@ function CommandBarKclInput({
|
|||||||
isNewVariableNameUnique,
|
isNewVariableNameUnique,
|
||||||
} = useCalculateKclExpression({
|
} = useCalculateKclExpression({
|
||||||
value,
|
value,
|
||||||
initialVariableName:
|
initialVariableName,
|
||||||
previouslySetValue && 'variableName' in previouslySetValue
|
|
||||||
? previouslySetValue.variableName
|
|
||||||
: arg.name,
|
|
||||||
})
|
})
|
||||||
const varMentionData: Completion[] = prevVariables.map((v) => ({
|
const varMentionData: Completion[] = prevVariables.map((v) => ({
|
||||||
label: v.key,
|
label: v.key,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { APP_VERSION } from 'routes/Settings'
|
import { APP_VERSION, getReleaseUrl } from 'routes/Settings'
|
||||||
import { CustomIcon } from 'components/CustomIcon'
|
import { CustomIcon } from 'components/CustomIcon'
|
||||||
import Tooltip from 'components/Tooltip'
|
import Tooltip from 'components/Tooltip'
|
||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
@ -72,10 +72,8 @@ export function LowerRightControls({
|
|||||||
<menu className="flex items-center justify-end gap-3 pointer-events-auto">
|
<menu className="flex items-center justify-end gap-3 pointer-events-auto">
|
||||||
{!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />}
|
{!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />}
|
||||||
<a
|
<a
|
||||||
onClick={openExternalBrowserIfDesktop(
|
onClick={openExternalBrowserIfDesktop(getReleaseUrl())}
|
||||||
`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`
|
href={getReleaseUrl()}
|
||||||
)}
|
|
||||||
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`}
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className={'!no-underline font-mono text-xs ' + linkOverrideClassName}
|
className={'!no-underline font-mono text-xs ' + linkOverrideClassName}
|
||||||
|
@ -69,14 +69,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
const [isKclLspReady, setIsKclLspReady] = useState(false)
|
const [isKclLspReady, setIsKclLspReady] = useState(false)
|
||||||
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
|
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
|
||||||
|
|
||||||
const {
|
const { auth } = useSettingsAuthContext()
|
||||||
auth,
|
|
||||||
settings: {
|
|
||||||
context: {
|
|
||||||
modeling: { defaultUnit },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} = useSettingsAuthContext()
|
|
||||||
const token = auth?.context.token
|
const token = auth?.context.token
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
@ -92,7 +85,6 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
const initEvent: KclWorkerOptions = {
|
const initEvent: KclWorkerOptions = {
|
||||||
wasmUrl: wasmUrl(),
|
wasmUrl: wasmUrl(),
|
||||||
token: token,
|
token: token,
|
||||||
baseUnit: defaultUnit.current,
|
|
||||||
apiBaseUrl: VITE_KC_API_BASE_URL,
|
apiBaseUrl: VITE_KC_API_BASE_URL,
|
||||||
}
|
}
|
||||||
lspWorker.postMessage({
|
lspWorker.postMessage({
|
||||||
|
@ -41,7 +41,10 @@ import {
|
|||||||
angleBetweenInfo,
|
angleBetweenInfo,
|
||||||
applyConstraintAngleBetween,
|
applyConstraintAngleBetween,
|
||||||
} from './Toolbar/SetAngleBetween'
|
} from './Toolbar/SetAngleBetween'
|
||||||
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
|
import {
|
||||||
|
applyConstraintAngleLength,
|
||||||
|
applyConstraintLength,
|
||||||
|
} from './Toolbar/setAngleLength'
|
||||||
import {
|
import {
|
||||||
canSweepSelection,
|
canSweepSelection,
|
||||||
handleSelectionBatch,
|
handleSelectionBatch,
|
||||||
@ -51,6 +54,8 @@ import {
|
|||||||
Selections,
|
Selections,
|
||||||
updateSelections,
|
updateSelections,
|
||||||
canLoftSelection,
|
canLoftSelection,
|
||||||
|
canRevolveSelection,
|
||||||
|
canShellSelection,
|
||||||
} from 'lib/selections'
|
} from 'lib/selections'
|
||||||
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
||||||
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
||||||
@ -62,13 +67,15 @@ import {
|
|||||||
getSketchOrientationDetails,
|
getSketchOrientationDetails,
|
||||||
} from 'clientSideScene/sceneEntities'
|
} from 'clientSideScene/sceneEntities'
|
||||||
import {
|
import {
|
||||||
moveValueIntoNewVariablePath,
|
insertNamedConstant,
|
||||||
|
replaceValueAtNodePath,
|
||||||
sketchOnExtrudedFace,
|
sketchOnExtrudedFace,
|
||||||
sketchOnOffsetPlane,
|
sketchOnOffsetPlane,
|
||||||
startSketchOnDefault,
|
startSketchOnDefault,
|
||||||
} from 'lang/modifyAst'
|
} from 'lang/modifyAst'
|
||||||
import { Program, parse, recast, resultIsOk } from 'lang/wasm'
|
import { PathToNode, Program, parse, recast, resultIsOk } from 'lang/wasm'
|
||||||
import {
|
import {
|
||||||
|
doesSceneHaveExtrudedSketch,
|
||||||
doesSceneHaveSweepableSketch,
|
doesSceneHaveSweepableSketch,
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
isSingleCursorInPipe,
|
isSingleCursorInPipe,
|
||||||
@ -79,7 +86,6 @@ import toast from 'react-hot-toast'
|
|||||||
import { EditorSelection, Transaction } from '@codemirror/state'
|
import { EditorSelection, Transaction } from '@codemirror/state'
|
||||||
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
||||||
import { getVarNameModal } from 'hooks/useToolbarGuards'
|
|
||||||
import { err, reportRejection, trap } from 'lib/trap'
|
import { err, reportRejection, trap } from 'lib/trap'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { modelingMachineEvent } from 'editor/manager'
|
import { modelingMachineEvent } from 'editor/manager'
|
||||||
@ -570,6 +576,26 @@ export const ModelingMachineProvider = ({
|
|||||||
if (err(canSweep)) return false
|
if (err(canSweep)) return false
|
||||||
return canSweep
|
return canSweep
|
||||||
},
|
},
|
||||||
|
'has valid revolve selection': ({ context: { selectionRanges } }) => {
|
||||||
|
// A user can begin extruding if they either have 1+ faces selected or nothing selected
|
||||||
|
// TODO: I believe this guard only allows for extruding a single face at a time
|
||||||
|
const hasNoSelection =
|
||||||
|
selectionRanges.graphSelections.length === 0 ||
|
||||||
|
isRangeBetweenCharacters(selectionRanges) ||
|
||||||
|
isSelectionLastLine(selectionRanges, codeManager.code)
|
||||||
|
|
||||||
|
if (hasNoSelection) {
|
||||||
|
// they have no selection, we should enable the button
|
||||||
|
// so they can select the face through the cmdbar
|
||||||
|
// BUT only if there's extrudable geometry
|
||||||
|
return doesSceneHaveSweepableSketch(kclManager.ast)
|
||||||
|
}
|
||||||
|
if (!isSketchPipe(selectionRanges)) return false
|
||||||
|
|
||||||
|
const canSweep = canRevolveSelection(selectionRanges)
|
||||||
|
if (err(canSweep)) return false
|
||||||
|
return canSweep
|
||||||
|
},
|
||||||
'has valid loft selection': ({ context: { selectionRanges } }) => {
|
'has valid loft selection': ({ context: { selectionRanges } }) => {
|
||||||
const hasNoSelection =
|
const hasNoSelection =
|
||||||
selectionRanges.graphSelections.length === 0 ||
|
selectionRanges.graphSelections.length === 0 ||
|
||||||
@ -585,6 +611,24 @@ export const ModelingMachineProvider = ({
|
|||||||
if (err(canLoft)) return false
|
if (err(canLoft)) return false
|
||||||
return canLoft
|
return canLoft
|
||||||
},
|
},
|
||||||
|
'has valid shell selection': ({
|
||||||
|
context: { selectionRanges },
|
||||||
|
event,
|
||||||
|
}) => {
|
||||||
|
const hasNoSelection =
|
||||||
|
selectionRanges.graphSelections.length === 0 ||
|
||||||
|
isRangeBetweenCharacters(selectionRanges) ||
|
||||||
|
isSelectionLastLine(selectionRanges, codeManager.code)
|
||||||
|
|
||||||
|
if (hasNoSelection) {
|
||||||
|
return doesSceneHaveExtrudedSketch(kclManager.ast)
|
||||||
|
}
|
||||||
|
|
||||||
|
const canShell = canShellSelection(selectionRanges)
|
||||||
|
console.log('canShellSelection', canShellSelection(selectionRanges))
|
||||||
|
if (err(canShell)) return false
|
||||||
|
return canShell
|
||||||
|
},
|
||||||
'has valid selection for deletion': ({
|
'has valid selection for deletion': ({
|
||||||
context: { selectionRanges },
|
context: { selectionRanges },
|
||||||
}) => {
|
}) => {
|
||||||
@ -869,12 +913,18 @@ export const ModelingMachineProvider = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
'Get length info': fromPromise(
|
astConstrainLength: fromPromise(
|
||||||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
async ({
|
||||||
const { modifiedAst, pathToNodeMap } =
|
input: { selectionRanges, sketchDetails, lengthValue },
|
||||||
await applyConstraintAngleLength({
|
}) => {
|
||||||
selectionRanges,
|
if (!lengthValue)
|
||||||
})
|
return Promise.reject(new Error('No length value'))
|
||||||
|
const constraintResult = await applyConstraintLength({
|
||||||
|
selectionRanges,
|
||||||
|
length: lengthValue,
|
||||||
|
})
|
||||||
|
if (err(constraintResult)) return Promise.reject(constraintResult)
|
||||||
|
const { modifiedAst, pathToNodeMap } = constraintResult
|
||||||
const pResult = parse(recast(modifiedAst))
|
const pResult = parse(recast(modifiedAst))
|
||||||
if (trap(pResult) || !resultIsOk(pResult))
|
if (trap(pResult) || !resultIsOk(pResult))
|
||||||
return Promise.reject(new Error('Unexpected compilation error'))
|
return Promise.reject(new Error('Unexpected compilation error'))
|
||||||
@ -1043,38 +1093,88 @@ export const ModelingMachineProvider = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
'Get convert to variable info': fromPromise(
|
'Apply named value constraint': fromPromise(
|
||||||
async ({ input: { selectionRanges, sketchDetails, data } }) => {
|
async ({ input: { selectionRanges, sketchDetails, data } }) => {
|
||||||
if (!sketchDetails)
|
if (!sketchDetails) {
|
||||||
return Promise.reject(new Error('No sketch details'))
|
return Promise.reject(new Error('No sketch details'))
|
||||||
const { variableName } = await getVarNameModal({
|
}
|
||||||
valueName: data?.variableName || 'var',
|
if (!data) {
|
||||||
})
|
return Promise.reject(new Error('No data from command flow'))
|
||||||
|
}
|
||||||
let pResult = parse(recast(kclManager.ast))
|
let pResult = parse(recast(kclManager.ast))
|
||||||
if (trap(pResult) || !resultIsOk(pResult))
|
if (trap(pResult) || !resultIsOk(pResult))
|
||||||
return Promise.reject(new Error('Unexpected compilation error'))
|
return Promise.reject(new Error('Unexpected compilation error'))
|
||||||
let parsed = pResult.program
|
let parsed = pResult.program
|
||||||
|
|
||||||
const { modifiedAst: _modifiedAst, pathToReplacedNode } =
|
let result: {
|
||||||
moveValueIntoNewVariablePath(
|
modifiedAst: Node<Program>
|
||||||
parsed,
|
pathToReplaced: PathToNode | null
|
||||||
kclManager.programMemory,
|
} = {
|
||||||
data?.pathToNode || [],
|
modifiedAst: parsed,
|
||||||
variableName
|
pathToReplaced: null,
|
||||||
|
}
|
||||||
|
// If the user provided a constant name,
|
||||||
|
// we need to insert the named constant
|
||||||
|
// and then replace the node with the constant's name.
|
||||||
|
if ('variableName' in data.namedValue) {
|
||||||
|
const astAfterReplacement = replaceValueAtNodePath({
|
||||||
|
ast: parsed,
|
||||||
|
pathToNode: data.currentValue.pathToNode,
|
||||||
|
newExpressionString: data.namedValue.variableName,
|
||||||
|
})
|
||||||
|
if (trap(astAfterReplacement)) {
|
||||||
|
return Promise.reject(astAfterReplacement)
|
||||||
|
}
|
||||||
|
const parseResultAfterInsertion = parse(
|
||||||
|
recast(
|
||||||
|
insertNamedConstant({
|
||||||
|
node: astAfterReplacement.modifiedAst,
|
||||||
|
newExpression: data.namedValue,
|
||||||
|
})
|
||||||
|
)
|
||||||
)
|
)
|
||||||
pResult = parse(recast(_modifiedAst))
|
if (
|
||||||
|
trap(parseResultAfterInsertion) ||
|
||||||
|
!resultIsOk(parseResultAfterInsertion)
|
||||||
|
)
|
||||||
|
return Promise.reject(parseResultAfterInsertion)
|
||||||
|
result = {
|
||||||
|
modifiedAst: parseResultAfterInsertion.program,
|
||||||
|
pathToReplaced: astAfterReplacement.pathToReplaced,
|
||||||
|
}
|
||||||
|
} else if ('valueText' in data.namedValue) {
|
||||||
|
// If they didn't provide a constant name,
|
||||||
|
// just replace the node with the value.
|
||||||
|
const astAfterReplacement = replaceValueAtNodePath({
|
||||||
|
ast: parsed,
|
||||||
|
pathToNode: data.currentValue.pathToNode,
|
||||||
|
newExpressionString: data.namedValue.valueText,
|
||||||
|
})
|
||||||
|
if (trap(astAfterReplacement)) {
|
||||||
|
return Promise.reject(astAfterReplacement)
|
||||||
|
}
|
||||||
|
// The `replacer` function returns a pathToNode that assumes
|
||||||
|
// an identifier is also being inserted into the AST, creating an off-by-one error.
|
||||||
|
// This corrects that error, but TODO we should fix this upstream
|
||||||
|
// to avoid this kind of error in the future.
|
||||||
|
astAfterReplacement.pathToReplaced[1][0] =
|
||||||
|
(astAfterReplacement.pathToReplaced[1][0] as number) - 1
|
||||||
|
result = astAfterReplacement
|
||||||
|
}
|
||||||
|
|
||||||
|
pResult = parse(recast(result.modifiedAst))
|
||||||
if (trap(pResult) || !resultIsOk(pResult))
|
if (trap(pResult) || !resultIsOk(pResult))
|
||||||
return Promise.reject(new Error('Unexpected compilation error'))
|
return Promise.reject(new Error('Unexpected compilation error'))
|
||||||
parsed = pResult.program
|
parsed = pResult.program
|
||||||
|
|
||||||
if (trap(parsed)) return Promise.reject(parsed)
|
if (trap(parsed)) return Promise.reject(parsed)
|
||||||
parsed = parsed as Node<Program>
|
parsed = parsed as Node<Program>
|
||||||
if (!pathToReplacedNode)
|
if (!result.pathToReplaced)
|
||||||
return Promise.reject(new Error('No path to replaced node'))
|
return Promise.reject(new Error('No path to replaced node'))
|
||||||
|
|
||||||
const updatedAst =
|
const updatedAst =
|
||||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||||
pathToReplacedNode || [],
|
result.pathToReplaced || [],
|
||||||
parsed,
|
parsed,
|
||||||
sketchDetails.zAxis,
|
sketchDetails.zAxis,
|
||||||
sketchDetails.yAxis,
|
sketchDetails.yAxis,
|
||||||
@ -1087,7 +1187,7 @@ export const ModelingMachineProvider = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const selection = updateSelections(
|
const selection = updateSelections(
|
||||||
{ 0: pathToReplacedNode },
|
{ 0: result.pathToReplaced },
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
updatedAst.newAst
|
updatedAst.newAst
|
||||||
)
|
)
|
||||||
@ -1095,7 +1195,7 @@ export const ModelingMachineProvider = ({
|
|||||||
return {
|
return {
|
||||||
selectionType: 'completeSelection',
|
selectionType: 'completeSelection',
|
||||||
selection,
|
selection,
|
||||||
updatedPathToNode: pathToReplacedNode,
|
updatedPathToNode: result.pathToReplaced,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
@ -76,7 +76,7 @@ export const ModelingPane = ({
|
|||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
{...props}
|
{...props}
|
||||||
title={title && typeof title === 'string' ? title : ''}
|
aria-label={title && typeof title === 'string' ? title : ''}
|
||||||
data-testid={detailsTestId}
|
data-testid={detailsTestId}
|
||||||
id={id}
|
id={id}
|
||||||
className={
|
className={
|
||||||
|
@ -10,7 +10,7 @@ interface AllKeybindingsFieldsProps {}
|
|||||||
|
|
||||||
export const AllKeybindingsFields = forwardRef(
|
export const AllKeybindingsFields = forwardRef(
|
||||||
(
|
(
|
||||||
props: AllKeybindingsFieldsProps,
|
_props: AllKeybindingsFieldsProps,
|
||||||
scrollRef: ForwardedRef<HTMLDivElement>
|
scrollRef: ForwardedRef<HTMLDivElement>
|
||||||
) => {
|
) => {
|
||||||
// This is how we will get the interaction map from the context
|
// This is how we will get the interaction map from the context
|
||||||
@ -25,7 +25,7 @@ export const AllKeybindingsFields = forwardRef(
|
|||||||
.map(([category, categoryItems]) => (
|
.map(([category, categoryItems]) => (
|
||||||
<div className="flex flex-col gap-4 px-2 pr-4">
|
<div className="flex flex-col gap-4 px-2 pr-4">
|
||||||
<h2
|
<h2
|
||||||
id={`category-${category}`}
|
id={`category-${category.replaceAll(/\s/g, '-')}`}
|
||||||
className="text-xl mt-6 first-of-type:mt-0 capitalize font-bold"
|
className="text-xl mt-6 first-of-type:mt-0 capitalize font-bold"
|
||||||
>
|
>
|
||||||
{category}
|
{category}
|
||||||
|
@ -13,7 +13,7 @@ import { isDesktop } from 'lib/isDesktop'
|
|||||||
import { ActionButton } from 'components/ActionButton'
|
import { ActionButton } from 'components/ActionButton'
|
||||||
import { SettingsFieldInput } from './SettingsFieldInput'
|
import { SettingsFieldInput } from './SettingsFieldInput'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { APP_VERSION, PACKAGE_NAME } from 'routes/Settings'
|
import { APP_VERSION, IS_NIGHTLY, getReleaseUrl } from 'routes/Settings'
|
||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
import {
|
import {
|
||||||
createAndOpenNewTutorialProject,
|
createAndOpenNewTutorialProject,
|
||||||
@ -246,10 +246,8 @@ export const AllSettingsFields = forwardRef(
|
|||||||
to inject the version from package.json */}
|
to inject the version from package.json */}
|
||||||
App version {APP_VERSION}.{' '}
|
App version {APP_VERSION}.{' '}
|
||||||
<a
|
<a
|
||||||
onClick={openExternalBrowserIfDesktop(
|
onClick={openExternalBrowserIfDesktop(getReleaseUrl())}
|
||||||
`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`
|
href={getReleaseUrl()}
|
||||||
)}
|
|
||||||
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`}
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
@ -271,7 +269,7 @@ export const AllSettingsFields = forwardRef(
|
|||||||
, and start a discussion if you don't see it! Your feedback will
|
, and start a discussion if you don't see it! Your feedback will
|
||||||
help us prioritize what to build next.
|
help us prioritize what to build next.
|
||||||
</p>
|
</p>
|
||||||
{PACKAGE_NAME.indexOf('-nightly') === -1 && (
|
{!IS_NIGHTLY && (
|
||||||
<p className="max-w-2xl mt-6">
|
<p className="max-w-2xl mt-6">
|
||||||
Want to experience the latest and (hopefully) greatest from our
|
Want to experience the latest and (hopefully) greatest from our
|
||||||
main development branch?{' '}
|
main development branch?{' '}
|
||||||
|
@ -19,7 +19,7 @@ export function KeybindingsSectionsList({
|
|||||||
key={category}
|
key={category}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
scrollRef.current
|
scrollRef.current
|
||||||
?.querySelector(`#category-${category}`)
|
?.querySelector(`#category-${category.replaceAll(/\s/g, '-')}`)
|
||||||
?.scrollIntoView({
|
?.scrollIntoView({
|
||||||
block: 'center',
|
block: 'center',
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { trap } from 'lib/trap'
|
import { trap } from 'lib/trap'
|
||||||
import { useMachine } from '@xstate/react'
|
import { useMachine, useSelector } from '@xstate/react'
|
||||||
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
|
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
|
||||||
import { PATHS, BROWSER_PATH } from 'lib/paths'
|
import { PATHS, BROWSER_PATH } from 'lib/paths'
|
||||||
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
|
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
|
||||||
@ -23,7 +23,6 @@ import {
|
|||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
sceneEntitiesManager,
|
sceneEntitiesManager,
|
||||||
} from 'lib/singletons'
|
} from 'lib/singletons'
|
||||||
import { uuidv4 } from 'lib/utils'
|
|
||||||
import { IndexLoaderData } from 'lib/types'
|
import { IndexLoaderData } from 'lib/types'
|
||||||
import { settings } from 'lib/settings/initialSettings'
|
import { settings } from 'lib/settings/initialSettings'
|
||||||
import {
|
import {
|
||||||
@ -55,11 +54,15 @@ type SettingsAuthContextType = {
|
|||||||
settings: MachineContext<typeof settingsMachine>
|
settings: MachineContext<typeof settingsMachine>
|
||||||
}
|
}
|
||||||
|
|
||||||
// a little hacky for sure, open to changing it
|
/**
|
||||||
// this implies that we should only even have one instance of this provider mounted at any one time
|
* This variable is used to store the last snapshot of the settings context
|
||||||
// but I think that's a safe assumption
|
* for use outside of React, such as in `wasm.ts`. It is updated every time
|
||||||
let settingsStateRef: ContextFrom<typeof settingsMachine> | undefined
|
* the settings machine changes with `useSelector`.
|
||||||
export const getSettingsState = () => settingsStateRef
|
* TODO: when we decouple XState from React, we can just subscribe to the actor directly from `wasm.ts`
|
||||||
|
*/
|
||||||
|
export let lastSettingsContextSnapshot:
|
||||||
|
| ContextFrom<typeof settingsMachine>
|
||||||
|
| undefined
|
||||||
|
|
||||||
export const SettingsAuthContext = createContext({} as SettingsAuthContextType)
|
export const SettingsAuthContext = createContext({} as SettingsAuthContextType)
|
||||||
|
|
||||||
@ -129,27 +132,11 @@ export const SettingsAuthProviderBase = ({
|
|||||||
.setTheme(context.app.theme.current)
|
.setTheme(context.app.theme.current)
|
||||||
.catch(reportRejection)
|
.catch(reportRejection)
|
||||||
},
|
},
|
||||||
setEngineScaleGridVisibility: ({ context }) => {
|
|
||||||
engineCommandManager.setScaleGridVisibility(
|
|
||||||
context.modeling.showScaleGrid.current
|
|
||||||
)
|
|
||||||
},
|
|
||||||
setClientTheme: ({ context }) => {
|
setClientTheme: ({ context }) => {
|
||||||
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
||||||
sceneInfra.theme = opposingTheme
|
sceneInfra.theme = opposingTheme
|
||||||
sceneEntitiesManager.updateSegmentBaseColor(opposingTheme)
|
sceneEntitiesManager.updateSegmentBaseColor(opposingTheme)
|
||||||
},
|
},
|
||||||
setEngineEdges: ({ context }) => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
engineCommandManager.sendSceneCommand({
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd: {
|
|
||||||
type: 'edge_lines_visible' as any, // TODO update kittycad.ts to get this new command type
|
|
||||||
hidden: !context.modeling.highlightEdges.current,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
toastSuccess: ({ event }) => {
|
toastSuccess: ({ event }) => {
|
||||||
if (!('data' in event)) return
|
if (!('data' in event)) return
|
||||||
const eventParts = event.type.replace(/^set./, '').split('.') as [
|
const eventParts = event.type.replace(/^set./, '').split('.') as [
|
||||||
@ -175,17 +162,27 @@ export const SettingsAuthProviderBase = ({
|
|||||||
},
|
},
|
||||||
'Execute AST': ({ context, event }) => {
|
'Execute AST': ({ context, event }) => {
|
||||||
try {
|
try {
|
||||||
|
const relevantSetting = (s: typeof settings) => {
|
||||||
|
return (
|
||||||
|
s.modeling?.defaultUnit?.current !==
|
||||||
|
context.modeling.defaultUnit.current ||
|
||||||
|
s.modeling.showScaleGrid.current !==
|
||||||
|
context.modeling.showScaleGrid.current ||
|
||||||
|
s.modeling?.highlightEdges.current !==
|
||||||
|
context.modeling.highlightEdges.current
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const allSettingsIncludesUnitChange =
|
const allSettingsIncludesUnitChange =
|
||||||
event.type === 'Set all settings' &&
|
event.type === 'Set all settings' &&
|
||||||
event.settings?.modeling?.defaultUnit?.current !==
|
relevantSetting(event.settings)
|
||||||
context.modeling.defaultUnit.current
|
|
||||||
const resetSettingsIncludesUnitChange =
|
const resetSettingsIncludesUnitChange =
|
||||||
event.type === 'Reset settings' &&
|
event.type === 'Reset settings' && relevantSetting(settings)
|
||||||
context.modeling.defaultUnit.current !==
|
|
||||||
settings?.modeling?.defaultUnit?.default
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
event.type === 'set.modeling.defaultUnit' ||
|
event.type === 'set.modeling.defaultUnit' ||
|
||||||
|
event.type === 'set.modeling.showScaleGrid' ||
|
||||||
|
event.type === 'set.modeling.highlightEdges' ||
|
||||||
allSettingsIncludesUnitChange ||
|
allSettingsIncludesUnitChange ||
|
||||||
resetSettingsIncludesUnitChange
|
resetSettingsIncludesUnitChange
|
||||||
) {
|
) {
|
||||||
@ -214,7 +211,10 @@ export const SettingsAuthProviderBase = ({
|
|||||||
}),
|
}),
|
||||||
{ input: loadedSettings }
|
{ input: loadedSettings }
|
||||||
)
|
)
|
||||||
settingsStateRef = settingsState.context
|
// Any time the actor changes, update the settings state for external use
|
||||||
|
useSelector(settingsActor, (s) => {
|
||||||
|
lastSettingsContextSnapshot = s.context
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDesktop()) return
|
if (!isDesktop()) return
|
||||||
|
@ -2,6 +2,7 @@ import toast from 'react-hot-toast'
|
|||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
import { Marked } from '@ts-stack/markdown'
|
import { Marked } from '@ts-stack/markdown'
|
||||||
|
import { getReleaseUrl } from 'routes/Settings'
|
||||||
|
|
||||||
export function ToastUpdate({
|
export function ToastUpdate({
|
||||||
version,
|
version,
|
||||||
@ -32,10 +33,8 @@ export function ToastUpdate({
|
|||||||
A new update has downloaded and will be available next time you
|
A new update has downloaded and will be available next time you
|
||||||
start the app. You can view the release notes{' '}
|
start the app. You can view the release notes{' '}
|
||||||
<a
|
<a
|
||||||
onClick={openExternalBrowserIfDesktop(
|
onClick={openExternalBrowserIfDesktop(getReleaseUrl(version))}
|
||||||
`https://github.com/KittyCAD/modeling-app/releases/tag/v${version}`
|
href={getReleaseUrl(version)}
|
||||||
)}
|
|
||||||
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${version}`}
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
|
@ -22,6 +22,7 @@ import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
|||||||
import { normaliseAngle } from '../../lib/utils'
|
import { normaliseAngle } from '../../lib/utils'
|
||||||
import { kclManager } from 'lib/singletons'
|
import { kclManager } from 'lib/singletons'
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
|
import { KclCommandValue } from 'lib/commandTypes'
|
||||||
|
|
||||||
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
|
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
|
||||||
|
|
||||||
@ -63,6 +64,57 @@ export function angleLengthInfo({
|
|||||||
return { enabled, transforms }
|
return { enabled, transforms }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function applyConstraintLength({
|
||||||
|
length,
|
||||||
|
selectionRanges,
|
||||||
|
}: {
|
||||||
|
length: KclCommandValue
|
||||||
|
selectionRanges: Selections
|
||||||
|
}) {
|
||||||
|
const ast = kclManager.ast
|
||||||
|
const angleLength = angleLengthInfo({ selectionRanges })
|
||||||
|
if (err(angleLength)) return angleLength
|
||||||
|
const { transforms } = angleLength
|
||||||
|
|
||||||
|
let distanceExpression: Expr = length.valueAst
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To be "constrained", the value must be a binary expression, a named value, or a function call.
|
||||||
|
* If it has a variable name, we need to insert a variable declaration at the correct index.
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
'variableName' in length &&
|
||||||
|
length.variableName &&
|
||||||
|
length.insertIndex !== undefined
|
||||||
|
) {
|
||||||
|
const newBody = [...ast.body]
|
||||||
|
newBody.splice(length.insertIndex, 0, length.variableDeclarationAst)
|
||||||
|
ast.body = newBody
|
||||||
|
distanceExpression = createIdentifier(length.variableName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isExprBinaryPart(distanceExpression)) {
|
||||||
|
return new Error('Invalid valueNode, is not a BinaryPart')
|
||||||
|
}
|
||||||
|
|
||||||
|
const retval = transformAstSketchLines({
|
||||||
|
ast,
|
||||||
|
selectionRanges,
|
||||||
|
transformInfos: transforms,
|
||||||
|
programMemory: kclManager.programMemory,
|
||||||
|
referenceSegName: '',
|
||||||
|
forceValueUsedInTransform: distanceExpression,
|
||||||
|
})
|
||||||
|
if (err(retval)) return Promise.reject(retval)
|
||||||
|
|
||||||
|
const { modifiedAst: _modifiedAst, pathToNodeMap } = retval
|
||||||
|
|
||||||
|
return {
|
||||||
|
modifiedAst: _modifiedAst,
|
||||||
|
pathToNodeMap,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function applyConstraintAngleLength({
|
export async function applyConstraintAngleLength({
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
angleOrLength = 'setLength',
|
angleOrLength = 'setLength',
|
||||||
|
@ -41,7 +41,10 @@ export function UnitsMenu() {
|
|||||||
close()
|
close()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{baseUnitLabels[unit]}
|
<span className="flex-1">{baseUnitLabels[unit]}</span>
|
||||||
|
{unit === settings.context.modeling.defaultUnit.current && (
|
||||||
|
<span className="text-chalkboard-60">current</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { LspWorkerEventType } from '@kittycad/codemirror-lsp-client'
|
import { LspWorkerEventType } from '@kittycad/codemirror-lsp-client'
|
||||||
|
|
||||||
import { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
|
|
||||||
|
|
||||||
export enum LspWorker {
|
export enum LspWorker {
|
||||||
Kcl = 'kcl',
|
Kcl = 'kcl',
|
||||||
Copilot = 'copilot',
|
Copilot = 'copilot',
|
||||||
@ -9,7 +7,6 @@ export enum LspWorker {
|
|||||||
export interface KclWorkerOptions {
|
export interface KclWorkerOptions {
|
||||||
wasmUrl: string
|
wasmUrl: string
|
||||||
token: string
|
token: string
|
||||||
baseUnit: UnitLength
|
|
||||||
apiBaseUrl: string
|
apiBaseUrl: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,6 @@ import {
|
|||||||
KclWorkerOptions,
|
KclWorkerOptions,
|
||||||
CopilotWorkerOptions,
|
CopilotWorkerOptions,
|
||||||
} from 'editor/plugins/lsp/types'
|
} from 'editor/plugins/lsp/types'
|
||||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
|
||||||
import { err, reportRejection } from 'lib/trap'
|
import { err, reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
const intoServer: IntoServer = new IntoServer()
|
const intoServer: IntoServer = new IntoServer()
|
||||||
@ -46,14 +45,12 @@ export async function copilotLspRun(
|
|||||||
|
|
||||||
export async function kclLspRun(
|
export async function kclLspRun(
|
||||||
config: ServerConfig,
|
config: ServerConfig,
|
||||||
engineCommandManager: EngineCommandManager | null,
|
|
||||||
token: string,
|
token: string,
|
||||||
baseUnit: string,
|
|
||||||
baseUrl: string
|
baseUrl: string
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
console.log('start kcl lsp')
|
console.log('start kcl lsp')
|
||||||
await kcl_lsp_run(config, engineCommandManager, baseUnit, token, baseUrl)
|
await kcl_lsp_run(config, null, undefined, token, baseUrl)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.log('kcl lsp failed', e)
|
console.log('kcl lsp failed', e)
|
||||||
// We can't restart here because a moved value, we should do this another way.
|
// We can't restart here because a moved value, we should do this another way.
|
||||||
@ -82,13 +79,7 @@ onmessage = function (event: MessageEvent) {
|
|||||||
switch (worker) {
|
switch (worker) {
|
||||||
case LspWorker.Kcl:
|
case LspWorker.Kcl:
|
||||||
const kclData = eventData as KclWorkerOptions
|
const kclData = eventData as KclWorkerOptions
|
||||||
await kclLspRun(
|
await kclLspRun(config, kclData.token, kclData.apiBaseUrl)
|
||||||
config,
|
|
||||||
null,
|
|
||||||
kclData.token,
|
|
||||||
kclData.baseUnit,
|
|
||||||
kclData.apiBaseUrl
|
|
||||||
)
|
|
||||||
break
|
break
|
||||||
case LspWorker.Copilot:
|
case LspWorker.Copilot:
|
||||||
let copilotData = eventData as CopilotWorkerOptions
|
let copilotData = eventData as CopilotWorkerOptions
|
||||||
|
@ -2,7 +2,7 @@ import { useLayoutEffect, useEffect, useRef } from 'react'
|
|||||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||||
import { deferExecution } from 'lib/utils'
|
import { deferExecution } from 'lib/utils'
|
||||||
import { Themes } from 'lib/theme'
|
import { Themes } from 'lib/theme'
|
||||||
import { makeDefaultPlanes, modifyGrid } from 'lang/wasm'
|
import { makeDefaultPlanes } from 'lang/wasm'
|
||||||
import { useModelingContext } from './useModelingContext'
|
import { useModelingContext } from './useModelingContext'
|
||||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
import { useAppState, useAppStream } from 'AppState'
|
import { useAppState, useAppStream } from 'AppState'
|
||||||
@ -56,9 +56,6 @@ export function useSetupEngineManager(
|
|||||||
makeDefaultPlanes: () => {
|
makeDefaultPlanes: () => {
|
||||||
return makeDefaultPlanes(kclManager.engineCommandManager)
|
return makeDefaultPlanes(kclManager.engineCommandManager)
|
||||||
},
|
},
|
||||||
modifyGrid: (hidden: boolean) => {
|
|
||||||
return modifyGrid(kclManager.engineCommandManager, hidden)
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
hasSetNonZeroDimensions.current = true
|
hasSetNonZeroDimensions.current = true
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,8 @@ export function useConvertToVariable(range?: SourceRange) {
|
|||||||
}, [enable])
|
}, [enable])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Return early if there are no selection ranges for whatever reason
|
||||||
|
if (!context.selectionRanges) return
|
||||||
const parsed = ast
|
const parsed = ast
|
||||||
|
|
||||||
const meta = isNodeSafeToReplace(
|
const meta = isNodeSafeToReplace(
|
||||||
|
@ -317,3 +317,8 @@ code {
|
|||||||
#code-mirror-override .cm-editor {
|
#code-mirror-override .cm-editor {
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Can't use #code-mirror-override here as we're outside of this div */
|
||||||
|
.body-bg .cm-diagnosticAction {
|
||||||
|
@apply bg-primary;
|
||||||
|
}
|
||||||
|
@ -45,6 +45,7 @@ import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
|
|||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import { ExtrudeFacePlane } from 'machines/modelingMachine'
|
import { ExtrudeFacePlane } from 'machines/modelingMachine'
|
||||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
|
import { KclExpressionWithVariable } from 'lib/commandTypes'
|
||||||
|
|
||||||
export function startSketchOnDefault(
|
export function startSketchOnDefault(
|
||||||
node: Node<Program>,
|
node: Node<Program>,
|
||||||
@ -590,6 +591,25 @@ export function addOffsetPlane({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a modified clone of an AST with a named constant inserted into the body
|
||||||
|
*/
|
||||||
|
export function insertNamedConstant({
|
||||||
|
node,
|
||||||
|
newExpression,
|
||||||
|
}: {
|
||||||
|
node: Node<Program>
|
||||||
|
newExpression: KclExpressionWithVariable
|
||||||
|
}): Node<Program> {
|
||||||
|
const ast = structuredClone(node)
|
||||||
|
ast.body.splice(
|
||||||
|
newExpression.insertIndex,
|
||||||
|
0,
|
||||||
|
newExpression.variableDeclarationAst
|
||||||
|
)
|
||||||
|
return ast
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modify the AST to create a new sketch using the variable declaration
|
* Modify the AST to create a new sketch using the variable declaration
|
||||||
* of an offset plane. The new sketch just has to come after the offset
|
* of an offset plane. The new sketch just has to come after the offset
|
||||||
@ -933,6 +953,31 @@ export function giveSketchFnCallTag(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace a
|
||||||
|
*/
|
||||||
|
export function replaceValueAtNodePath({
|
||||||
|
ast,
|
||||||
|
pathToNode,
|
||||||
|
newExpressionString,
|
||||||
|
}: {
|
||||||
|
ast: Node<Program>
|
||||||
|
pathToNode: PathToNode
|
||||||
|
newExpressionString: string
|
||||||
|
}) {
|
||||||
|
const replaceCheckResult = isNodeSafeToReplacePath(ast, pathToNode)
|
||||||
|
if (err(replaceCheckResult)) {
|
||||||
|
return replaceCheckResult
|
||||||
|
}
|
||||||
|
const { isSafe, value, replacer } = replaceCheckResult
|
||||||
|
|
||||||
|
if (!isSafe || value.type === 'Identifier') {
|
||||||
|
return new Error('Not safe to replace')
|
||||||
|
}
|
||||||
|
|
||||||
|
return replacer(ast, newExpressionString)
|
||||||
|
}
|
||||||
|
|
||||||
export function moveValueIntoNewVariablePath(
|
export function moveValueIntoNewVariablePath(
|
||||||
ast: Node<Program>,
|
ast: Node<Program>,
|
||||||
programMemory: ProgramMemory,
|
programMemory: ProgramMemory,
|
||||||
|
@ -22,7 +22,7 @@ import {
|
|||||||
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
|
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
|
||||||
import { createLiteral } from 'lang/modifyAst'
|
import { createLiteral } from 'lang/modifyAst'
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selection, Selections } from 'lib/selections'
|
||||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||||
import { VITE_KC_DEV_TOKEN } from 'env'
|
import { VITE_KC_DEV_TOKEN } from 'env'
|
||||||
import { isOverlap } from 'lib/utils'
|
import { isOverlap } from 'lib/utils'
|
||||||
@ -40,7 +40,6 @@ beforeAll(async () => {
|
|||||||
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
|
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
|
||||||
setMediaStream: () => {},
|
setMediaStream: () => {},
|
||||||
setIsStreamReady: () => {},
|
setIsStreamReady: () => {},
|
||||||
modifyGrid: async () => {},
|
|
||||||
callbackOnEngineLiteConnect: () => {
|
callbackOnEngineLiteConnect: () => {
|
||||||
resolve(true)
|
resolve(true)
|
||||||
},
|
},
|
||||||
@ -118,13 +117,8 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
|
|||||||
code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length,
|
code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length,
|
||||||
true,
|
true,
|
||||||
]
|
]
|
||||||
const selection: Selections = {
|
const selection: Selection = {
|
||||||
graphSelections: [
|
codeRef: codeRefFromRange(segmentRange, ast),
|
||||||
{
|
|
||||||
codeRef: codeRefFromRange(segmentRange, ast),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
otherSelections: [],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// executeAst and artifactGraph
|
// executeAst and artifactGraph
|
||||||
|
@ -29,7 +29,7 @@ import {
|
|||||||
sketchLineHelperMap,
|
sketchLineHelperMap,
|
||||||
} from '../std/sketch'
|
} from '../std/sketch'
|
||||||
import { err, trap } from 'lib/trap'
|
import { err, trap } from 'lib/trap'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selection, Selections } from 'lib/selections'
|
||||||
import { KclCommandValue } from 'lib/commandTypes'
|
import { KclCommandValue } from 'lib/commandTypes'
|
||||||
import {
|
import {
|
||||||
Artifact,
|
Artifact,
|
||||||
@ -99,14 +99,9 @@ export function modifyAstWithEdgeTreatmentAndTag(
|
|||||||
const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison
|
const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison
|
||||||
|
|
||||||
for (const selection of selections.graphSelections) {
|
for (const selection of selections.graphSelections) {
|
||||||
const singleSelection = {
|
|
||||||
graphSelections: [selection],
|
|
||||||
otherSelections: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = getPathToExtrudeForSegmentSelection(
|
const result = getPathToExtrudeForSegmentSelection(
|
||||||
clonedAstForGetExtrude,
|
clonedAstForGetExtrude,
|
||||||
singleSelection,
|
selection,
|
||||||
artifactGraph
|
artifactGraph
|
||||||
)
|
)
|
||||||
if (err(result)) return result
|
if (err(result)) return result
|
||||||
@ -259,12 +254,12 @@ function insertParametersIntoAst(
|
|||||||
|
|
||||||
export function getPathToExtrudeForSegmentSelection(
|
export function getPathToExtrudeForSegmentSelection(
|
||||||
ast: Program,
|
ast: Program,
|
||||||
selection: Selections,
|
selection: Selection,
|
||||||
artifactGraph: ArtifactGraph
|
artifactGraph: ArtifactGraph
|
||||||
): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error {
|
): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error {
|
||||||
const pathToSegmentNode = getNodePathFromSourceRange(
|
const pathToSegmentNode = getNodePathFromSourceRange(
|
||||||
ast,
|
ast,
|
||||||
selection.graphSelections[0]?.codeRef?.range
|
selection.codeRef?.range
|
||||||
)
|
)
|
||||||
|
|
||||||
const varDecNode = getNodeFromPath<VariableDeclaration>(
|
const varDecNode = getNodeFromPath<VariableDeclaration>(
|
||||||
@ -308,7 +303,7 @@ async function updateAstAndFocus(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mutateAstWithTagForSketchSegment(
|
export function mutateAstWithTagForSketchSegment(
|
||||||
astClone: Node<Program>,
|
astClone: Node<Program>,
|
||||||
pathToSegmentNode: PathToNode
|
pathToSegmentNode: PathToNode
|
||||||
): { modifiedAst: Program; tag: string } | Error {
|
): { modifiedAst: Program; tag: string } | Error {
|
||||||
@ -340,7 +335,7 @@ function mutateAstWithTagForSketchSegment(
|
|||||||
return { modifiedAst: astClone, tag }
|
return { modifiedAst: astClone, tag }
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEdgeTagCall(
|
export function getEdgeTagCall(
|
||||||
tag: string,
|
tag: string,
|
||||||
artifact: Artifact
|
artifact: Artifact
|
||||||
): Node<Identifier | CallExpression> {
|
): Node<Identifier | CallExpression> {
|
||||||
|
154
src/lang/modifyAst/addRevolve.ts
Normal file
154
src/lang/modifyAst/addRevolve.ts
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import { err } from 'lib/trap'
|
||||||
|
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
|
||||||
|
import {
|
||||||
|
Program,
|
||||||
|
PathToNode,
|
||||||
|
Expr,
|
||||||
|
CallExpression,
|
||||||
|
PipeExpression,
|
||||||
|
VariableDeclarator,
|
||||||
|
} from 'lang/wasm'
|
||||||
|
import { Selections } from 'lib/selections'
|
||||||
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
|
import {
|
||||||
|
createLiteral,
|
||||||
|
createCallExpressionStdLib,
|
||||||
|
createObjectExpression,
|
||||||
|
createIdentifier,
|
||||||
|
createPipeExpression,
|
||||||
|
findUniqueName,
|
||||||
|
createVariableDeclaration,
|
||||||
|
} from 'lang/modifyAst'
|
||||||
|
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||||
|
import {
|
||||||
|
mutateAstWithTagForSketchSegment,
|
||||||
|
getEdgeTagCall,
|
||||||
|
} from 'lang/modifyAst/addEdgeTreatment'
|
||||||
|
export function revolveSketch(
|
||||||
|
ast: Node<Program>,
|
||||||
|
pathToSketchNode: PathToNode,
|
||||||
|
shouldPipe = false,
|
||||||
|
angle: Expr = createLiteral(4),
|
||||||
|
axis: Selections
|
||||||
|
):
|
||||||
|
| {
|
||||||
|
modifiedAst: Node<Program>
|
||||||
|
pathToSketchNode: PathToNode
|
||||||
|
pathToRevolveArg: PathToNode
|
||||||
|
}
|
||||||
|
| Error {
|
||||||
|
const clonedAst = structuredClone(ast)
|
||||||
|
const sketchNode = getNodeFromPath(clonedAst, pathToSketchNode)
|
||||||
|
if (err(sketchNode)) return sketchNode
|
||||||
|
|
||||||
|
// testing code
|
||||||
|
const pathToAxisSelection = getNodePathFromSourceRange(
|
||||||
|
clonedAst,
|
||||||
|
axis.graphSelections[0]?.codeRef.range
|
||||||
|
)
|
||||||
|
|
||||||
|
const lineNode = getNodeFromPath<CallExpression>(
|
||||||
|
clonedAst,
|
||||||
|
pathToAxisSelection,
|
||||||
|
'CallExpression'
|
||||||
|
)
|
||||||
|
if (err(lineNode)) return lineNode
|
||||||
|
|
||||||
|
// TODO Kevin: What if |> close(%)?
|
||||||
|
// TODO Kevin: What if opposite edge
|
||||||
|
// TODO Kevin: What if the edge isn't planar to the sketch?
|
||||||
|
// TODO Kevin: add a tag.
|
||||||
|
const tagResult = mutateAstWithTagForSketchSegment(
|
||||||
|
clonedAst,
|
||||||
|
pathToAxisSelection
|
||||||
|
)
|
||||||
|
|
||||||
|
// Have the tag whether it is already created or a new one is generated
|
||||||
|
if (err(tagResult)) return tagResult
|
||||||
|
const { tag } = tagResult
|
||||||
|
|
||||||
|
/* Original Code */
|
||||||
|
const { node: sketchExpression } = sketchNode
|
||||||
|
|
||||||
|
// determine if sketchExpression is in a pipeExpression or not
|
||||||
|
const sketchPipeExpressionNode = getNodeFromPath<PipeExpression>(
|
||||||
|
clonedAst,
|
||||||
|
pathToSketchNode,
|
||||||
|
'PipeExpression'
|
||||||
|
)
|
||||||
|
if (err(sketchPipeExpressionNode)) return sketchPipeExpressionNode
|
||||||
|
const { node: sketchPipeExpression } = sketchPipeExpressionNode
|
||||||
|
const isInPipeExpression = sketchPipeExpression.type === 'PipeExpression'
|
||||||
|
|
||||||
|
const sketchVariableDeclaratorNode = getNodeFromPath<VariableDeclarator>(
|
||||||
|
clonedAst,
|
||||||
|
pathToSketchNode,
|
||||||
|
'VariableDeclarator'
|
||||||
|
)
|
||||||
|
if (err(sketchVariableDeclaratorNode)) return sketchVariableDeclaratorNode
|
||||||
|
const {
|
||||||
|
node: sketchVariableDeclarator,
|
||||||
|
shallowPath: sketchPathToDecleration,
|
||||||
|
} = sketchVariableDeclaratorNode
|
||||||
|
|
||||||
|
const axisSelection = axis?.graphSelections[0]?.artifact
|
||||||
|
|
||||||
|
if (!axisSelection) return new Error('Axis selection is missing.')
|
||||||
|
|
||||||
|
const revolveCall = createCallExpressionStdLib('revolve', [
|
||||||
|
createObjectExpression({
|
||||||
|
angle: angle,
|
||||||
|
axis: getEdgeTagCall(tag, axisSelection),
|
||||||
|
}),
|
||||||
|
createIdentifier(sketchVariableDeclarator.id.name),
|
||||||
|
])
|
||||||
|
|
||||||
|
if (shouldPipe) {
|
||||||
|
const pipeChain = createPipeExpression(
|
||||||
|
isInPipeExpression
|
||||||
|
? [...sketchPipeExpression.body, revolveCall]
|
||||||
|
: [sketchExpression as any, revolveCall]
|
||||||
|
)
|
||||||
|
|
||||||
|
sketchVariableDeclarator.init = pipeChain
|
||||||
|
const pathToRevolveArg: PathToNode = [
|
||||||
|
...sketchPathToDecleration,
|
||||||
|
['init', 'VariableDeclarator'],
|
||||||
|
['body', ''],
|
||||||
|
[pipeChain.body.length - 1, 'index'],
|
||||||
|
['arguments', 'CallExpression'],
|
||||||
|
[0, 'index'],
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
modifiedAst: clonedAst,
|
||||||
|
pathToSketchNode,
|
||||||
|
pathToRevolveArg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're not creating a pipe expression,
|
||||||
|
// but rather a separate constant for the extrusion
|
||||||
|
const name = findUniqueName(clonedAst, KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE)
|
||||||
|
const VariableDeclaration = createVariableDeclaration(name, revolveCall)
|
||||||
|
const sketchIndexInPathToNode =
|
||||||
|
sketchPathToDecleration.findIndex((a) => a[0] === 'body') + 1
|
||||||
|
const sketchIndexInBody = sketchPathToDecleration[sketchIndexInPathToNode][0]
|
||||||
|
if (typeof sketchIndexInBody !== 'number')
|
||||||
|
return new Error('expected sketchIndexInBody to be a number')
|
||||||
|
clonedAst.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)
|
||||||
|
|
||||||
|
const pathToRevolveArg: PathToNode = [
|
||||||
|
['body', ''],
|
||||||
|
[sketchIndexInBody + 1, 'index'],
|
||||||
|
['declaration', 'VariableDeclaration'],
|
||||||
|
['init', 'VariableDeclarator'],
|
||||||
|
['arguments', 'CallExpression'],
|
||||||
|
[0, 'index'],
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
modifiedAst: clonedAst,
|
||||||
|
pathToSketchNode: [...pathToSketchNode.slice(0, -1), [-1, 'index']],
|
||||||
|
pathToRevolveArg,
|
||||||
|
}
|
||||||
|
}
|
123
src/lang/modifyAst/addShell.ts
Normal file
123
src/lang/modifyAst/addShell.ts
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
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 {
|
||||||
|
getPathToExtrudeForSegmentSelection,
|
||||||
|
mutateAstWithTagForSketchSegment,
|
||||||
|
} from './addEdgeTreatment'
|
||||||
|
import { getNodeFromPath } from 'lang/queryAst'
|
||||||
|
import { err } from 'lib/trap'
|
||||||
|
import {
|
||||||
|
createLiteral,
|
||||||
|
createIdentifier,
|
||||||
|
findUniqueName,
|
||||||
|
createCallExpressionStdLib,
|
||||||
|
createObjectExpression,
|
||||||
|
createArrayExpression,
|
||||||
|
createVariableDeclaration,
|
||||||
|
} from 'lang/modifyAst'
|
||||||
|
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
|
||||||
|
|
||||||
|
export function addShell({
|
||||||
|
node,
|
||||||
|
selection,
|
||||||
|
artifactGraph,
|
||||||
|
thickness,
|
||||||
|
}: {
|
||||||
|
node: Node<Program>
|
||||||
|
selection: Selections
|
||||||
|
artifactGraph: ArtifactGraph
|
||||||
|
thickness: Expr
|
||||||
|
}): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } {
|
||||||
|
const modifiedAst = structuredClone(node)
|
||||||
|
|
||||||
|
// Look up the corresponding extrude
|
||||||
|
const clonedAstForGetExtrude = structuredClone(modifiedAst)
|
||||||
|
|
||||||
|
const expressions: Expr[] = []
|
||||||
|
let pathToExtrudeNode: PathToNode | undefined = undefined
|
||||||
|
for (const graphSelection of selection.graphSelections) {
|
||||||
|
const extrudeLookupResult = getPathToExtrudeForSegmentSelection(
|
||||||
|
clonedAstForGetExtrude,
|
||||||
|
graphSelection,
|
||||||
|
artifactGraph
|
||||||
|
)
|
||||||
|
if (err(extrudeLookupResult)) {
|
||||||
|
return new Error("Couldn't find extrude")
|
||||||
|
}
|
||||||
|
|
||||||
|
pathToExtrudeNode = extrudeLookupResult.pathToExtrudeNode
|
||||||
|
// Get the sketch ref from the selection
|
||||||
|
// TODO: this assumes the segment is piped directly from the sketch, with no intermediate `VariableDeclarator` between.
|
||||||
|
// We must find a technique for these situations that is robust to intermediate declarations
|
||||||
|
const sketchNode = getNodeFromPath<VariableDeclarator>(
|
||||||
|
modifiedAst,
|
||||||
|
graphSelection.codeRef.pathToNode,
|
||||||
|
'VariableDeclarator'
|
||||||
|
)
|
||||||
|
if (err(sketchNode)) {
|
||||||
|
return sketchNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedArtifact = graphSelection.artifact
|
||||||
|
if (!selectedArtifact) {
|
||||||
|
return new Error('Bad artifact')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check on the selection, and handle the wall vs cap casees
|
||||||
|
let expr: Expr
|
||||||
|
if (selectedArtifact.type === 'cap') {
|
||||||
|
expr = createLiteral(selectedArtifact.subType)
|
||||||
|
} else if (selectedArtifact.type === 'wall') {
|
||||||
|
const tagResult = mutateAstWithTagForSketchSegment(
|
||||||
|
modifiedAst,
|
||||||
|
extrudeLookupResult.pathToSegmentNode
|
||||||
|
)
|
||||||
|
if (err(tagResult)) return tagResult
|
||||||
|
const { tag } = tagResult
|
||||||
|
expr = createIdentifier(tag)
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
expressions.push(expr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pathToExtrudeNode) return new Error('No extrude found')
|
||||||
|
|
||||||
|
const extrudeNode = getNodeFromPath<VariableDeclarator>(
|
||||||
|
modifiedAst,
|
||||||
|
pathToExtrudeNode,
|
||||||
|
'VariableDeclarator'
|
||||||
|
)
|
||||||
|
if (err(extrudeNode)) {
|
||||||
|
return extrudeNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SHELL)
|
||||||
|
const shell = createCallExpressionStdLib('shell', [
|
||||||
|
createObjectExpression({
|
||||||
|
faces: createArrayExpression(expressions),
|
||||||
|
thickness,
|
||||||
|
}),
|
||||||
|
createIdentifier(extrudeNode.node.id.name),
|
||||||
|
])
|
||||||
|
const declaration = createVariableDeclaration(name, shell)
|
||||||
|
|
||||||
|
// TODO: check if we should append at the end like here or right after the extrude
|
||||||
|
modifiedAst.body.push(declaration)
|
||||||
|
const pathToNode: PathToNode = [
|
||||||
|
['body', ''],
|
||||||
|
[modifiedAst.body.length - 1, 'index'],
|
||||||
|
['declaration', 'VariableDeclaration'],
|
||||||
|
['init', 'VariableDeclarator'],
|
||||||
|
['arguments', 'CallExpression'],
|
||||||
|
[0, 'index'],
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
modifiedAst,
|
||||||
|
pathToNode,
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,7 @@ import {
|
|||||||
doesSceneHaveSweepableSketch,
|
doesSceneHaveSweepableSketch,
|
||||||
traverse,
|
traverse,
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
|
doesSceneHaveExtrudedSketch,
|
||||||
} from './queryAst'
|
} from './queryAst'
|
||||||
import { enginelessExecutor } from '../lib/testHelpers'
|
import { enginelessExecutor } from '../lib/testHelpers'
|
||||||
import {
|
import {
|
||||||
@ -654,6 +655,38 @@ extrude001 = extrude(10, sketch001)
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Testing doesSceneHaveExtrudedSketch', () => {
|
||||||
|
it('finds extruded sketch as variable', async () => {
|
||||||
|
const exampleCode = `sketch001 = startSketchOn('XZ')
|
||||||
|
|> circle({ center = [0, 0], radius = 1 }, %)
|
||||||
|
extrude001 = extrude(1, sketch001)
|
||||||
|
`
|
||||||
|
const ast = assertParse(exampleCode)
|
||||||
|
if (err(ast)) throw ast
|
||||||
|
const extrudable = doesSceneHaveExtrudedSketch(ast)
|
||||||
|
expect(extrudable).toBeTruthy()
|
||||||
|
})
|
||||||
|
it('finds extruded sketch in pipe', async () => {
|
||||||
|
const exampleCode = `extrude001 = startSketchOn('XZ')
|
||||||
|
|> circle({ center = [0, 0], radius = 1 }, %)
|
||||||
|
|> extrude(1, %)
|
||||||
|
`
|
||||||
|
const ast = assertParse(exampleCode)
|
||||||
|
if (err(ast)) throw ast
|
||||||
|
const extrudable = doesSceneHaveExtrudedSketch(ast)
|
||||||
|
expect(extrudable).toBeTruthy()
|
||||||
|
})
|
||||||
|
it('finds no extrusion with sketch only', async () => {
|
||||||
|
const exampleCode = `extrude001 = startSketchOn('XZ')
|
||||||
|
|> circle({ center = [0, 0], radius = 1 }, %)
|
||||||
|
`
|
||||||
|
const ast = assertParse(exampleCode)
|
||||||
|
if (err(ast)) throw ast
|
||||||
|
const extrudable = doesSceneHaveExtrudedSketch(ast)
|
||||||
|
expect(extrudable).toBeFalsy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('Testing traverse and pathToNode', () => {
|
describe('Testing traverse and pathToNode', () => {
|
||||||
it.each([
|
it.each([
|
||||||
['basic', '2.73'],
|
['basic', '2.73'],
|
||||||
|
@ -1064,6 +1064,35 @@ export function doesSceneHaveSweepableSketch(ast: Node<Program>, count = 1) {
|
|||||||
return Object.keys(theMap).length >= count
|
return Object.keys(theMap).length >= count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function doesSceneHaveExtrudedSketch(ast: Node<Program>) {
|
||||||
|
const theMap: any = {}
|
||||||
|
traverse(ast as any, {
|
||||||
|
enter(node) {
|
||||||
|
if (
|
||||||
|
node.type === 'VariableDeclarator' &&
|
||||||
|
node.init?.type === 'PipeExpression'
|
||||||
|
) {
|
||||||
|
for (const pipe of node.init.body) {
|
||||||
|
if (
|
||||||
|
pipe.type === 'CallExpression' &&
|
||||||
|
pipe.callee.name === 'extrude'
|
||||||
|
) {
|
||||||
|
theMap[node.id.name] = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
node.type === 'CallExpression' &&
|
||||||
|
node.callee.name === 'extrude' &&
|
||||||
|
node.arguments[1]?.type === 'Identifier'
|
||||||
|
) {
|
||||||
|
theMap[node.moduleId] = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return Object.keys(theMap).length > 0
|
||||||
|
}
|
||||||
|
|
||||||
export function getObjExprProperty(
|
export function getObjExprProperty(
|
||||||
node: ObjectExpression,
|
node: ObjectExpression,
|
||||||
propName: string
|
propName: string
|
||||||
|
@ -139,7 +139,6 @@ beforeAll(async () => {
|
|||||||
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
|
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
|
||||||
setMediaStream: () => {},
|
setMediaStream: () => {},
|
||||||
setIsStreamReady: () => {},
|
setIsStreamReady: () => {},
|
||||||
modifyGrid: async () => {},
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
callbackOnEngineLiteConnect: async () => {
|
callbackOnEngineLiteConnect: async () => {
|
||||||
const cacheEntries = Object.entries(codeToWriteCacheFor) as [
|
const cacheEntries = Object.entries(codeToWriteCacheFor) as [
|
||||||
|
@ -871,3 +871,15 @@ export function codeRefFromRange(range: SourceRange, ast: Program): CodeRef {
|
|||||||
pathToNode: getNodePathFromSourceRange(ast, range),
|
pathToNode: getNodePathFromSourceRange(ast, range),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isSolid2D(artifact: Artifact): artifact is solid2D {
|
||||||
|
return (artifact as solid2D).pathId !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSegment(artifact: Artifact): artifact is SegmentArtifact {
|
||||||
|
return (artifact as SegmentArtifact).pathId !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSweep(artifact: Artifact): artifact is SweepArtifact {
|
||||||
|
return (artifact as SweepArtifact).pathId !== undefined
|
||||||
|
}
|
||||||
|
@ -1399,7 +1399,6 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null
|
private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null
|
||||||
private modifyGrid: (hidden: boolean) => Promise<void> | null = () => null
|
|
||||||
|
|
||||||
private onEngineConnectionOpened = () => {}
|
private onEngineConnectionOpened = () => {}
|
||||||
private onEngineConnectionClosed = () => {}
|
private onEngineConnectionClosed = () => {}
|
||||||
@ -1432,7 +1431,6 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
height,
|
height,
|
||||||
token,
|
token,
|
||||||
makeDefaultPlanes,
|
makeDefaultPlanes,
|
||||||
modifyGrid,
|
|
||||||
settings = {
|
settings = {
|
||||||
pool: null,
|
pool: null,
|
||||||
theme: Themes.Dark,
|
theme: Themes.Dark,
|
||||||
@ -1452,14 +1450,12 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
height: number
|
height: number
|
||||||
token?: string
|
token?: string
|
||||||
makeDefaultPlanes: () => Promise<DefaultPlanes>
|
makeDefaultPlanes: () => Promise<DefaultPlanes>
|
||||||
modifyGrid: (hidden: boolean) => Promise<void>
|
|
||||||
settings?: SettingsViaQueryString
|
settings?: SettingsViaQueryString
|
||||||
}) {
|
}) {
|
||||||
if (settings) {
|
if (settings) {
|
||||||
this.settings = settings
|
this.settings = settings
|
||||||
}
|
}
|
||||||
this.makeDefaultPlanes = makeDefaultPlanes
|
this.makeDefaultPlanes = makeDefaultPlanes
|
||||||
this.modifyGrid = modifyGrid
|
|
||||||
if (width === 0 || height === 0) {
|
if (width === 0 || height === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1539,21 +1535,15 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
type: 'default_camera_get_settings',
|
type: 'default_camera_get_settings',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
// We want modify the grid first because we don't want it to flash.
|
await this.initPlanes()
|
||||||
// Ideally these would already be default hidden in engine (TODO do
|
setIsStreamReady(true)
|
||||||
// that) https://github.com/KittyCAD/engine/issues/2282
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.modifyGrid(!this.settings.showScaleGrid)?.then(async () => {
|
|
||||||
await this.initPlanes()
|
|
||||||
setIsStreamReady(true)
|
|
||||||
|
|
||||||
// Other parts of the application should use this to react on scene ready.
|
// Other parts of the application should use this to react on scene ready.
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent(EngineCommandManagerEvents.SceneReady, {
|
new CustomEvent(EngineCommandManagerEvents.SceneReady, {
|
||||||
detail: this.engineConnection,
|
detail: this.engineConnection,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.engineConnection.addEventListener(
|
this.engineConnection.addEventListener(
|
||||||
@ -2212,15 +2202,6 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
}).catch(reportRejection)
|
}).catch(reportRejection)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the visibility of the scale grid in the engine scene.
|
|
||||||
* @param visible - whether to show or hide the scale grid
|
|
||||||
*/
|
|
||||||
setScaleGridVisibility(visible: boolean) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.modifyGrid(!visible)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some "objects" have the same source range, such as sketch_mode_start and start_path.
|
// Some "objects" have the same source range, such as sketch_mode_start and start_path.
|
||||||
// So when passing a range, we need to also specify the command type
|
// So when passing a range, we need to also specify the command type
|
||||||
mapRangeToObjectId(
|
mapRangeToObjectId(
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
import { parse, ParseResult } from './wasm'
|
import { initPromise, parse, ParseResult } from './wasm'
|
||||||
import { enginelessExecutor } from 'lib/testHelpers'
|
import { enginelessExecutor } from 'lib/testHelpers'
|
||||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
import { Program } from '../wasm-lib/kcl/bindings/Program'
|
import { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await initPromise
|
||||||
|
})
|
||||||
|
|
||||||
it('can execute parsed AST', async () => {
|
it('can execute parsed AST', async () => {
|
||||||
const code = `x = 1
|
const code = `x = 1
|
||||||
// A comment.`
|
// A comment.`
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import init, {
|
import init, {
|
||||||
parse_wasm,
|
parse_wasm,
|
||||||
recast_wasm,
|
recast_wasm,
|
||||||
execute_wasm,
|
execute,
|
||||||
kcl_lint,
|
kcl_lint,
|
||||||
modify_ast_for_sketch_wasm,
|
modify_ast_for_sketch_wasm,
|
||||||
is_points_ccw,
|
is_points_ccw,
|
||||||
get_tangential_arc_to_info,
|
get_tangential_arc_to_info,
|
||||||
program_memory_init,
|
program_memory_init,
|
||||||
make_default_planes,
|
make_default_planes,
|
||||||
modify_grid,
|
|
||||||
coredump,
|
coredump,
|
||||||
toml_stringify,
|
toml_stringify,
|
||||||
default_app_settings,
|
default_app_settings,
|
||||||
@ -43,7 +42,9 @@ import { Environment } from '../wasm-lib/kcl/bindings/Environment'
|
|||||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError'
|
import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError'
|
||||||
import { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
|
import { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
|
||||||
|
import { getAllCurrentSettings } from 'lib/settings/settingsUtils'
|
||||||
|
|
||||||
|
export type { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||||
export type { Program } from '../wasm-lib/kcl/bindings/Program'
|
export type { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||||
export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
|
export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
|
||||||
export type { ObjectExpression } from '../wasm-lib/kcl/bindings/ObjectExpression'
|
export type { ObjectExpression } from '../wasm-lib/kcl/bindings/ObjectExpression'
|
||||||
@ -92,12 +93,26 @@ export type { Solid } from '../wasm-lib/kcl/bindings/Solid'
|
|||||||
export type { KclValue } from '../wasm-lib/kcl/bindings/KclValue'
|
export type { KclValue } from '../wasm-lib/kcl/bindings/KclValue'
|
||||||
export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'
|
export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The first two items are the start and end points (byte offsets from the start of the file).
|
||||||
|
* The third item is whether the source range belongs to the 'main' file, i.e., the file currently
|
||||||
|
* being rendered/displayed in the editor (TODO we need to handle modules better in the frontend).
|
||||||
|
*/
|
||||||
export type SourceRange = [number, number, boolean]
|
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 {
|
export function sourceRangeFromRust(s: RustSourceRange): SourceRange {
|
||||||
return [s[0], s[1], s[2] === 0]
|
return [s[0], s[1], s[2] === 0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a default SourceRange for testing or as a placeholder.
|
||||||
|
*/
|
||||||
export function defaultSourceRange(): SourceRange {
|
export function defaultSourceRange(): SourceRange {
|
||||||
return [0, 0, true]
|
return [0, 0, true]
|
||||||
}
|
}
|
||||||
@ -122,7 +137,7 @@ const initialise = async () => {
|
|||||||
const fullUrl = wasmUrl()
|
const fullUrl = wasmUrl()
|
||||||
const input = await fetch(fullUrl)
|
const input = await fetch(fullUrl)
|
||||||
const buffer = await input.arrayBuffer()
|
const buffer = await input.arrayBuffer()
|
||||||
return await init(buffer)
|
return await init({ module_or_path: buffer })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Error initialising WASM', e)
|
console.log('Error initialising WASM', e)
|
||||||
return Promise.reject(e)
|
return Promise.reject(e)
|
||||||
@ -163,6 +178,10 @@ export class ParseResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parsing was successful. There is guaranteed to be an AST and no fatal errors. There may or may
|
||||||
|
* not be warnings or non-fatal errors.
|
||||||
|
*/
|
||||||
class SuccessParseResult extends ParseResult {
|
class SuccessParseResult extends ParseResult {
|
||||||
program: Node<Program>
|
program: Node<Program>
|
||||||
|
|
||||||
@ -493,18 +512,19 @@ export const _executor = async (
|
|||||||
return Promise.reject(programMemoryOverride)
|
return Promise.reject(programMemoryOverride)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let baseUnit = 'mm'
|
let jsAppSettings = default_app_settings()
|
||||||
if (!TEST) {
|
if (!TEST) {
|
||||||
const getSettingsState = import('components/SettingsAuthProvider').then(
|
const lastSettingsSnapshot = await import(
|
||||||
(module) => module.getSettingsState
|
'components/SettingsAuthProvider'
|
||||||
)
|
).then((module) => module.lastSettingsContextSnapshot)
|
||||||
baseUnit =
|
if (lastSettingsSnapshot) {
|
||||||
(await getSettingsState)()?.modeling.defaultUnit.current || 'mm'
|
jsAppSettings = getAllCurrentSettings(lastSettingsSnapshot)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const execState: RawExecState = await execute_wasm(
|
const execState: RawExecState = await execute(
|
||||||
JSON.stringify(node),
|
JSON.stringify(node),
|
||||||
JSON.stringify(programMemoryOverride?.toRaw() || null),
|
JSON.stringify(programMemoryOverride?.toRaw() || null),
|
||||||
baseUnit,
|
JSON.stringify({ settings: jsAppSettings }),
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
fileSystemManager
|
fileSystemManager
|
||||||
)
|
)
|
||||||
@ -552,20 +572,6 @@ export const makeDefaultPlanes = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const modifyGrid = async (
|
|
||||||
engineCommandManager: EngineCommandManager,
|
|
||||||
hidden: boolean
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
|
||||||
await modify_grid(engineCommandManager, hidden)
|
|
||||||
return
|
|
||||||
} catch (e) {
|
|
||||||
// TODO: do something real with the error.
|
|
||||||
console.log('modify grid error', e)
|
|
||||||
return Promise.reject(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const modifyAstForSketch = async (
|
export const modifyAstForSketch = async (
|
||||||
engineCommandManager: EngineCommandManager,
|
engineCommandManager: EngineCommandManager,
|
||||||
ast: Node<Program>,
|
ast: Node<Program>,
|
||||||
|
@ -32,8 +32,14 @@ export function mouseControlsToCameraSystem(
|
|||||||
mouseControl: MouseControlType | undefined
|
mouseControl: MouseControlType | undefined
|
||||||
): CameraSystem | undefined {
|
): CameraSystem | undefined {
|
||||||
switch (mouseControl) {
|
switch (mouseControl) {
|
||||||
|
// TODO: understand why the values come back without underscores and fix the root cause
|
||||||
|
// @ts-ignore: TS2678
|
||||||
|
case 'kittycad':
|
||||||
case 'kitty_cad':
|
case 'kitty_cad':
|
||||||
return 'KittyCAD'
|
return 'KittyCAD'
|
||||||
|
// TODO: understand why the values come back without underscores and fix the root cause
|
||||||
|
// @ts-ignore: TS2678
|
||||||
|
case 'onshape':
|
||||||
case 'on_shape':
|
case 'on_shape':
|
||||||
return 'OnShape'
|
return 'OnShape'
|
||||||
case 'trackpad_friendly':
|
case 'trackpad_friendly':
|
||||||
@ -44,6 +50,9 @@ export function mouseControlsToCameraSystem(
|
|||||||
return 'NX'
|
return 'NX'
|
||||||
case 'creo':
|
case 'creo':
|
||||||
return 'Creo'
|
return 'Creo'
|
||||||
|
// TODO: understand why the values come back without underscores and fix the root cause
|
||||||
|
// @ts-ignore: TS2678
|
||||||
|
case 'autocad':
|
||||||
case 'auto_cad':
|
case 'auto_cad':
|
||||||
return 'AutoCAD'
|
return 'AutoCAD'
|
||||||
default:
|
default:
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
|
import { angleLengthInfo } from 'components/Toolbar/setAngleLength'
|
||||||
|
import { transformAstSketchLines } from 'lang/std/sketchcombos'
|
||||||
|
import { PathToNode } from 'lang/wasm'
|
||||||
import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes'
|
import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes'
|
||||||
import { KCL_DEFAULT_LENGTH, KCL_DEFAULT_DEGREE } from 'lib/constants'
|
import { KCL_DEFAULT_LENGTH, KCL_DEFAULT_DEGREE } from 'lib/constants'
|
||||||
import { components } from 'lib/machine-api'
|
import { components } from 'lib/machine-api'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
|
import { kclManager } from 'lib/singletons'
|
||||||
|
import { err } from 'lib/trap'
|
||||||
import { modelingMachine, SketchTool } from 'machines/modelingMachine'
|
import { modelingMachine, SketchTool } from 'machines/modelingMachine'
|
||||||
|
import { revolveAxisValidator } from './validators'
|
||||||
|
|
||||||
type OutputFormat = Models['OutputFormat_type']
|
type OutputFormat = Models['OutputFormat_type']
|
||||||
type OutputTypeKey = OutputFormat['type']
|
type OutputTypeKey = OutputFormat['type']
|
||||||
@ -34,9 +40,14 @@ export type ModelingCommandSchema = {
|
|||||||
Loft: {
|
Loft: {
|
||||||
selection: Selections
|
selection: Selections
|
||||||
}
|
}
|
||||||
|
Shell: {
|
||||||
|
selection: Selections
|
||||||
|
thickness: KclCommandValue
|
||||||
|
}
|
||||||
Revolve: {
|
Revolve: {
|
||||||
selection: Selections
|
selection: Selections
|
||||||
angle: KclCommandValue
|
angle: KclCommandValue
|
||||||
|
axis: Selections
|
||||||
}
|
}
|
||||||
Fillet: {
|
Fillet: {
|
||||||
// todo
|
// todo
|
||||||
@ -50,6 +61,18 @@ export type ModelingCommandSchema = {
|
|||||||
'change tool': {
|
'change tool': {
|
||||||
tool: SketchTool
|
tool: SketchTool
|
||||||
}
|
}
|
||||||
|
'Constrain length': {
|
||||||
|
selection: Selections
|
||||||
|
length: KclCommandValue
|
||||||
|
}
|
||||||
|
'Constrain with named value': {
|
||||||
|
currentValue: {
|
||||||
|
valueText: string
|
||||||
|
pathToNode: PathToNode
|
||||||
|
variableName: string
|
||||||
|
}
|
||||||
|
namedValue: KclCommandValue
|
||||||
|
}
|
||||||
'Text-to-CAD': {
|
'Text-to-CAD': {
|
||||||
prompt: string
|
prompt: string
|
||||||
}
|
}
|
||||||
@ -277,6 +300,25 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Shell: {
|
||||||
|
description: 'Hollow out a 3D solid.',
|
||||||
|
icon: 'shell',
|
||||||
|
needsReview: true,
|
||||||
|
args: {
|
||||||
|
selection: {
|
||||||
|
inputType: 'selection',
|
||||||
|
selectionTypes: ['cap', 'wall'],
|
||||||
|
multiple: true,
|
||||||
|
required: true,
|
||||||
|
skip: false,
|
||||||
|
},
|
||||||
|
thickness: {
|
||||||
|
inputType: 'kcl',
|
||||||
|
defaultValue: KCL_DEFAULT_LENGTH,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
// TODO: Update this configuration, copied from extrude for MVP of revolve, specifically the args.selection
|
// TODO: Update this configuration, copied from extrude for MVP of revolve, specifically the args.selection
|
||||||
Revolve: {
|
Revolve: {
|
||||||
description: 'Create a 3D body by rotating a sketch region about an axis.',
|
description: 'Create a 3D body by rotating a sketch region about an axis.',
|
||||||
@ -290,6 +332,13 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
|||||||
required: true,
|
required: true,
|
||||||
skip: true,
|
skip: true,
|
||||||
},
|
},
|
||||||
|
axis: {
|
||||||
|
required: true,
|
||||||
|
inputType: 'selection',
|
||||||
|
selectionTypes: ['segment', 'sweepEdge', 'edgeCutEdge'],
|
||||||
|
multiple: false,
|
||||||
|
validation: revolveAxisValidator,
|
||||||
|
},
|
||||||
angle: {
|
angle: {
|
||||||
inputType: 'kcl',
|
inputType: 'kcl',
|
||||||
defaultValue: KCL_DEFAULT_DEGREE,
|
defaultValue: KCL_DEFAULT_DEGREE,
|
||||||
@ -337,6 +386,88 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'Constrain length': {
|
||||||
|
description: 'Constrain the length of one or more segments.',
|
||||||
|
icon: 'dimension',
|
||||||
|
args: {
|
||||||
|
selection: {
|
||||||
|
inputType: 'selection',
|
||||||
|
selectionTypes: ['segment'],
|
||||||
|
multiple: false,
|
||||||
|
required: true,
|
||||||
|
skip: true,
|
||||||
|
},
|
||||||
|
length: {
|
||||||
|
inputType: 'kcl',
|
||||||
|
required: true,
|
||||||
|
createVariableByDefault: true,
|
||||||
|
defaultValue(_, machineContext) {
|
||||||
|
const selectionRanges = machineContext?.selectionRanges
|
||||||
|
if (!selectionRanges) return KCL_DEFAULT_LENGTH
|
||||||
|
const angleLength = angleLengthInfo({
|
||||||
|
selectionRanges,
|
||||||
|
angleOrLength: 'setLength',
|
||||||
|
})
|
||||||
|
if (err(angleLength)) return KCL_DEFAULT_LENGTH
|
||||||
|
const { transforms } = angleLength
|
||||||
|
|
||||||
|
// QUESTION: is it okay to reference kclManager here? will its state be up to date?
|
||||||
|
const sketched = transformAstSketchLines({
|
||||||
|
ast: structuredClone(kclManager.ast),
|
||||||
|
selectionRanges,
|
||||||
|
transformInfos: transforms,
|
||||||
|
programMemory: kclManager.programMemory,
|
||||||
|
referenceSegName: '',
|
||||||
|
})
|
||||||
|
if (err(sketched)) return KCL_DEFAULT_LENGTH
|
||||||
|
const { valueUsedInTransform } = sketched
|
||||||
|
return valueUsedInTransform?.toString() || KCL_DEFAULT_LENGTH
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Constrain with named value': {
|
||||||
|
description: 'Constrain a value by making it a named constant.',
|
||||||
|
icon: 'make-variable',
|
||||||
|
args: {
|
||||||
|
currentValue: {
|
||||||
|
description:
|
||||||
|
'Path to the node in the AST to constrain. This is never shown to the user.',
|
||||||
|
inputType: 'text',
|
||||||
|
required: false,
|
||||||
|
skip: true,
|
||||||
|
},
|
||||||
|
namedValue: {
|
||||||
|
inputType: 'kcl',
|
||||||
|
required: true,
|
||||||
|
createVariableByDefault: true,
|
||||||
|
variableName(commandBarContext, machineContext) {
|
||||||
|
const { currentValue } = commandBarContext.argumentsToSubmit
|
||||||
|
if (
|
||||||
|
!currentValue ||
|
||||||
|
!(currentValue instanceof Object) ||
|
||||||
|
!('variableName' in currentValue) ||
|
||||||
|
typeof currentValue.variableName !== 'string'
|
||||||
|
) {
|
||||||
|
return 'value'
|
||||||
|
}
|
||||||
|
return currentValue.variableName
|
||||||
|
},
|
||||||
|
defaultValue: (commandBarContext) => {
|
||||||
|
const { currentValue } = commandBarContext.argumentsToSubmit
|
||||||
|
if (
|
||||||
|
!currentValue ||
|
||||||
|
!(currentValue instanceof Object) ||
|
||||||
|
!('valueText' in currentValue) ||
|
||||||
|
typeof currentValue.valueText !== 'string'
|
||||||
|
) {
|
||||||
|
return KCL_DEFAULT_LENGTH
|
||||||
|
}
|
||||||
|
return currentValue.valueText
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
'Text-to-CAD': {
|
'Text-to-CAD': {
|
||||||
description: 'Use the Zoo Text-to-CAD API to generate part starters.',
|
description: 'Use the Zoo Text-to-CAD API to generate part starters.',
|
||||||
icon: 'chat',
|
icon: 'chat',
|
||||||
|
107
src/lib/commandBarConfigs/validators.ts
Normal file
107
src/lib/commandBarConfigs/validators.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { Models } from '@kittycad/lib'
|
||||||
|
import { engineCommandManager } from 'lib/singletons'
|
||||||
|
import { uuidv4 } from 'lib/utils'
|
||||||
|
import { CommandBarContext } from 'machines/commandBarMachine'
|
||||||
|
import { Selections } from 'lib/selections'
|
||||||
|
import { isSolid2D, isSegment, isSweep } from 'lang/std/artifactGraph'
|
||||||
|
|
||||||
|
export const disableDryRunWithRetry = async (numberOfRetries = 3) => {
|
||||||
|
for (let tries = 0; tries < numberOfRetries; tries++) {
|
||||||
|
try {
|
||||||
|
await engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: { type: 'disable_dry_run' },
|
||||||
|
})
|
||||||
|
// Exit out since the command was successful
|
||||||
|
return
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
console.error('disable_dry_run failed. This is bad!')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Takes a callback function and wraps it around enable_dry_run and disable_dry_run
|
||||||
|
export const dryRunWrapper = async (callback: () => Promise<any>) => {
|
||||||
|
// Gotcha: What about race conditions?
|
||||||
|
try {
|
||||||
|
await engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: { type: 'enable_dry_run' },
|
||||||
|
})
|
||||||
|
const result = await callback()
|
||||||
|
return result
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
await disableDryRunWithRetry(5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSelections(selections: unknown): selections is Selections {
|
||||||
|
return (
|
||||||
|
(selections as Selections).graphSelections !== undefined &&
|
||||||
|
(selections as Selections).otherSelections !== undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const revolveAxisValidator = async ({
|
||||||
|
data,
|
||||||
|
context,
|
||||||
|
}: {
|
||||||
|
data: { [key: string]: Selections }
|
||||||
|
context: CommandBarContext
|
||||||
|
}): Promise<boolean | string> => {
|
||||||
|
if (!isSelections(context.argumentsToSubmit.selection)) {
|
||||||
|
return 'Unable to revolve, selections are missing'
|
||||||
|
}
|
||||||
|
const artifact =
|
||||||
|
context.argumentsToSubmit.selection.graphSelections[0].artifact
|
||||||
|
|
||||||
|
if (!artifact) {
|
||||||
|
return 'Unable to revolve, sketch not found'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(isSolid2D(artifact) || isSegment(artifact) || isSweep(artifact))) {
|
||||||
|
return 'Unable to revolve, sketch has no path'
|
||||||
|
}
|
||||||
|
|
||||||
|
const sketchSelection = artifact.pathId
|
||||||
|
let edgeSelection = data.axis.graphSelections[0].artifact?.id
|
||||||
|
|
||||||
|
if (!sketchSelection) {
|
||||||
|
return 'Unable to revolve, sketch is missing'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!edgeSelection) {
|
||||||
|
return 'Unable to revolve, edge is missing'
|
||||||
|
}
|
||||||
|
|
||||||
|
const angleInDegrees: Models['Angle_type'] = {
|
||||||
|
unit: 'degrees',
|
||||||
|
value: 360,
|
||||||
|
}
|
||||||
|
|
||||||
|
const revolveAboutEdgeCommand = async () => {
|
||||||
|
return await engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'revolve_about_edge',
|
||||||
|
angle: angleInDegrees,
|
||||||
|
edge_id: edgeSelection,
|
||||||
|
target: sketchSelection,
|
||||||
|
tolerance: 0.0001,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const attemptRevolve = await dryRunWrapper(revolveAboutEdgeCommand)
|
||||||
|
if (attemptRevolve?.success) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
// return error message for the toast
|
||||||
|
return 'Unable to revolve with selected axis'
|
||||||
|
}
|
||||||
|
}
|
@ -7,7 +7,7 @@ import { ReactNode } from 'react'
|
|||||||
import { MachineManager } from 'components/MachineManagerProvider'
|
import { MachineManager } from 'components/MachineManagerProvider'
|
||||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
import { Artifact } from 'lang/std/artifactGraph'
|
import { Artifact } from 'lang/std/artifactGraph'
|
||||||
|
import { CommandBarContext } from 'machines/commandBarMachine'
|
||||||
type Icon = CustomIconName
|
type Icon = CustomIconName
|
||||||
const PLATFORMS = ['both', 'web', 'desktop'] as const
|
const PLATFORMS = ['both', 'web', 'desktop'] as const
|
||||||
const INPUT_TYPES = [
|
const INPUT_TYPES = [
|
||||||
@ -147,8 +147,30 @@ export type CommandArgumentConfig<
|
|||||||
inputType: 'selection'
|
inputType: 'selection'
|
||||||
selectionTypes: Artifact['type'][]
|
selectionTypes: Artifact['type'][]
|
||||||
multiple: boolean
|
multiple: boolean
|
||||||
|
validation?: ({
|
||||||
|
data,
|
||||||
|
context,
|
||||||
|
}: {
|
||||||
|
data: any
|
||||||
|
context: CommandBarContext
|
||||||
|
}) => Promise<boolean | string>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
inputType: 'kcl'
|
||||||
|
createVariableByDefault?: boolean
|
||||||
|
variableName?:
|
||||||
|
| string
|
||||||
|
| ((
|
||||||
|
commandBarContext: ContextFrom<typeof commandBarMachine>,
|
||||||
|
machineContext?: C
|
||||||
|
) => string)
|
||||||
|
defaultValue?:
|
||||||
|
| string
|
||||||
|
| ((
|
||||||
|
commandBarContext: ContextFrom<typeof commandBarMachine>,
|
||||||
|
machineContext?: C
|
||||||
|
) => string)
|
||||||
}
|
}
|
||||||
| { inputType: 'kcl'; defaultValue?: string } // KCL expression inputs have simple strings as default values
|
|
||||||
| {
|
| {
|
||||||
inputType: 'string'
|
inputType: 'string'
|
||||||
defaultValue?:
|
defaultValue?:
|
||||||
@ -221,8 +243,30 @@ export type CommandArgument<
|
|||||||
inputType: 'selection'
|
inputType: 'selection'
|
||||||
selectionTypes: Artifact['type'][]
|
selectionTypes: Artifact['type'][]
|
||||||
multiple: boolean
|
multiple: boolean
|
||||||
|
validation?: ({
|
||||||
|
data,
|
||||||
|
context,
|
||||||
|
}: {
|
||||||
|
data: any
|
||||||
|
context: CommandBarContext
|
||||||
|
}) => Promise<boolean | string>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
inputType: 'kcl'
|
||||||
|
createVariableByDefault?: boolean
|
||||||
|
variableName?:
|
||||||
|
| string
|
||||||
|
| ((
|
||||||
|
commandBarContext: ContextFrom<typeof commandBarMachine>,
|
||||||
|
machineContext?: ContextFrom<T>
|
||||||
|
) => string)
|
||||||
|
defaultValue?:
|
||||||
|
| string
|
||||||
|
| ((
|
||||||
|
commandBarContext: ContextFrom<typeof commandBarMachine>,
|
||||||
|
machineContext?: ContextFrom<T>
|
||||||
|
) => string)
|
||||||
}
|
}
|
||||||
| { inputType: 'kcl'; defaultValue?: string } // KCL expression inputs have simple strings as default value
|
|
||||||
| {
|
| {
|
||||||
inputType: 'string'
|
inputType: 'string'
|
||||||
defaultValue?:
|
defaultValue?:
|
||||||
|
@ -53,6 +53,7 @@ export const KCL_DEFAULT_CONSTANT_PREFIXES = {
|
|||||||
SKETCH: 'sketch',
|
SKETCH: 'sketch',
|
||||||
EXTRUDE: 'extrude',
|
EXTRUDE: 'extrude',
|
||||||
LOFT: 'loft',
|
LOFT: 'loft',
|
||||||
|
SHELL: 'shell',
|
||||||
SEGMENT: 'seg',
|
SEGMENT: 'seg',
|
||||||
REVOLVE: 'revolve',
|
REVOLVE: 'revolve',
|
||||||
PLANE: 'plane',
|
PLANE: 'plane',
|
||||||
@ -110,3 +111,10 @@ export const KCL_SAMPLES_MANIFEST_URLS = {
|
|||||||
|
|
||||||
/** Toast id for the app auto-updater toast */
|
/** Toast id for the app auto-updater toast */
|
||||||
export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast'
|
export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast'
|
||||||
|
|
||||||
|
/** Local sketch axis values in KCL for operations, it could either be 'X' or 'Y' */
|
||||||
|
export const KCL_AXIS_X = 'X'
|
||||||
|
export const KCL_AXIS_Y = 'Y'
|
||||||
|
export const KCL_AXIS_NEG_X = '-X'
|
||||||
|
export const KCL_AXIS_NEG_Y = '-Y'
|
||||||
|
export const KCL_DEFAULT_AXIS = 'X'
|
||||||
|
@ -155,6 +155,8 @@ export function buildCommandArgument<
|
|||||||
context: ContextFrom<T>,
|
context: ContextFrom<T>,
|
||||||
machineActor: Actor<T>
|
machineActor: Actor<T>
|
||||||
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
|
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
|
||||||
|
// GOTCHA: modelingCommandConfig is not a 1:1 mapping to this baseCommandArgument
|
||||||
|
// You need to manually add key/value pairs here.
|
||||||
const baseCommandArgument = {
|
const baseCommandArgument = {
|
||||||
description: arg.description,
|
description: arg.description,
|
||||||
required: arg.required,
|
required: arg.required,
|
||||||
@ -181,10 +183,13 @@ export function buildCommandArgument<
|
|||||||
...baseCommandArgument,
|
...baseCommandArgument,
|
||||||
multiple: arg.multiple,
|
multiple: arg.multiple,
|
||||||
selectionTypes: arg.selectionTypes,
|
selectionTypes: arg.selectionTypes,
|
||||||
|
validation: arg.validation,
|
||||||
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
|
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
|
||||||
} else if (arg.inputType === 'kcl') {
|
} else if (arg.inputType === 'kcl') {
|
||||||
return {
|
return {
|
||||||
inputType: arg.inputType,
|
inputType: arg.inputType,
|
||||||
|
createVariableByDefault: arg.createVariableByDefault,
|
||||||
|
variableName: arg.variableName,
|
||||||
defaultValue: arg.defaultValue,
|
defaultValue: arg.defaultValue,
|
||||||
...baseCommandArgument,
|
...baseCommandArgument,
|
||||||
} satisfies CommandArgument<O, T> & { inputType: 'kcl' }
|
} satisfies CommandArgument<O, T> & { inputType: 'kcl' }
|
||||||
|
@ -569,6 +569,17 @@ export function canSweepSelection(selection: Selections) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canRevolveSelection(selection: Selections) {
|
||||||
|
const commonNodes = selection.graphSelections.map((_, i) =>
|
||||||
|
buildCommonNodeFromSelection(selection, i)
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
!!isSketchPipe(selection) &&
|
||||||
|
(commonNodes.every((n) => nodeHasClose(n)) ||
|
||||||
|
commonNodes.every((n) => nodeHasCircle(n)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function canLoftSelection(selection: Selections) {
|
export function canLoftSelection(selection: Selections) {
|
||||||
const commonNodes = selection.graphSelections.map((_, i) =>
|
const commonNodes = selection.graphSelections.map((_, i) =>
|
||||||
buildCommonNodeFromSelection(selection, i)
|
buildCommonNodeFromSelection(selection, i)
|
||||||
@ -585,6 +596,17 @@ export function canLoftSelection(selection: Selections) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canShellSelection(selection: Selections) {
|
||||||
|
const commonNodes = selection.graphSelections.map((_, i) =>
|
||||||
|
buildCommonNodeFromSelection(selection, i)
|
||||||
|
)
|
||||||
|
return commonNodes.every(
|
||||||
|
(n) =>
|
||||||
|
n.selection.artifact?.type === 'cap' ||
|
||||||
|
n.selection.artifact?.type === 'wall'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// This accounts for non-geometry selections under "other"
|
// This accounts for non-geometry selections under "other"
|
||||||
export type ResolvedSelectionType = Artifact['type'] | 'other'
|
export type ResolvedSelectionType = Artifact['type'] | 'other'
|
||||||
export type SelectionCountsByType = Map<ResolvedSelectionType, number>
|
export type SelectionCountsByType = Map<ResolvedSelectionType, number>
|
||||||
@ -619,12 +641,29 @@ export function getSelectionCountByType(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
selection.graphSelections.forEach((selection) => {
|
selection.graphSelections.forEach((graphSelection) => {
|
||||||
if (!selection.artifact) {
|
if (!graphSelection.artifact) {
|
||||||
incrementOrInitializeSelectionType('other')
|
/**
|
||||||
return
|
* TODO: remove this heuristic-based selection type detection.
|
||||||
|
* Currently, if you've created a sketch and have not left sketch mode,
|
||||||
|
* the selection will be a segment selection with no artifact.
|
||||||
|
* This is because the mock execution does not update the artifact graph.
|
||||||
|
* Once we move the artifactGraph creation to WASM, we can remove this,
|
||||||
|
* as the artifactGraph will always be up-to-date.
|
||||||
|
*/
|
||||||
|
if (isSingleCursorInPipe(selection, kclManager.ast)) {
|
||||||
|
incrementOrInitializeSelectionType('segment')
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
'Selection is outside of a sketch but has no artifact. Sketch segment selections are the only kind that can have a valid selection with no artifact.',
|
||||||
|
JSON.stringify(graphSelection)
|
||||||
|
)
|
||||||
|
incrementOrInitializeSelectionType('other')
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
incrementOrInitializeSelectionType(selection.artifact.type)
|
incrementOrInitializeSelectionType(graphSelection.artifact.type)
|
||||||
})
|
})
|
||||||
|
|
||||||
return selectionsByType
|
return selectionsByType
|
||||||
|
@ -12,7 +12,7 @@ export type InteractionMapItem = {
|
|||||||
* Controls both the available names for interaction map categories
|
* Controls both the available names for interaction map categories
|
||||||
* and the order in which they are displayed.
|
* and the order in which they are displayed.
|
||||||
*/
|
*/
|
||||||
export const interactionMapCategories = [
|
const interactionMapCategories = [
|
||||||
'Sketching',
|
'Sketching',
|
||||||
'Modeling',
|
'Modeling',
|
||||||
'Command Palette',
|
'Command Palette',
|
||||||
|
@ -2,6 +2,7 @@ import { DeepPartial } from 'lib/types'
|
|||||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||||
import {
|
import {
|
||||||
configurationToSettingsPayload,
|
configurationToSettingsPayload,
|
||||||
|
getAllCurrentSettings,
|
||||||
projectConfigurationToSettingsPayload,
|
projectConfigurationToSettingsPayload,
|
||||||
setSettingsAtLevel,
|
setSettingsAtLevel,
|
||||||
} from './settingsUtils'
|
} from './settingsUtils'
|
||||||
@ -65,3 +66,48 @@ describe(`testing settings initialization`, () => {
|
|||||||
expect(settings.app.themeColor.current).toBe('200')
|
expect(settings.app.themeColor.current).toBe('200')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe(`testing getAllCurrentSettings`, () => {
|
||||||
|
it(`returns the correct settings`, () => {
|
||||||
|
// Set up the settings
|
||||||
|
let settings = createSettings()
|
||||||
|
const appConfiguration: DeepPartial<Configuration> = {
|
||||||
|
settings: {
|
||||||
|
app: {
|
||||||
|
appearance: {
|
||||||
|
theme: 'dark',
|
||||||
|
color: 190,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const projectConfiguration: DeepPartial<Configuration> = {
|
||||||
|
settings: {
|
||||||
|
app: {
|
||||||
|
appearance: {
|
||||||
|
theme: 'light',
|
||||||
|
color: 200,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modeling: {
|
||||||
|
base_unit: 'ft',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const appSettingsPayload = configurationToSettingsPayload(appConfiguration)
|
||||||
|
const projectSettingsPayload =
|
||||||
|
projectConfigurationToSettingsPayload(projectConfiguration)
|
||||||
|
|
||||||
|
setSettingsAtLevel(settings, 'user', appSettingsPayload)
|
||||||
|
setSettingsAtLevel(settings, 'project', projectSettingsPayload)
|
||||||
|
|
||||||
|
// Now the test: get all the settings' current resolved values
|
||||||
|
const allCurrentSettings = getAllCurrentSettings(settings)
|
||||||
|
// This one gets the 'user'-level theme because it's ignored at the project level
|
||||||
|
// (see the test "doesn't read theme from project settings")
|
||||||
|
expect(allCurrentSettings.app.theme).toBe('dark')
|
||||||
|
expect(allCurrentSettings.app.themeColor).toBe('200')
|
||||||
|
expect(allCurrentSettings.modeling.defaultUnit).toBe('ft')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
@ -286,6 +286,27 @@ export function getChangedSettingsAtLevel(
|
|||||||
return changedSettings
|
return changedSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAllCurrentSettings(
|
||||||
|
allSettings: typeof settings
|
||||||
|
): SaveSettingsPayload {
|
||||||
|
const currentSettings = {} as SaveSettingsPayload
|
||||||
|
Object.entries(allSettings).forEach(([category, settingsCategory]) => {
|
||||||
|
const categoryKey = category as keyof typeof settings
|
||||||
|
Object.entries(settingsCategory).forEach(
|
||||||
|
([setting, settingValue]: [string, Setting]) => {
|
||||||
|
const settingKey =
|
||||||
|
setting as keyof (typeof settings)[typeof categoryKey]
|
||||||
|
currentSettings[categoryKey] = {
|
||||||
|
...currentSettings[categoryKey],
|
||||||
|
[settingKey]: settingValue.current,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return currentSettings
|
||||||
|
}
|
||||||
|
|
||||||
export function setSettingsAtLevel(
|
export function setSettingsAtLevel(
|
||||||
allSettings: typeof settings,
|
allSettings: typeof settings,
|
||||||
level: SettingsLevel,
|
level: SettingsLevel,
|
||||||
|
@ -112,9 +112,6 @@ export async function executor(
|
|||||||
makeDefaultPlanes: () => {
|
makeDefaultPlanes: () => {
|
||||||
return new Promise((resolve) => resolve(defaultPlanes))
|
return new Promise((resolve) => resolve(defaultPlanes))
|
||||||
},
|
},
|
||||||
modifyGrid: (hidden: boolean) => {
|
|
||||||
return new Promise((resolve) => resolve())
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
@ -190,9 +190,15 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'shell',
|
id: 'shell',
|
||||||
onClick: () => console.error('Shell not yet implemented'),
|
onClick: ({ commandBarSend }) => {
|
||||||
|
commandBarSend({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: { name: 'Shell', groupId: 'modeling' },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
disabled: (state) => !state.can({ type: 'Shell' }),
|
||||||
icon: 'shell',
|
icon: 'shell',
|
||||||
status: 'kcl-only',
|
status: 'available',
|
||||||
title: 'Shell',
|
title: 'Shell',
|
||||||
description: 'Hollow out a 3D solid.',
|
description: 'Hollow out a 3D solid.',
|
||||||
links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/shell' }],
|
links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/shell' }],
|
||||||
@ -534,13 +540,15 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
id: 'constraint-length',
|
id: 'constraint-length',
|
||||||
disabled: (state) =>
|
disabled: (state) => !state.matches({ Sketch: 'SketchIdle' }),
|
||||||
!(
|
onClick: ({ commandBarSend }) =>
|
||||||
state.matches({ Sketch: 'SketchIdle' }) &&
|
commandBarSend({
|
||||||
state.can({ type: 'Constrain length' })
|
type: 'Find and select command',
|
||||||
),
|
data: {
|
||||||
onClick: ({ modelingSend }) =>
|
name: 'Constrain length',
|
||||||
modelingSend({ type: 'Constrain length' }),
|
groupId: 'modeling',
|
||||||
|
},
|
||||||
|
}),
|
||||||
icon: 'dimension',
|
icon: 'dimension',
|
||||||
status: 'available',
|
status: 'available',
|
||||||
title: 'Length',
|
title: 'Length',
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
import { Selections__old } from 'lib/selections'
|
import { Selections__old } from 'lib/selections'
|
||||||
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
|
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
|
||||||
import { MachineManager } from 'components/MachineManagerProvider'
|
import { MachineManager } from 'components/MachineManagerProvider'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
export type CommandBarContext = {
|
export type CommandBarContext = {
|
||||||
commands: Command[]
|
commands: Command[]
|
||||||
@ -247,14 +248,69 @@ export const commandBarMachine = setup({
|
|||||||
'All arguments are skippable': () => false,
|
'All arguments are skippable': () => false,
|
||||||
},
|
},
|
||||||
actors: {
|
actors: {
|
||||||
'Validate argument': fromPromise(({ input }) => {
|
'Validate argument': fromPromise(
|
||||||
return new Promise((resolve, reject) => {
|
({
|
||||||
// TODO: figure out if we should validate argument data here or in the form itself,
|
input,
|
||||||
// and if we should support people configuring a argument's validation function
|
}: {
|
||||||
|
input: {
|
||||||
|
context: CommandBarContext | undefined
|
||||||
|
event: CommandBarMachineEvent | undefined
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!input || input?.event?.type !== 'Submit argument') {
|
||||||
|
toast.error(`Unable to validate, wrong event type.`)
|
||||||
|
return reject(`Unable to validate, wrong event type`)
|
||||||
|
}
|
||||||
|
|
||||||
resolve(input)
|
const context = input?.context
|
||||||
})
|
|
||||||
}),
|
if (!context) {
|
||||||
|
toast.error(`Unable to validate, wrong argument.`)
|
||||||
|
return reject(`Unable to validate, wrong argument`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = input.event.data
|
||||||
|
const argName = context.currentArgument?.name
|
||||||
|
const args = context?.selectedCommand?.args
|
||||||
|
const argConfig = args && argName ? args[argName] : undefined
|
||||||
|
// Only do a validation check if the argument, selectedCommand, and the validation function are defined
|
||||||
|
if (
|
||||||
|
context.currentArgument &&
|
||||||
|
context.selectedCommand &&
|
||||||
|
argConfig?.inputType === 'selection' &&
|
||||||
|
argConfig?.validation
|
||||||
|
) {
|
||||||
|
argConfig
|
||||||
|
.validation({ context, data })
|
||||||
|
.then((result) => {
|
||||||
|
if (typeof result === 'boolean' && result === true) {
|
||||||
|
return resolve(data)
|
||||||
|
} else {
|
||||||
|
// validation failed
|
||||||
|
if (typeof result === 'string') {
|
||||||
|
// The result of the validation is the error message
|
||||||
|
toast.error(result)
|
||||||
|
return reject(
|
||||||
|
`unable to validate ${argName}, Message: ${result}`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Default message if there is not a custom one sent
|
||||||
|
toast.error(`Unable to validate ${argName}`)
|
||||||
|
return reject(`unable to validate ${argName}}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return reject(`unable to validate ${argName}}`)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Missing several requirements for validate argument, just bypass
|
||||||
|
return resolve(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
),
|
||||||
'Validate all arguments': fromPromise(
|
'Validate all arguments': fromPromise(
|
||||||
({ input }: { input: CommandBarContext }) => {
|
({ input }: { input: CommandBarContext }) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -449,9 +505,10 @@ export const commandBarMachine = setup({
|
|||||||
invoke: {
|
invoke: {
|
||||||
src: 'Validate argument',
|
src: 'Validate argument',
|
||||||
id: 'validateSingleArgument',
|
id: 'validateSingleArgument',
|
||||||
input: ({ event }) => {
|
input: ({ event, context }) => {
|
||||||
if (event.type !== 'Submit argument') return {}
|
if (event.type !== 'Submit argument')
|
||||||
return event.data
|
return { event: undefined, context: undefined }
|
||||||
|
return { event, context }
|
||||||
},
|
},
|
||||||
onDone: {
|
onDone: {
|
||||||
target: '#Command Bar.Checking Arguments',
|
target: '#Command Bar.Checking Arguments',
|
||||||
|
File diff suppressed because one or more lines are too long
@ -42,8 +42,6 @@ export const settingsMachine = setup({
|
|||||||
setClientTheme: () => {},
|
setClientTheme: () => {},
|
||||||
'Execute AST': () => {},
|
'Execute AST': () => {},
|
||||||
toastSuccess: () => {},
|
toastSuccess: () => {},
|
||||||
setEngineEdges: () => {},
|
|
||||||
setEngineScaleGridVisibility: () => {},
|
|
||||||
setClientSideSceneUnits: () => {},
|
setClientSideSceneUnits: () => {},
|
||||||
persistSettings: () => {},
|
persistSettings: () => {},
|
||||||
resetSettings: assign(({ context, event }) => {
|
resetSettings: assign(({ context, event }) => {
|
||||||
@ -172,7 +170,7 @@ export const settingsMachine = setup({
|
|||||||
'set.modeling.highlightEdges': {
|
'set.modeling.highlightEdges': {
|
||||||
target: 'persisting settings',
|
target: 'persisting settings',
|
||||||
|
|
||||||
actions: ['setSettingAtLevel', 'toastSuccess', 'setEngineEdges'],
|
actions: ['setSettingAtLevel', 'toastSuccess', 'Execute AST'],
|
||||||
},
|
},
|
||||||
|
|
||||||
'Reset settings': {
|
'Reset settings': {
|
||||||
@ -201,11 +199,7 @@ export const settingsMachine = setup({
|
|||||||
|
|
||||||
'set.modeling.showScaleGrid': {
|
'set.modeling.showScaleGrid': {
|
||||||
target: 'persisting settings',
|
target: 'persisting settings',
|
||||||
actions: [
|
actions: ['setSettingAtLevel', 'toastSuccess', 'Execute AST'],
|
||||||
'setSettingAtLevel',
|
|
||||||
'toastSuccess',
|
|
||||||
'setEngineScaleGridVisibility',
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -44,11 +44,6 @@ process.env.VITE_KC_SITE_BASE_URL ??= 'https://zoo.dev'
|
|||||||
process.env.VITE_KC_SKIP_AUTH ??= 'false'
|
process.env.VITE_KC_SKIP_AUTH ??= 'false'
|
||||||
process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??= '15000'
|
process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??= '15000'
|
||||||
|
|
||||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
|
||||||
if (require('electron-squirrel-startup')) {
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
|
|
||||||
const ZOO_STUDIO_PROTOCOL = 'zoo-studio'
|
const ZOO_STUDIO_PROTOCOL = 'zoo-studio'
|
||||||
|
|
||||||
/// Register our application to handle all "electron-fiddle://" protocols.
|
/// Register our application to handle all "electron-fiddle://" protocols.
|
||||||
@ -256,6 +251,9 @@ export function getAutoUpdater(): AppUpdater {
|
|||||||
|
|
||||||
app.on('ready', () => {
|
app.on('ready', () => {
|
||||||
const autoUpdater = getAutoUpdater()
|
const autoUpdater = getAutoUpdater()
|
||||||
|
// TODO: we're getting `Error: Response ends without calling any handlers` with our setup,
|
||||||
|
// so at the moment this isn't worth enabling
|
||||||
|
autoUpdater.disableDifferentialDownload = true
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
autoUpdater.checkForUpdates().catch(reportRejection)
|
autoUpdater.checkForUpdates().catch(reportRejection)
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
@ -30,6 +30,14 @@ export const PACKAGE_NAME = isDesktop()
|
|||||||
? window.electron.packageJson.name
|
? window.electron.packageJson.name
|
||||||
: 'zoo-modeling-app'
|
: 'zoo-modeling-app'
|
||||||
|
|
||||||
|
export const IS_NIGHTLY = PACKAGE_NAME.indexOf('-nightly') > -1
|
||||||
|
|
||||||
|
export function getReleaseUrl(version: string = APP_VERSION) {
|
||||||
|
return `https://github.com/KittyCAD/modeling-app/releases/tag/${
|
||||||
|
IS_NIGHTLY ? 'nightly-' : ''
|
||||||
|
}v${version}`
|
||||||
|
}
|
||||||
|
|
||||||
export const Settings = () => {
|
export const Settings = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
|
8
src/wasm-lib/Cargo.lock
generated
8
src/wasm-lib/Cargo.lock
generated
@ -1721,7 +1721,9 @@ dependencies = [
|
|||||||
"parse-display 0.9.1",
|
"parse-display 0.9.1",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"pyo3",
|
"pyo3",
|
||||||
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"rgba_simple",
|
||||||
"ropey",
|
"ropey",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
@ -2971,6 +2973,12 @@ dependencies = [
|
|||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rgba_simple"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6cd655523701785087f69900df39892fb7b9b0721aa67682f571c38c32ac58a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.8"
|
version = "0.17.8"
|
||||||
|
@ -79,7 +79,10 @@ kittycad = { version = "0.3.28", default-features = false, features = ["js", "re
|
|||||||
kittycad-modeling-cmds = { version = "0.2.77", features = ["websocket"] }
|
kittycad-modeling-cmds = { version = "0.2.77", features = ["websocket"] }
|
||||||
|
|
||||||
[workspace.lints.clippy]
|
[workspace.lints.clippy]
|
||||||
|
assertions_on_result_states = "warn"
|
||||||
|
dbg_macro = "warn"
|
||||||
iter_over_hash_type = "warn"
|
iter_over_hash_type = "warn"
|
||||||
|
lossy_float_literal = "warn"
|
||||||
|
|
||||||
[[test]]
|
[[test]]
|
||||||
name = "executor"
|
name = "executor"
|
||||||
|
@ -12,8 +12,8 @@ redo-kcl-stdlib-docs-no-imgs:
|
|||||||
# Generate the stdlib image artifacts
|
# Generate the stdlib image artifacts
|
||||||
# Then run the stdlib docs generation
|
# Then run the stdlib docs generation
|
||||||
redo-kcl-stdlib-docs:
|
redo-kcl-stdlib-docs:
|
||||||
TWENTY_TWENTY=overwrite {{cnr}} -p kcl-lib kcl_test_example
|
TWENTY_TWENTY=overwrite {{cnr}} -p kcl-lib --no-fail-fast -- kcl_test_example
|
||||||
EXPECTORATE=overwrite {{cnr}} -p kcl-lib docs::gen_std_tests::test_generate_stdlib
|
EXPECTORATE=overwrite {{cnr}} -p kcl-lib --no-fail-fast -- docs::gen_std_tests::test_generate_stdlib
|
||||||
|
|
||||||
# Copy a test KCL file from executor tests into a new simulation test.
|
# Copy a test KCL file from executor tests into a new simulation test.
|
||||||
copy-exec-test-into-sim-test test_name:
|
copy-exec-test-into-sim-test test_name:
|
||||||
|
@ -15,5 +15,5 @@ async fn kcl_to_core_test() {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert!(result.is_ok());
|
result.unwrap();
|
||||||
}
|
}
|
||||||
|
@ -40,10 +40,12 @@ miette = "7.2.0"
|
|||||||
mime_guess = "2.0.5"
|
mime_guess = "2.0.5"
|
||||||
parse-display = "0.9.1"
|
parse-display = "0.9.1"
|
||||||
pyo3 = { version = "0.22.6", optional = true }
|
pyo3 = { version = "0.22.6", optional = true }
|
||||||
|
regex = "1.11.1"
|
||||||
reqwest = { version = "0.12", default-features = false, features = [
|
reqwest = { version = "0.12", default-features = false, features = [
|
||||||
"stream",
|
"stream",
|
||||||
"rustls-tls",
|
"rustls-tls",
|
||||||
] }
|
] }
|
||||||
|
rgba_simple = "0.10.0"
|
||||||
ropey = "1.6.1"
|
ropey = "1.6.1"
|
||||||
schemars = { version = "0.8.17", features = [
|
schemars = { version = "0.8.17", features = [
|
||||||
"impl_json_schema",
|
"impl_json_schema",
|
||||||
|
@ -489,6 +489,12 @@ fn get_autocomplete_snippet_from_schema(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if prop_name == "color" {
|
||||||
|
fn_docs.push_str(&format!("\t{}: ${{{}:\"#ff0000\"}},\n", prop_name, i));
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some((new_index, snippet)) = get_autocomplete_snippet_from_schema(prop, i)? {
|
if let Some((new_index, snippet)) = get_autocomplete_snippet_from_schema(prop, i)? {
|
||||||
fn_docs.push_str(&format!("\t{}: {},\n", prop_name, snippet));
|
fn_docs.push_str(&format!("\t{}: {},\n", prop_name, snippet));
|
||||||
i = new_index + 1;
|
i = new_index + 1;
|
||||||
@ -946,6 +952,21 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_autocomplete_snippet_appearance() {
|
||||||
|
let appearance_fn: Box<dyn StdLibFn> = Box::new(crate::std::appearance::Appearance);
|
||||||
|
let snippet = appearance_fn.to_autocomplete_snippet().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
snippet,
|
||||||
|
r#"appearance({
|
||||||
|
color: ${0:"#
|
||||||
|
.to_owned()
|
||||||
|
+ "\"#"
|
||||||
|
+ r#"ff0000"},
|
||||||
|
}, ${1:%})${}"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// We want to test the snippets we compile at lsp start.
|
// We want to test the snippets we compile at lsp start.
|
||||||
#[test]
|
#[test]
|
||||||
fn get_all_stdlib_autocomplete_snippets() {
|
fn get_all_stdlib_autocomplete_snippets() {
|
||||||
|
@ -120,6 +120,61 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the visibility of edges.
|
||||||
|
async fn set_edge_visibility(
|
||||||
|
&self,
|
||||||
|
visible: bool,
|
||||||
|
source_range: SourceRange,
|
||||||
|
) -> Result<(), crate::errors::KclError> {
|
||||||
|
self.batch_modeling_cmd(
|
||||||
|
uuid::Uuid::new_v4(),
|
||||||
|
source_range,
|
||||||
|
&ModelingCmd::from(mcmd::EdgeLinesVisible { hidden: !visible }),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_units(
|
||||||
|
&self,
|
||||||
|
units: crate::UnitLength,
|
||||||
|
source_range: SourceRange,
|
||||||
|
) -> Result<(), crate::errors::KclError> {
|
||||||
|
// Before we even start executing the program, set the units.
|
||||||
|
self.batch_modeling_cmd(
|
||||||
|
uuid::Uuid::new_v4(),
|
||||||
|
source_range,
|
||||||
|
&ModelingCmd::from(mcmd::SetSceneUnits { unit: units.into() }),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Re-run the command to apply the settings.
|
||||||
|
async fn reapply_settings(
|
||||||
|
&self,
|
||||||
|
settings: &crate::ExecutorSettings,
|
||||||
|
source_range: SourceRange,
|
||||||
|
) -> Result<(), crate::errors::KclError> {
|
||||||
|
// Set the edge visibility.
|
||||||
|
self.set_edge_visibility(settings.highlight_edges, source_range).await?;
|
||||||
|
|
||||||
|
// Change the units.
|
||||||
|
self.set_units(settings.units, source_range).await?;
|
||||||
|
|
||||||
|
// Send the command to show the grid.
|
||||||
|
self.modify_grid(!settings.show_grid, source_range).await?;
|
||||||
|
|
||||||
|
// We do not have commands for changing ssao on the fly.
|
||||||
|
|
||||||
|
// Flush the batch queue, so the settings are applied right away.
|
||||||
|
self.flush_batch(false, source_range).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// Add a modeling command to the batch but don't fire it right away.
|
// Add a modeling command to the batch but don't fire it right away.
|
||||||
async fn batch_modeling_cmd(
|
async fn batch_modeling_cmd(
|
||||||
&self,
|
&self,
|
||||||
@ -504,11 +559,11 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn modify_grid(&self, hidden: bool) -> Result<(), KclError> {
|
async fn modify_grid(&self, hidden: bool, source_range: SourceRange) -> Result<(), KclError> {
|
||||||
// Hide/show the grid.
|
// Hide/show the grid.
|
||||||
self.batch_modeling_cmd(
|
self.batch_modeling_cmd(
|
||||||
uuid::Uuid::new_v4(),
|
uuid::Uuid::new_v4(),
|
||||||
Default::default(),
|
source_range,
|
||||||
&ModelingCmd::from(mcmd::ObjectVisible {
|
&ModelingCmd::from(mcmd::ObjectVisible {
|
||||||
hidden,
|
hidden,
|
||||||
object_id: *GRID_OBJECT_ID,
|
object_id: *GRID_OBJECT_ID,
|
||||||
@ -519,7 +574,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
// Hide/show the grid scale text.
|
// Hide/show the grid scale text.
|
||||||
self.batch_modeling_cmd(
|
self.batch_modeling_cmd(
|
||||||
uuid::Uuid::new_v4(),
|
uuid::Uuid::new_v4(),
|
||||||
Default::default(),
|
source_range,
|
||||||
&ModelingCmd::from(mcmd::ObjectVisible {
|
&ModelingCmd::from(mcmd::ObjectVisible {
|
||||||
hidden,
|
hidden,
|
||||||
object_id: *GRID_SCALE_TEXT_OBJECT_ID,
|
object_id: *GRID_SCALE_TEXT_OBJECT_ID,
|
||||||
@ -527,8 +582,6 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
self.flush_batch(false, Default::default()).await?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
50
src/wasm-lib/kcl/src/execution/cache.rs
Normal file
50
src/wasm-lib/kcl/src/execution/cache.rs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
//! Functions for helping with caching an ast and finding the parts the changed.
|
||||||
|
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
execution::ExecState,
|
||||||
|
parsing::ast::types::{Node, Program},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Information for the caching an AST and smartly re-executing it if we can.
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct CacheInformation {
|
||||||
|
/// The old information.
|
||||||
|
pub old: Option<OldAstState>,
|
||||||
|
/// The new ast to executed.
|
||||||
|
pub new_ast: Node<Program>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The old ast and program memory.
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct OldAstState {
|
||||||
|
/// The ast.
|
||||||
|
pub ast: Node<Program>,
|
||||||
|
/// The exec state.
|
||||||
|
pub exec_state: ExecState,
|
||||||
|
/// The last settings used for execution.
|
||||||
|
pub settings: crate::execution::ExecutorSettings,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::Program> for CacheInformation {
|
||||||
|
fn from(program: crate::Program) -> Self {
|
||||||
|
CacheInformation {
|
||||||
|
old: None,
|
||||||
|
new_ast: program.ast,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The result of a cache check.
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct CacheResult {
|
||||||
|
/// Should we clear the scene and start over?
|
||||||
|
pub clear_scene: bool,
|
||||||
|
/// The program that needs to be executed.
|
||||||
|
pub program: Node<Program>,
|
||||||
|
}
|
@ -326,29 +326,12 @@ async fn inner_execute_pipe_body(
|
|||||||
ctx: &ExecutorContext,
|
ctx: &ExecutorContext,
|
||||||
) -> Result<KclValue, KclError> {
|
) -> Result<KclValue, KclError> {
|
||||||
for expression in body {
|
for expression in body {
|
||||||
match expression {
|
if let Expr::TagDeclarator(_) = expression {
|
||||||
Expr::TagDeclarator(_) => {
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
message: format!("This cannot be in a PipeExpression: {:?}", expression),
|
||||||
message: format!("This cannot be in a PipeExpression: {:?}", expression),
|
source_ranges: vec![expression.into()],
|
||||||
source_ranges: vec![expression.into()],
|
}));
|
||||||
}));
|
}
|
||||||
}
|
|
||||||
Expr::Literal(_)
|
|
||||||
| Expr::Identifier(_)
|
|
||||||
| Expr::BinaryExpression(_)
|
|
||||||
| Expr::FunctionExpression(_)
|
|
||||||
| Expr::CallExpression(_)
|
|
||||||
| Expr::CallExpressionKw(_)
|
|
||||||
| Expr::PipeExpression(_)
|
|
||||||
| Expr::PipeSubstitution(_)
|
|
||||||
| Expr::ArrayExpression(_)
|
|
||||||
| Expr::ArrayRangeExpression(_)
|
|
||||||
| Expr::ObjectExpression(_)
|
|
||||||
| Expr::MemberExpression(_)
|
|
||||||
| Expr::UnaryExpression(_)
|
|
||||||
| Expr::IfExpression(_)
|
|
||||||
| Expr::None(_) => {}
|
|
||||||
};
|
|
||||||
let metadata = Metadata {
|
let metadata = Metadata {
|
||||||
source_range: SourceRange::from(expression),
|
source_range: SourceRange::from(expression),
|
||||||
};
|
};
|
||||||
@ -366,9 +349,11 @@ impl Node<CallExpressionKw> {
|
|||||||
#[async_recursion]
|
#[async_recursion]
|
||||||
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
||||||
let fn_name = &self.callee.name;
|
let fn_name = &self.callee.name;
|
||||||
|
let callsite: SourceRange = self.into();
|
||||||
|
|
||||||
// Build a hashmap from argument labels to the final evaluated values.
|
// Build a hashmap from argument labels to the final evaluated values.
|
||||||
let mut fn_args = HashMap::with_capacity(self.arguments.len());
|
let mut fn_args = HashMap::with_capacity(self.arguments.len());
|
||||||
|
let mut tag_declarator_args = Vec::new();
|
||||||
for arg_expr in &self.arguments {
|
for arg_expr in &self.arguments {
|
||||||
let source_range = SourceRange::from(arg_expr.arg.clone());
|
let source_range = SourceRange::from(arg_expr.arg.clone());
|
||||||
let metadata = Metadata { source_range };
|
let metadata = Metadata { source_range };
|
||||||
@ -376,8 +361,12 @@ impl Node<CallExpressionKw> {
|
|||||||
.execute_expr(&arg_expr.arg, exec_state, &metadata, StatementKind::Expression)
|
.execute_expr(&arg_expr.arg, exec_state, &metadata, StatementKind::Expression)
|
||||||
.await?;
|
.await?;
|
||||||
fn_args.insert(arg_expr.label.name.clone(), Arg::new(value, source_range));
|
fn_args.insert(arg_expr.label.name.clone(), Arg::new(value, source_range));
|
||||||
|
if let Expr::TagDeclarator(td) = &arg_expr.arg {
|
||||||
|
tag_declarator_args.push((td.inner.clone(), source_range));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let fn_args = fn_args; // remove mutability
|
let fn_args = fn_args; // remove mutability
|
||||||
|
let tag_declarator_args = tag_declarator_args; // remove mutability
|
||||||
|
|
||||||
// Evaluate the unlabeled first param, if any exists.
|
// Evaluate the unlabeled first param, if any exists.
|
||||||
let unlabeled = if let Some(ref arg_expr) = self.unlabeled {
|
let unlabeled = if let Some(ref arg_expr) = self.unlabeled {
|
||||||
@ -403,11 +392,43 @@ impl Node<CallExpressionKw> {
|
|||||||
FunctionKind::Core(func) => {
|
FunctionKind::Core(func) => {
|
||||||
// Attempt to call the function.
|
// Attempt to call the function.
|
||||||
let mut result = func.std_lib_fn()(exec_state, args).await?;
|
let mut result = func.std_lib_fn()(exec_state, args).await?;
|
||||||
update_memory_for_tags_of_geometry(&mut result, exec_state)?;
|
update_memory_for_tags_of_geometry(&mut result, &tag_declarator_args, exec_state)?;
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
FunctionKind::UserDefined => {
|
FunctionKind::UserDefined => {
|
||||||
todo!("Part of modeling-app#4600: Support keyword arguments for user-defined functions")
|
let source_range = SourceRange::from(self);
|
||||||
|
// Clone the function so that we can use a mutable reference to
|
||||||
|
// exec_state.
|
||||||
|
let func = exec_state.memory.get(fn_name, source_range)?.clone();
|
||||||
|
let fn_dynamic_state = exec_state.dynamic_state.merge(&exec_state.memory);
|
||||||
|
|
||||||
|
let return_value = {
|
||||||
|
let previous_dynamic_state = std::mem::replace(&mut exec_state.dynamic_state, fn_dynamic_state);
|
||||||
|
let result = func
|
||||||
|
.call_fn_kw(args, exec_state, ctx.clone(), callsite)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
// Add the call expression to the source ranges.
|
||||||
|
// TODO currently ignored by the frontend
|
||||||
|
e.add_source_ranges(vec![source_range])
|
||||||
|
});
|
||||||
|
exec_state.dynamic_state = previous_dynamic_state;
|
||||||
|
result?
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = return_value.ok_or_else(move || {
|
||||||
|
let mut source_ranges: Vec<SourceRange> = vec![source_range];
|
||||||
|
// We want to send the source range of the original function.
|
||||||
|
if let KclValue::Function { meta, .. } = func {
|
||||||
|
source_ranges = meta.iter().map(|m| m.source_range).collect();
|
||||||
|
};
|
||||||
|
KclError::UndefinedValue(KclErrorDetails {
|
||||||
|
message: format!("Result of user-defined function {} is undefined", fn_name),
|
||||||
|
source_ranges,
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -419,6 +440,7 @@ impl Node<CallExpression> {
|
|||||||
let fn_name = &self.callee.name;
|
let fn_name = &self.callee.name;
|
||||||
|
|
||||||
let mut fn_args: Vec<Arg> = Vec::with_capacity(self.arguments.len());
|
let mut fn_args: Vec<Arg> = Vec::with_capacity(self.arguments.len());
|
||||||
|
let mut tag_declarator_args = Vec::new();
|
||||||
|
|
||||||
for arg_expr in &self.arguments {
|
for arg_expr in &self.arguments {
|
||||||
let metadata = Metadata {
|
let metadata = Metadata {
|
||||||
@ -428,15 +450,19 @@ impl Node<CallExpression> {
|
|||||||
.execute_expr(arg_expr, exec_state, &metadata, StatementKind::Expression)
|
.execute_expr(arg_expr, exec_state, &metadata, StatementKind::Expression)
|
||||||
.await?;
|
.await?;
|
||||||
let arg = Arg::new(value, SourceRange::from(arg_expr));
|
let arg = Arg::new(value, SourceRange::from(arg_expr));
|
||||||
|
if let Expr::TagDeclarator(td) = arg_expr {
|
||||||
|
tag_declarator_args.push((td.inner.clone(), arg.source_range));
|
||||||
|
}
|
||||||
fn_args.push(arg);
|
fn_args.push(arg);
|
||||||
}
|
}
|
||||||
|
let tag_declarator_args = tag_declarator_args; // remove mutability
|
||||||
|
|
||||||
match ctx.stdlib.get_either(fn_name) {
|
match ctx.stdlib.get_either(fn_name) {
|
||||||
FunctionKind::Core(func) => {
|
FunctionKind::Core(func) => {
|
||||||
// Attempt to call the function.
|
// Attempt to call the function.
|
||||||
let args = crate::std::Args::new(fn_args, self.into(), ctx.clone());
|
let args = crate::std::Args::new(fn_args, self.into(), ctx.clone());
|
||||||
let mut result = func.std_lib_fn()(exec_state, args).await?;
|
let mut result = func.std_lib_fn()(exec_state, args).await?;
|
||||||
update_memory_for_tags_of_geometry(&mut result, exec_state)?;
|
update_memory_for_tags_of_geometry(&mut result, &tag_declarator_args, exec_state)?;
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
FunctionKind::UserDefined => {
|
FunctionKind::UserDefined => {
|
||||||
@ -475,7 +501,24 @@ impl Node<CallExpression> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut ExecState) -> Result<(), KclError> {
|
/// `tag_declarator_args` should only contain tag declarator literals, which
|
||||||
|
/// will be defined as local variables. Non-literals that evaluate to tag
|
||||||
|
/// declarators should not be defined.
|
||||||
|
fn update_memory_for_tags_of_geometry(
|
||||||
|
result: &mut KclValue,
|
||||||
|
tag_declarator_args: &[(TagDeclarator, SourceRange)],
|
||||||
|
exec_state: &mut ExecState,
|
||||||
|
) -> Result<(), KclError> {
|
||||||
|
// Define all the tags in the memory.
|
||||||
|
for (tag_declarator, arg_sr) in tag_declarator_args {
|
||||||
|
let tag = TagIdentifier {
|
||||||
|
value: tag_declarator.name.clone(),
|
||||||
|
info: None,
|
||||||
|
meta: vec![Metadata { source_range: *arg_sr }],
|
||||||
|
};
|
||||||
|
|
||||||
|
exec_state.memory.add_tag(&tag.value, tag.clone(), *arg_sr)?;
|
||||||
|
}
|
||||||
// If the return result is a sketch or solid, we want to update the
|
// If the return result is a sketch or solid, we want to update the
|
||||||
// memory for the tags of the group.
|
// memory for the tags of the group.
|
||||||
// TODO: This could probably be done in a better way, but as of now this was my only idea
|
// TODO: This could probably be done in a better way, but as of now this was my only idea
|
||||||
@ -483,7 +526,7 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
|
|||||||
match result {
|
match result {
|
||||||
KclValue::Sketch { value: ref mut sketch } => {
|
KclValue::Sketch { value: ref mut sketch } => {
|
||||||
for (_, tag) in sketch.tags.iter() {
|
for (_, tag) in sketch.tags.iter() {
|
||||||
exec_state.memory.update_tag(&tag.value, tag.clone())?;
|
exec_state.memory.update_tag_if_defined(&tag.value, tag.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KclValue::Solid(ref mut solid) => {
|
KclValue::Solid(ref mut solid) => {
|
||||||
@ -521,7 +564,7 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
|
|||||||
info.sketch = solid.id;
|
info.sketch = solid.id;
|
||||||
t.info = Some(info);
|
t.info = Some(info);
|
||||||
|
|
||||||
exec_state.memory.update_tag(&tag.name, t.clone())?;
|
exec_state.memory.update_tag_if_defined(&tag.name, t.clone());
|
||||||
|
|
||||||
// update the sketch tags.
|
// update the sketch tags.
|
||||||
solid.sketch.tags.insert(tag.name.clone(), t);
|
solid.sketch.tags.insert(tag.name.clone(), t);
|
||||||
@ -542,22 +585,6 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Node<TagDeclarator> {
|
|
||||||
pub async fn execute(&self, exec_state: &mut ExecState) -> Result<KclValue, KclError> {
|
|
||||||
let memory_item = KclValue::TagIdentifier(Box::new(TagIdentifier {
|
|
||||||
value: self.name.clone(),
|
|
||||||
info: None,
|
|
||||||
meta: vec![Metadata {
|
|
||||||
source_range: self.into(),
|
|
||||||
}],
|
|
||||||
}));
|
|
||||||
|
|
||||||
exec_state.memory.add(&self.name, memory_item.clone(), self.into())?;
|
|
||||||
|
|
||||||
Ok(self.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Node<ArrayExpression> {
|
impl Node<ArrayExpression> {
|
||||||
#[async_recursion]
|
#[async_recursion]
|
||||||
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
||||||
|
@ -72,6 +72,10 @@ pub enum KclValue {
|
|||||||
ImportedGeometry(ImportedGeometry),
|
ImportedGeometry(ImportedGeometry),
|
||||||
#[ts(skip)]
|
#[ts(skip)]
|
||||||
Function {
|
Function {
|
||||||
|
/// Adam Chalmers speculation:
|
||||||
|
/// Reference to a KCL stdlib function (written in Rust).
|
||||||
|
/// Some if the KCL value is an alias of a stdlib function,
|
||||||
|
/// None if it's a KCL function written/declared in KCL.
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
func: Option<MemoryFunction>,
|
func: Option<MemoryFunction>,
|
||||||
#[schemars(skip)]
|
#[schemars(skip)]
|
||||||
@ -503,4 +507,39 @@ impl KclValue {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If this is a function, call it by applying keyword arguments.
|
||||||
|
/// If it's not a function, returns an error.
|
||||||
|
pub async fn call_fn_kw(
|
||||||
|
&self,
|
||||||
|
args: crate::std::Args,
|
||||||
|
exec_state: &mut ExecState,
|
||||||
|
ctx: ExecutorContext,
|
||||||
|
callsite: SourceRange,
|
||||||
|
) -> Result<Option<KclValue>, KclError> {
|
||||||
|
let KclValue::Function {
|
||||||
|
func,
|
||||||
|
expression,
|
||||||
|
memory: closure_memory,
|
||||||
|
meta: _,
|
||||||
|
} = &self
|
||||||
|
else {
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: "cannot call this because it isn't a function".to_string(),
|
||||||
|
source_ranges: vec![callsite],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
if let Some(_func) = func {
|
||||||
|
todo!("Implement calling KCL stdlib fns that are aliased. Part of https://github.com/KittyCAD/modeling-app/issues/4600");
|
||||||
|
} else {
|
||||||
|
crate::execution::call_user_defined_function_kw(
|
||||||
|
args.kw_args,
|
||||||
|
closure_memory.as_ref(),
|
||||||
|
expression.as_ref(),
|
||||||
|
exec_state,
|
||||||
|
&ctx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,15 +23,18 @@ type Point3D = kcmc::shared::Point3d<f64>;
|
|||||||
pub use function_param::FunctionParam;
|
pub use function_param::FunctionParam;
|
||||||
pub use kcl_value::{KclObjectFields, KclValue};
|
pub use kcl_value::{KclObjectFields, KclValue};
|
||||||
|
|
||||||
|
pub(crate) mod cache;
|
||||||
|
mod exec_ast;
|
||||||
|
mod function_param;
|
||||||
|
mod kcl_value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
engine::{EngineManager, ExecutionKind},
|
engine::{EngineManager, ExecutionKind},
|
||||||
errors::{KclError, KclErrorDetails},
|
errors::{KclError, KclErrorDetails},
|
||||||
|
execution::cache::{CacheInformation, CacheResult},
|
||||||
fs::{FileManager, FileSystem},
|
fs::{FileManager, FileSystem},
|
||||||
parsing::ast::{
|
parsing::ast::types::{
|
||||||
cache::{get_changed_program, CacheInformation},
|
BodyItem, Expr, FunctionExpression, ImportSelector, ItemVisibility, Node, NodeRef, TagDeclarator, TagNode,
|
||||||
types::{
|
|
||||||
BodyItem, Expr, FunctionExpression, ImportSelector, ItemVisibility, Node, NodeRef, TagDeclarator, TagNode,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
settings::types::UnitLength,
|
settings::types::UnitLength,
|
||||||
source_range::{ModuleId, SourceRange},
|
source_range::{ModuleId, SourceRange},
|
||||||
@ -39,10 +42,6 @@ use crate::{
|
|||||||
ExecError, Program,
|
ExecError, Program,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod exec_ast;
|
|
||||||
mod function_param;
|
|
||||||
mod kcl_value;
|
|
||||||
|
|
||||||
/// State for executing a program.
|
/// State for executing a program.
|
||||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
@ -125,10 +124,16 @@ impl ProgramMemory {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_tag(&mut self, tag: &str, value: TagIdentifier) -> Result<(), KclError> {
|
pub fn add_tag(&mut self, tag: &str, value: TagIdentifier, source_range: SourceRange) -> Result<(), KclError> {
|
||||||
self.environments[self.current_env.index()].insert(tag.to_string(), KclValue::TagIdentifier(Box::new(value)));
|
self.add(tag, KclValue::TagIdentifier(Box::new(value)), source_range)
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
pub fn update_tag_if_defined(&mut self, tag: &str, value: TagIdentifier) {
|
||||||
|
if !self.environments[self.current_env.index()].contains_key(tag) {
|
||||||
|
// Do nothing if the tag isn't defined.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.environments[self.current_env.index()].insert(tag.to_string(), KclValue::TagIdentifier(Box::new(value)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a value from the program memory.
|
/// Get a value from the program memory.
|
||||||
@ -845,7 +850,7 @@ impl GetTangentialInfoFromPathsResult {
|
|||||||
|
|
||||||
impl Sketch {
|
impl Sketch {
|
||||||
pub(crate) fn add_tag(&mut self, tag: NodeRef<'_, TagDeclarator>, current_path: &Path) {
|
pub(crate) fn add_tag(&mut self, tag: NodeRef<'_, TagDeclarator>, current_path: &Path) {
|
||||||
let mut tag_identifier: TagIdentifier = tag.into();
|
let mut tag_identifier = TagIdentifier::from(tag);
|
||||||
let base = current_path.get_base();
|
let base = current_path.get_base();
|
||||||
tag_identifier.info = Some(TagEngineInfo {
|
tag_identifier.info = Some(TagEngineInfo {
|
||||||
id: base.geo_meta.id,
|
id: base.geo_meta.id,
|
||||||
@ -1654,17 +1659,6 @@ impl ExecutorContext {
|
|||||||
let engine: Arc<Box<dyn EngineManager>> =
|
let engine: Arc<Box<dyn EngineManager>> =
|
||||||
Arc::new(Box::new(crate::engine::conn::EngineConnection::new(ws).await?));
|
Arc::new(Box::new(crate::engine::conn::EngineConnection::new(ws).await?));
|
||||||
|
|
||||||
// Set the edge visibility.
|
|
||||||
engine
|
|
||||||
.batch_modeling_cmd(
|
|
||||||
uuid::Uuid::new_v4(),
|
|
||||||
SourceRange::default(),
|
|
||||||
&ModelingCmd::from(mcmd::EdgeLinesVisible {
|
|
||||||
hidden: !settings.highlight_edges,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
engine,
|
engine,
|
||||||
fs: Arc::new(FileManager::new()),
|
fs: Arc::new(FileManager::new()),
|
||||||
@ -1691,7 +1685,7 @@ impl ExecutorContext {
|
|||||||
pub async fn new(
|
pub async fn new(
|
||||||
engine_manager: crate::engine::conn_wasm::EngineCommandManager,
|
engine_manager: crate::engine::conn_wasm::EngineCommandManager,
|
||||||
fs_manager: crate::fs::wasm::FileSystemManager,
|
fs_manager: crate::fs::wasm::FileSystemManager,
|
||||||
units: UnitLength,
|
settings: ExecutorSettings,
|
||||||
) -> Result<Self, String> {
|
) -> Result<Self, String> {
|
||||||
Ok(ExecutorContext {
|
Ok(ExecutorContext {
|
||||||
engine: Arc::new(Box::new(
|
engine: Arc::new(Box::new(
|
||||||
@ -1701,16 +1695,16 @@ impl ExecutorContext {
|
|||||||
)),
|
)),
|
||||||
fs: Arc::new(FileManager::new(fs_manager)),
|
fs: Arc::new(FileManager::new(fs_manager)),
|
||||||
stdlib: Arc::new(StdLib::new()),
|
stdlib: Arc::new(StdLib::new()),
|
||||||
settings: ExecutorSettings {
|
settings,
|
||||||
units,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
context_type: ContextType::Live,
|
context_type: ContextType::Live,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub async fn new_mock(fs_manager: crate::fs::wasm::FileSystemManager, units: UnitLength) -> Result<Self, String> {
|
pub async fn new_mock(
|
||||||
|
fs_manager: crate::fs::wasm::FileSystemManager,
|
||||||
|
settings: ExecutorSettings,
|
||||||
|
) -> Result<Self, String> {
|
||||||
Ok(ExecutorContext {
|
Ok(ExecutorContext {
|
||||||
engine: Arc::new(Box::new(
|
engine: Arc::new(Box::new(
|
||||||
crate::engine::conn_mock::EngineConnection::new()
|
crate::engine::conn_mock::EngineConnection::new()
|
||||||
@ -1719,10 +1713,7 @@ impl ExecutorContext {
|
|||||||
)),
|
)),
|
||||||
fs: Arc::new(FileManager::new(fs_manager)),
|
fs: Arc::new(FileManager::new(fs_manager)),
|
||||||
stdlib: Arc::new(StdLib::new()),
|
stdlib: Arc::new(StdLib::new()),
|
||||||
settings: ExecutorSettings {
|
settings,
|
||||||
units,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
context_type: ContextType::Mock,
|
context_type: ContextType::Mock,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1811,6 +1802,71 @@ impl ExecutorContext {
|
|||||||
// AND if we aren't in wasm it doesn't really matter.
|
// AND if we aren't in wasm it doesn't really matter.
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
// Given an old ast, old program memory and new ast, find the parts of the code that need to be
|
||||||
|
// re-executed.
|
||||||
|
// This function should never error, because in the case of any internal error, we should just pop
|
||||||
|
// the cache.
|
||||||
|
pub async fn get_changed_program(&self, info: CacheInformation) -> Option<CacheResult> {
|
||||||
|
let Some(old) = info.old else {
|
||||||
|
// We have no old info, we need to re-execute the whole thing.
|
||||||
|
return Some(CacheResult {
|
||||||
|
clear_scene: true,
|
||||||
|
program: info.new_ast,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the settings are different we might need to bust the cache.
|
||||||
|
// We specifically do this before checking if they are the exact same.
|
||||||
|
if old.settings != self.settings {
|
||||||
|
// If the units are different we need to re-execute the whole thing.
|
||||||
|
if old.settings.units != self.settings.units {
|
||||||
|
return Some(CacheResult {
|
||||||
|
clear_scene: true,
|
||||||
|
program: info.new_ast,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If anything else is different we do not need to re-execute, but rather just
|
||||||
|
// run the settings again.
|
||||||
|
|
||||||
|
if self
|
||||||
|
.engine
|
||||||
|
.reapply_settings(&self.settings, Default::default())
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
// Bust the cache, we errored.
|
||||||
|
return Some(CacheResult {
|
||||||
|
clear_scene: true,
|
||||||
|
program: info.new_ast,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the ASTs are the EXACT same we return None.
|
||||||
|
// We don't even need to waste time computing the digests.
|
||||||
|
if old.ast == info.new_ast {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut old_ast = old.ast.inner;
|
||||||
|
old_ast.compute_digest();
|
||||||
|
let mut new_ast = info.new_ast.inner.clone();
|
||||||
|
new_ast.compute_digest();
|
||||||
|
|
||||||
|
// Check if the digest is the same.
|
||||||
|
if old_ast.digest == new_ast.digest {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the changes were only to Non-code areas, like comments or whitespace.
|
||||||
|
|
||||||
|
// For any unhandled cases just re-execute the whole thing.
|
||||||
|
Some(CacheResult {
|
||||||
|
clear_scene: true,
|
||||||
|
program: info.new_ast,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Perform the execution of a program.
|
/// Perform the execution of a program.
|
||||||
/// You can optionally pass in some initialization memory.
|
/// You can optionally pass in some initialization memory.
|
||||||
@ -1831,7 +1887,7 @@ impl ExecutorContext {
|
|||||||
let _stats = crate::log::LogPerfStats::new("Interpretation");
|
let _stats = crate::log::LogPerfStats::new("Interpretation");
|
||||||
|
|
||||||
// Get the program that actually changed from the old and new information.
|
// Get the program that actually changed from the old and new information.
|
||||||
let cache_result = get_changed_program(cache_info.clone(), &self.settings);
|
let cache_result = self.get_changed_program(cache_info.clone()).await;
|
||||||
|
|
||||||
// Check if we don't need to re-execute.
|
// Check if we don't need to re-execute.
|
||||||
let Some(cache_result) = cache_result else {
|
let Some(cache_result) = cache_result else {
|
||||||
@ -1848,23 +1904,9 @@ impl ExecutorContext {
|
|||||||
|
|
||||||
// TODO: Use the top-level file's path.
|
// TODO: Use the top-level file's path.
|
||||||
exec_state.add_module(std::path::PathBuf::from(""));
|
exec_state.add_module(std::path::PathBuf::from(""));
|
||||||
// Before we even start executing the program, set the units.
|
|
||||||
self.engine
|
// Re-apply the settings, in case the cache was busted.
|
||||||
.batch_modeling_cmd(
|
self.engine.reapply_settings(&self.settings, Default::default()).await?;
|
||||||
exec_state.id_generator.next_uuid(),
|
|
||||||
SourceRange::default(),
|
|
||||||
&ModelingCmd::from(mcmd::SetSceneUnits {
|
|
||||||
unit: match self.settings.units {
|
|
||||||
UnitLength::Cm => kcmc::units::UnitLength::Centimeters,
|
|
||||||
UnitLength::Ft => kcmc::units::UnitLength::Feet,
|
|
||||||
UnitLength::In => kcmc::units::UnitLength::Inches,
|
|
||||||
UnitLength::M => kcmc::units::UnitLength::Meters,
|
|
||||||
UnitLength::Mm => kcmc::units::UnitLength::Millimeters,
|
|
||||||
UnitLength::Yd => kcmc::units::UnitLength::Yards,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
self.inner_execute(&cache_result.program, exec_state, crate::execution::BodyType::Root)
|
self.inner_execute(&cache_result.program, exec_state, crate::execution::BodyType::Root)
|
||||||
.await?;
|
.await?;
|
||||||
@ -2075,7 +2117,8 @@ impl ExecutorContext {
|
|||||||
Ok((module_memory, module_exports))
|
Ok((module_memory, module_exports))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn execute_expr<'a>(
|
#[async_recursion]
|
||||||
|
pub async fn execute_expr<'a: 'async_recursion>(
|
||||||
&self,
|
&self,
|
||||||
init: &Expr,
|
init: &Expr,
|
||||||
exec_state: &mut ExecState,
|
exec_state: &mut ExecState,
|
||||||
@ -2085,7 +2128,7 @@ impl ExecutorContext {
|
|||||||
let item = match init {
|
let item = match init {
|
||||||
Expr::None(none) => KclValue::from(none),
|
Expr::None(none) => KclValue::from(none),
|
||||||
Expr::Literal(literal) => KclValue::from(literal),
|
Expr::Literal(literal) => KclValue::from(literal),
|
||||||
Expr::TagDeclarator(tag) => tag.execute(exec_state).await?,
|
Expr::TagDeclarator(tag) => KclValue::from(tag),
|
||||||
Expr::Identifier(identifier) => {
|
Expr::Identifier(identifier) => {
|
||||||
let value = exec_state.memory.get(&identifier.name, identifier.into())?;
|
let value = exec_state.memory.get(&identifier.name, identifier.into())?;
|
||||||
value.clone()
|
value.clone()
|
||||||
@ -2132,6 +2175,14 @@ impl ExecutorContext {
|
|||||||
Expr::MemberExpression(member_expression) => member_expression.get_result(exec_state)?,
|
Expr::MemberExpression(member_expression) => member_expression.get_result(exec_state)?,
|
||||||
Expr::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, self).await?,
|
Expr::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, self).await?,
|
||||||
Expr::IfExpression(expr) => expr.get_result(exec_state, self).await?,
|
Expr::IfExpression(expr) => expr.get_result(exec_state, self).await?,
|
||||||
|
Expr::LabelledExpression(expr) => {
|
||||||
|
let result = self
|
||||||
|
.execute_expr(&expr.expr, exec_state, metadata, statement_kind)
|
||||||
|
.await?;
|
||||||
|
exec_state.memory.add(&expr.label.name, result.clone(), init.into())?;
|
||||||
|
// TODO this lets us use the label as a variable name, but not as a tag in most cases
|
||||||
|
result
|
||||||
|
}
|
||||||
};
|
};
|
||||||
Ok(item)
|
Ok(item)
|
||||||
}
|
}
|
||||||
@ -2141,23 +2192,8 @@ impl ExecutorContext {
|
|||||||
self.settings.units = units;
|
self.settings.units = units;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute the program, then get a PNG screenshot.
|
/// Get a snapshot of the current scene.
|
||||||
pub async fn execute_and_prepare_snapshot(
|
pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
|
||||||
&self,
|
|
||||||
program: &Program,
|
|
||||||
exec_state: &mut ExecState,
|
|
||||||
) -> std::result::Result<TakeSnapshot, ExecError> {
|
|
||||||
self.execute_and_prepare(program, exec_state).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Execute the program, return the interpreter and outputs.
|
|
||||||
pub async fn execute_and_prepare(
|
|
||||||
&self,
|
|
||||||
program: &Program,
|
|
||||||
exec_state: &mut ExecState,
|
|
||||||
) -> std::result::Result<TakeSnapshot, ExecError> {
|
|
||||||
self.run(program.clone().into(), exec_state).await?;
|
|
||||||
|
|
||||||
// Zoom to fit.
|
// Zoom to fit.
|
||||||
self.engine
|
self.engine
|
||||||
.send_modeling_cmd(
|
.send_modeling_cmd(
|
||||||
@ -2193,6 +2229,17 @@ impl ExecutorContext {
|
|||||||
};
|
};
|
||||||
Ok(contents)
|
Ok(contents)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Execute the program, then get a PNG screenshot.
|
||||||
|
pub async fn execute_and_prepare_snapshot(
|
||||||
|
&self,
|
||||||
|
program: &Program,
|
||||||
|
exec_state: &mut ExecState,
|
||||||
|
) -> std::result::Result<TakeSnapshot, ExecError> {
|
||||||
|
self.run(program.clone().into(), exec_state).await?;
|
||||||
|
|
||||||
|
self.prepare_snapshot().await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For each argument given,
|
/// For each argument given,
|
||||||
@ -2247,6 +2294,59 @@ fn assign_args_to_params(
|
|||||||
Ok(fn_memory)
|
Ok(fn_memory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn assign_args_to_params_kw(
|
||||||
|
function_expression: NodeRef<'_, FunctionExpression>,
|
||||||
|
mut args: crate::std::args::KwArgs,
|
||||||
|
mut fn_memory: ProgramMemory,
|
||||||
|
) -> Result<ProgramMemory, KclError> {
|
||||||
|
// Add the arguments to the memory. A new call frame should have already
|
||||||
|
// been created.
|
||||||
|
let source_ranges = vec![function_expression.into()];
|
||||||
|
for param in function_expression.params.iter() {
|
||||||
|
if param.labeled {
|
||||||
|
let arg = args.labeled.get(¶m.identifier.name);
|
||||||
|
let arg_val = match arg {
|
||||||
|
Some(arg) => arg.value.clone(),
|
||||||
|
None => match param.default_value {
|
||||||
|
Some(ref default_val) => KclValue::from(default_val.clone()),
|
||||||
|
None => {
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
source_ranges,
|
||||||
|
message: format!(
|
||||||
|
"This function requires a parameter {}, but you haven't passed it one.",
|
||||||
|
param.identifier.name
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
fn_memory.add(¶m.identifier.name, arg_val, (¶m.identifier).into())?;
|
||||||
|
} else {
|
||||||
|
let Some(unlabeled) = args.unlabeled.take() else {
|
||||||
|
let param_name = ¶m.identifier.name;
|
||||||
|
return Err(if args.labeled.contains_key(param_name) {
|
||||||
|
KclError::Semantic(KclErrorDetails {
|
||||||
|
source_ranges,
|
||||||
|
message: format!("The function does declare a parameter named '{param_name}', but this parameter doesn't use a label. Try removing the `{param_name}:`"),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
KclError::Semantic(KclErrorDetails {
|
||||||
|
source_ranges,
|
||||||
|
message: "This function expects an unlabeled first parameter, but you haven't passed it one."
|
||||||
|
.to_owned(),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
fn_memory.add(
|
||||||
|
¶m.identifier.name,
|
||||||
|
unlabeled.value.clone(),
|
||||||
|
(¶m.identifier).into(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(fn_memory)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn call_user_defined_function(
|
pub(crate) async fn call_user_defined_function(
|
||||||
args: Vec<Arg>,
|
args: Vec<Arg>,
|
||||||
memory: &ProgramMemory,
|
memory: &ProgramMemory,
|
||||||
@ -2277,6 +2377,36 @@ pub(crate) async fn call_user_defined_function(
|
|||||||
result.map(|_| fn_memory.return_)
|
result.map(|_| fn_memory.return_)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn call_user_defined_function_kw(
|
||||||
|
args: crate::std::args::KwArgs,
|
||||||
|
memory: &ProgramMemory,
|
||||||
|
function_expression: NodeRef<'_, FunctionExpression>,
|
||||||
|
exec_state: &mut ExecState,
|
||||||
|
ctx: &ExecutorContext,
|
||||||
|
) -> Result<Option<KclValue>, KclError> {
|
||||||
|
// Create a new environment to execute the function body in so that local
|
||||||
|
// variables shadow variables in the parent scope. The new environment's
|
||||||
|
// parent should be the environment of the closure.
|
||||||
|
let mut body_memory = memory.clone();
|
||||||
|
let body_env = body_memory.new_env_for_call(memory.current_env);
|
||||||
|
body_memory.current_env = body_env;
|
||||||
|
let fn_memory = assign_args_to_params_kw(function_expression, args, body_memory)?;
|
||||||
|
|
||||||
|
// Execute the function body using the memory we just created.
|
||||||
|
let (result, fn_memory) = {
|
||||||
|
let previous_memory = std::mem::replace(&mut exec_state.memory, fn_memory);
|
||||||
|
let result = ctx
|
||||||
|
.inner_execute(&function_expression.body, exec_state, BodyType::Block)
|
||||||
|
.await;
|
||||||
|
// Restore the previous memory.
|
||||||
|
let fn_memory = std::mem::replace(&mut exec_state.memory, previous_memory);
|
||||||
|
|
||||||
|
(result, fn_memory)
|
||||||
|
};
|
||||||
|
|
||||||
|
result.map(|_| fn_memory.return_)
|
||||||
|
}
|
||||||
|
|
||||||
pub enum StatementKind<'a> {
|
pub enum StatementKind<'a> {
|
||||||
Declaration { name: &'a str },
|
Declaration { name: &'a str },
|
||||||
Expression,
|
Expression,
|
||||||
@ -2289,9 +2419,12 @@ mod tests {
|
|||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::parsing::ast::types::{DefaultParamVal, Identifier, Node, Parameter};
|
use crate::{
|
||||||
|
parsing::ast::types::{DefaultParamVal, Identifier, Node, Parameter},
|
||||||
|
OldAstState,
|
||||||
|
};
|
||||||
|
|
||||||
pub async fn parse_execute(code: &str) -> Result<ProgramMemory> {
|
pub async fn parse_execute(code: &str) -> Result<(Program, ExecutorContext, ExecState)> {
|
||||||
let program = Program::parse_no_errs(code)?;
|
let program = Program::parse_no_errs(code)?;
|
||||||
|
|
||||||
let ctx = ExecutorContext {
|
let ctx = ExecutorContext {
|
||||||
@ -2302,9 +2435,9 @@ mod tests {
|
|||||||
context_type: ContextType::Mock,
|
context_type: ContextType::Mock,
|
||||||
};
|
};
|
||||||
let mut exec_state = ExecState::default();
|
let mut exec_state = ExecState::default();
|
||||||
ctx.run(program.into(), &mut exec_state).await?;
|
ctx.run(program.clone().into(), &mut exec_state).await?;
|
||||||
|
|
||||||
Ok(exec_state.memory)
|
Ok((program, ctx, exec_state))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience function to get a JSON value from memory and unwrap.
|
/// Convenience function to get a JSON value from memory and unwrap.
|
||||||
@ -2715,36 +2848,39 @@ let shape = layer() |> patternTransform(10, transform, %)
|
|||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_math_execute_with_functions() {
|
async fn test_math_execute_with_functions() {
|
||||||
let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#;
|
let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#;
|
||||||
let memory = parse_execute(ast).await.unwrap();
|
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||||
assert_eq!(5.0, mem_get_json(&memory, "myVar").as_f64().unwrap());
|
assert_eq!(5.0, mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_math_execute() {
|
async fn test_math_execute() {
|
||||||
let ast = r#"const myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
|
let ast = r#"const myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
|
||||||
let memory = parse_execute(ast).await.unwrap();
|
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||||
assert_eq!(7.4, mem_get_json(&memory, "myVar").as_f64().unwrap());
|
assert_eq!(7.4, mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_math_execute_start_negative() {
|
async fn test_math_execute_start_negative() {
|
||||||
let ast = r#"const myVar = -5 + 6"#;
|
let ast = r#"const myVar = -5 + 6"#;
|
||||||
let memory = parse_execute(ast).await.unwrap();
|
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||||
assert_eq!(1.0, mem_get_json(&memory, "myVar").as_f64().unwrap());
|
assert_eq!(1.0, mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_math_execute_with_pi() {
|
async fn test_math_execute_with_pi() {
|
||||||
let ast = r#"const myVar = pi() * 2"#;
|
let ast = r#"const myVar = pi() * 2"#;
|
||||||
let memory = parse_execute(ast).await.unwrap();
|
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||||
assert_eq!(std::f64::consts::TAU, mem_get_json(&memory, "myVar").as_f64().unwrap());
|
assert_eq!(
|
||||||
|
std::f64::consts::TAU,
|
||||||
|
mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_math_define_decimal_without_leading_zero() {
|
async fn test_math_define_decimal_without_leading_zero() {
|
||||||
let ast = r#"let thing = .4 + 7"#;
|
let ast = r#"let thing = .4 + 7"#;
|
||||||
let memory = parse_execute(ast).await.unwrap();
|
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||||
assert_eq!(7.4, mem_get_json(&memory, "thing").as_f64().unwrap());
|
assert_eq!(7.4, mem_get_json(&exec_state.memory, "thing").as_f64().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
@ -2783,11 +2919,11 @@ fn check = (x) => {
|
|||||||
}
|
}
|
||||||
check(false)
|
check(false)
|
||||||
"#;
|
"#;
|
||||||
let mem = parse_execute(ast).await.unwrap();
|
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||||
assert_eq!(false, mem_get_json(&mem, "notTrue").as_bool().unwrap());
|
assert_eq!(false, mem_get_json(&exec_state.memory, "notTrue").as_bool().unwrap());
|
||||||
assert_eq!(true, mem_get_json(&mem, "notFalse").as_bool().unwrap());
|
assert_eq!(true, mem_get_json(&exec_state.memory, "notFalse").as_bool().unwrap());
|
||||||
assert_eq!(true, mem_get_json(&mem, "c").as_bool().unwrap());
|
assert_eq!(true, mem_get_json(&exec_state.memory, "c").as_bool().unwrap());
|
||||||
assert_eq!(false, mem_get_json(&mem, "d").as_bool().unwrap());
|
assert_eq!(false, mem_get_json(&exec_state.memory, "d").as_bool().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
@ -2888,8 +3024,10 @@ let notTagDeclarator = !myTagDeclarator";
|
|||||||
);
|
);
|
||||||
|
|
||||||
let code9 = "
|
let code9 = "
|
||||||
let myTagDeclarator = $myTag
|
sk = startSketchOn('XY')
|
||||||
let notTagIdentifier = !myTag";
|
|> startProfileAt([0, 0], %)
|
||||||
|
|> line([5, 0], %, $myTag)
|
||||||
|
notTagIdentifier = !myTag";
|
||||||
let tag_identifier_err = parse_execute(code9).await.unwrap_err().downcast::<KclError>().unwrap();
|
let tag_identifier_err = parse_execute(code9).await.unwrap_err().downcast::<KclError>().unwrap();
|
||||||
// These are currently printed out as JSON objects, so we don't want to
|
// These are currently printed out as JSON objects, so we don't want to
|
||||||
// check the full error.
|
// check the full error.
|
||||||
@ -3167,4 +3305,310 @@ let w = f() + f()
|
|||||||
let json = serde_json::to_string(&mem).unwrap();
|
let json = serde_json::to_string(&mem).unwrap();
|
||||||
assert_eq!(json, r#"{"type":"Solids","value":[]}"#);
|
assert_eq!(json, r#"{"type":"Solids","value":[]}"#);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Easy case where we have no old ast and memory.
|
||||||
|
// We need to re-execute everything.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn test_get_changed_program_no_old_information() {
|
||||||
|
let new = r#"// Remove the end face for the extrusion.
|
||||||
|
firstSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-12, 12], %)
|
||||||
|
|> line([24, 0], %)
|
||||||
|
|> line([0, -24], %)
|
||||||
|
|> line([-24, 0], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(6, %)
|
||||||
|
|
||||||
|
// Remove the end face for the extrusion.
|
||||||
|
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||||
|
let (program, ctx, _) = parse_execute(new).await.unwrap();
|
||||||
|
|
||||||
|
let result = ctx
|
||||||
|
.get_changed_program(CacheInformation {
|
||||||
|
old: None,
|
||||||
|
new_ast: program.ast.clone(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_some());
|
||||||
|
|
||||||
|
let result = result.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.program, program.ast);
|
||||||
|
assert!(result.clear_scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn test_get_changed_program_same_code() {
|
||||||
|
let new = r#"// Remove the end face for the extrusion.
|
||||||
|
firstSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-12, 12], %)
|
||||||
|
|> line([24, 0], %)
|
||||||
|
|> line([0, -24], %)
|
||||||
|
|> line([-24, 0], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(6, %)
|
||||||
|
|
||||||
|
// Remove the end face for the extrusion.
|
||||||
|
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||||
|
|
||||||
|
let (program, ctx, exec_state) = parse_execute(new).await.unwrap();
|
||||||
|
|
||||||
|
let result = ctx
|
||||||
|
.get_changed_program(CacheInformation {
|
||||||
|
old: Some(OldAstState {
|
||||||
|
ast: program.ast.clone(),
|
||||||
|
exec_state,
|
||||||
|
settings: Default::default(),
|
||||||
|
}),
|
||||||
|
new_ast: program.ast.clone(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(result, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn test_get_changed_program_same_code_changed_whitespace() {
|
||||||
|
let old = r#" // Remove the end face for the extrusion.
|
||||||
|
firstSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-12, 12], %)
|
||||||
|
|> line([24, 0], %)
|
||||||
|
|> line([0, -24], %)
|
||||||
|
|> line([-24, 0], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(6, %)
|
||||||
|
|
||||||
|
// Remove the end face for the extrusion.
|
||||||
|
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
|
||||||
|
|
||||||
|
let new = r#"// Remove the end face for the extrusion.
|
||||||
|
firstSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-12, 12], %)
|
||||||
|
|> line([24, 0], %)
|
||||||
|
|> line([0, -24], %)
|
||||||
|
|> line([-24, 0], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(6, %)
|
||||||
|
|
||||||
|
// Remove the end face for the extrusion.
|
||||||
|
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||||
|
|
||||||
|
let (program_old, ctx, exec_state) = parse_execute(old).await.unwrap();
|
||||||
|
|
||||||
|
let program_new = crate::Program::parse_no_errs(new).unwrap();
|
||||||
|
|
||||||
|
let result = ctx
|
||||||
|
.get_changed_program(CacheInformation {
|
||||||
|
old: Some(OldAstState {
|
||||||
|
ast: program_old.ast.clone(),
|
||||||
|
exec_state,
|
||||||
|
settings: Default::default(),
|
||||||
|
}),
|
||||||
|
new_ast: program_new.ast.clone(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(result, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
|
||||||
|
let old = r#" // Removed the end face for the extrusion.
|
||||||
|
firstSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-12, 12], %)
|
||||||
|
|> line([24, 0], %)
|
||||||
|
|> line([0, -24], %)
|
||||||
|
|> line([-24, 0], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(6, %)
|
||||||
|
|
||||||
|
// Remove the end face for the extrusion.
|
||||||
|
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
|
||||||
|
|
||||||
|
let new = r#"// Remove the end face for the extrusion.
|
||||||
|
firstSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-12, 12], %)
|
||||||
|
|> line([24, 0], %)
|
||||||
|
|> line([0, -24], %)
|
||||||
|
|> line([-24, 0], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(6, %)
|
||||||
|
|
||||||
|
// Remove the end face for the extrusion.
|
||||||
|
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||||
|
|
||||||
|
let (program, ctx, exec_state) = parse_execute(old).await.unwrap();
|
||||||
|
|
||||||
|
let program_new = crate::Program::parse_no_errs(new).unwrap();
|
||||||
|
|
||||||
|
let result = ctx
|
||||||
|
.get_changed_program(CacheInformation {
|
||||||
|
old: Some(OldAstState {
|
||||||
|
ast: program.ast.clone(),
|
||||||
|
exec_state,
|
||||||
|
settings: Default::default(),
|
||||||
|
}),
|
||||||
|
new_ast: program_new.ast.clone(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(result, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn test_get_changed_program_same_code_changed_code_comments() {
|
||||||
|
let old = r#" // Removed the end face for the extrusion.
|
||||||
|
firstSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-12, 12], %)
|
||||||
|
|> line([24, 0], %)
|
||||||
|
|> line([0, -24], %)
|
||||||
|
|> line([-24, 0], %) // my thing
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(6, %)
|
||||||
|
|
||||||
|
// Remove the end face for the extrusion.
|
||||||
|
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
|
||||||
|
|
||||||
|
let new = r#"// Remove the end face for the extrusion.
|
||||||
|
firstSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-12, 12], %)
|
||||||
|
|> line([24, 0], %)
|
||||||
|
|> line([0, -24], %)
|
||||||
|
|> line([-24, 0], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(6, %)
|
||||||
|
|
||||||
|
// Remove the end face for the extrusion.
|
||||||
|
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||||
|
|
||||||
|
let (program, ctx, exec_state) = parse_execute(old).await.unwrap();
|
||||||
|
|
||||||
|
let program_new = crate::Program::parse_no_errs(new).unwrap();
|
||||||
|
|
||||||
|
let result = ctx
|
||||||
|
.get_changed_program(CacheInformation {
|
||||||
|
old: Some(OldAstState {
|
||||||
|
ast: program.ast.clone(),
|
||||||
|
exec_state,
|
||||||
|
settings: Default::default(),
|
||||||
|
}),
|
||||||
|
new_ast: program_new.ast.clone(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_some());
|
||||||
|
|
||||||
|
let result = result.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.program, program_new.ast);
|
||||||
|
assert!(result.clear_scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changing the units with the exact same file should bust the cache.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn test_get_changed_program_same_code_but_different_units() {
|
||||||
|
let new = r#"// Remove the end face for the extrusion.
|
||||||
|
firstSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-12, 12], %)
|
||||||
|
|> line([24, 0], %)
|
||||||
|
|> line([0, -24], %)
|
||||||
|
|> line([-24, 0], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(6, %)
|
||||||
|
|
||||||
|
// Remove the end face for the extrusion.
|
||||||
|
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||||
|
|
||||||
|
let (program, mut ctx, exec_state) = parse_execute(new).await.unwrap();
|
||||||
|
|
||||||
|
// Change the settings to cm.
|
||||||
|
ctx.settings.units = crate::UnitLength::Cm;
|
||||||
|
|
||||||
|
let result = ctx
|
||||||
|
.get_changed_program(CacheInformation {
|
||||||
|
old: Some(OldAstState {
|
||||||
|
ast: program.ast.clone(),
|
||||||
|
exec_state,
|
||||||
|
settings: Default::default(),
|
||||||
|
}),
|
||||||
|
new_ast: program.ast.clone(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_some());
|
||||||
|
|
||||||
|
let result = result.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.program, program.ast);
|
||||||
|
assert!(result.clear_scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changing the grid settings with the exact same file should NOT bust the cache.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn test_get_changed_program_same_code_but_different_grid_setting() {
|
||||||
|
let new = r#"// Remove the end face for the extrusion.
|
||||||
|
firstSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-12, 12], %)
|
||||||
|
|> line([24, 0], %)
|
||||||
|
|> line([0, -24], %)
|
||||||
|
|> line([-24, 0], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(6, %)
|
||||||
|
|
||||||
|
// Remove the end face for the extrusion.
|
||||||
|
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||||
|
|
||||||
|
let (program, mut ctx, exec_state) = parse_execute(new).await.unwrap();
|
||||||
|
|
||||||
|
// Change the settings.
|
||||||
|
ctx.settings.show_grid = !ctx.settings.show_grid;
|
||||||
|
|
||||||
|
let result = ctx
|
||||||
|
.get_changed_program(CacheInformation {
|
||||||
|
old: Some(OldAstState {
|
||||||
|
ast: program.ast.clone(),
|
||||||
|
exec_state,
|
||||||
|
settings: Default::default(),
|
||||||
|
}),
|
||||||
|
new_ast: program.ast.clone(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(result, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changing the edge visibility settings with the exact same file should NOT bust the cache.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn test_get_changed_program_same_code_but_different_edge_visiblity_setting() {
|
||||||
|
let new = r#"// Remove the end face for the extrusion.
|
||||||
|
firstSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-12, 12], %)
|
||||||
|
|> line([24, 0], %)
|
||||||
|
|> line([0, -24], %)
|
||||||
|
|> line([-24, 0], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(6, %)
|
||||||
|
|
||||||
|
// Remove the end face for the extrusion.
|
||||||
|
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||||
|
|
||||||
|
let (program, mut ctx, exec_state) = parse_execute(new).await.unwrap();
|
||||||
|
|
||||||
|
// Change the settings.
|
||||||
|
ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
|
||||||
|
|
||||||
|
let result = ctx
|
||||||
|
.get_changed_program(CacheInformation {
|
||||||
|
old: Some(OldAstState {
|
||||||
|
ast: program.ast.clone(),
|
||||||
|
exec_state,
|
||||||
|
settings: Default::default(),
|
||||||
|
}),
|
||||||
|
new_ast: program.ast.clone(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(result, None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,16 +82,15 @@ mod wasm;
|
|||||||
pub use coredump::CoreDump;
|
pub use coredump::CoreDump;
|
||||||
pub use engine::{EngineManager, ExecutionKind};
|
pub use engine::{EngineManager, ExecutionKind};
|
||||||
pub use errors::{CompilationError, ConnectionError, ExecError, KclError};
|
pub use errors::{CompilationError, ConnectionError, ExecError, KclError};
|
||||||
pub use execution::{ExecState, ExecutorContext, ExecutorSettings};
|
pub use execution::{
|
||||||
|
cache::{CacheInformation, OldAstState},
|
||||||
|
ExecState, ExecutorContext, ExecutorSettings,
|
||||||
|
};
|
||||||
pub use lsp::{
|
pub use lsp::{
|
||||||
copilot::Backend as CopilotLspBackend,
|
copilot::Backend as CopilotLspBackend,
|
||||||
kcl::{Backend as KclLspBackend, Server as KclLspServerSubCommand},
|
kcl::{Backend as KclLspBackend, Server as KclLspServerSubCommand},
|
||||||
};
|
};
|
||||||
pub use parsing::ast::{
|
pub use parsing::ast::{modify::modify_ast_for_sketch, types::FormatOptions};
|
||||||
cache::{CacheInformation, OldAstState},
|
|
||||||
modify::modify_ast_for_sketch,
|
|
||||||
types::FormatOptions,
|
|
||||||
};
|
|
||||||
pub use settings::types::{project::ProjectConfiguration, Configuration, UnitLength};
|
pub use settings::types::{project::ProjectConfiguration, Configuration, UnitLength};
|
||||||
pub use source_range::{ModuleId, SourceRange};
|
pub use source_range::{ModuleId, SourceRange};
|
||||||
|
|
||||||
@ -138,7 +137,7 @@ pub use lsp::test_util::kcl_lsp_server;
|
|||||||
impl Program {
|
impl Program {
|
||||||
pub fn parse(input: &str) -> Result<(Option<Program>, Vec<CompilationError>), KclError> {
|
pub fn parse(input: &str) -> Result<(Option<Program>, Vec<CompilationError>), KclError> {
|
||||||
let module_id = ModuleId::default();
|
let module_id = ModuleId::default();
|
||||||
let tokens = parsing::token::lexer(input, module_id)?;
|
let tokens = parsing::token::lex(input, module_id)?;
|
||||||
let (ast, errs) = parsing::parse_tokens(tokens).0?;
|
let (ast, errs) = parsing::parse_tokens(tokens).0?;
|
||||||
|
|
||||||
Ok((ast.map(|ast| Program { ast }), errs))
|
Ok((ast.map(|ast| Program { ast }), errs))
|
||||||
@ -146,7 +145,7 @@ impl Program {
|
|||||||
|
|
||||||
pub fn parse_no_errs(input: &str) -> Result<Program, KclError> {
|
pub fn parse_no_errs(input: &str) -> Result<Program, KclError> {
|
||||||
let module_id = ModuleId::default();
|
let module_id = ModuleId::default();
|
||||||
let tokens = parsing::token::lexer(input, module_id)?;
|
let tokens = parsing::token::lex(input, module_id)?;
|
||||||
let ast = parsing::parse_tokens(tokens).parse_errs_as_err()?;
|
let ast = parsing::parse_tokens(tokens).parse_errs_as_err()?;
|
||||||
|
|
||||||
Ok(Program { ast })
|
Ok(Program { ast })
|
||||||
|
@ -45,38 +45,32 @@ use crate::{
|
|||||||
errors::Suggestion,
|
errors::Suggestion,
|
||||||
lsp::{backend::Backend as _, util::IntoDiagnostic},
|
lsp::{backend::Backend as _, util::IntoDiagnostic},
|
||||||
parsing::{
|
parsing::{
|
||||||
ast::{
|
ast::types::{Expr, Node, VariableKind},
|
||||||
cache::{CacheInformation, OldAstState},
|
token::TokenStream,
|
||||||
types::{Expr, Node, VariableKind},
|
|
||||||
},
|
|
||||||
token::TokenType,
|
|
||||||
PIPE_OPERATOR,
|
PIPE_OPERATOR,
|
||||||
},
|
},
|
||||||
ModuleId, Program, SourceRange,
|
CacheInformation, ModuleId, OldAstState, Program, SourceRange,
|
||||||
};
|
};
|
||||||
|
const SEMANTIC_TOKEN_TYPES: [SemanticTokenType; 10] = [
|
||||||
|
SemanticTokenType::NUMBER,
|
||||||
|
SemanticTokenType::VARIABLE,
|
||||||
|
SemanticTokenType::KEYWORD,
|
||||||
|
SemanticTokenType::TYPE,
|
||||||
|
SemanticTokenType::STRING,
|
||||||
|
SemanticTokenType::OPERATOR,
|
||||||
|
SemanticTokenType::COMMENT,
|
||||||
|
SemanticTokenType::FUNCTION,
|
||||||
|
SemanticTokenType::PARAMETER,
|
||||||
|
SemanticTokenType::PROPERTY,
|
||||||
|
];
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
const SEMANTIC_TOKEN_MODIFIERS: [SemanticTokenModifier; 5] = [
|
||||||
pub static ref SEMANTIC_TOKEN_TYPES: Vec<SemanticTokenType> = {
|
SemanticTokenModifier::DECLARATION,
|
||||||
// This is safe to unwrap because we know all the token types are valid.
|
SemanticTokenModifier::DEFINITION,
|
||||||
// And the test would fail if they were not.
|
SemanticTokenModifier::DEFAULT_LIBRARY,
|
||||||
let mut gen = TokenType::all_semantic_token_types().unwrap();
|
SemanticTokenModifier::READONLY,
|
||||||
gen.extend(vec![
|
SemanticTokenModifier::STATIC,
|
||||||
SemanticTokenType::PARAMETER,
|
];
|
||||||
SemanticTokenType::PROPERTY,
|
|
||||||
]);
|
|
||||||
gen
|
|
||||||
};
|
|
||||||
|
|
||||||
pub static ref SEMANTIC_TOKEN_MODIFIERS: Vec<SemanticTokenModifier> = {
|
|
||||||
vec![
|
|
||||||
SemanticTokenModifier::DECLARATION,
|
|
||||||
SemanticTokenModifier::DEFINITION,
|
|
||||||
SemanticTokenModifier::DEFAULT_LIBRARY,
|
|
||||||
SemanticTokenModifier::READONLY,
|
|
||||||
SemanticTokenModifier::STATIC,
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A subcommand for running the server.
|
/// A subcommand for running the server.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@ -105,7 +99,7 @@ pub struct Backend {
|
|||||||
/// The stdlib signatures for the language.
|
/// The stdlib signatures for the language.
|
||||||
pub stdlib_signatures: HashMap<String, SignatureHelp>,
|
pub stdlib_signatures: HashMap<String, SignatureHelp>,
|
||||||
/// Token maps.
|
/// Token maps.
|
||||||
pub token_map: DashMap<String, Vec<crate::parsing::token::Token>>,
|
pub(super) token_map: DashMap<String, TokenStream>,
|
||||||
/// AST maps.
|
/// AST maps.
|
||||||
pub ast_map: DashMap<String, Node<crate::parsing::ast::types::Program>>,
|
pub ast_map: DashMap<String, Node<crate::parsing::ast::types::Program>>,
|
||||||
/// Last successful execution.
|
/// Last successful execution.
|
||||||
@ -284,7 +278,7 @@ impl crate::lsp::backend::Backend for Backend {
|
|||||||
|
|
||||||
// Lets update the tokens.
|
// Lets update the tokens.
|
||||||
let module_id = ModuleId::default();
|
let module_id = ModuleId::default();
|
||||||
let tokens = match crate::parsing::token::lexer(¶ms.text, module_id) {
|
let tokens = match crate::parsing::token::lex(¶ms.text, module_id) {
|
||||||
Ok(tokens) => tokens,
|
Ok(tokens) => tokens,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.add_to_diagnostics(¶ms, &[err], true).await;
|
self.add_to_diagnostics(¶ms, &[err], true).await;
|
||||||
@ -410,11 +404,11 @@ impl Backend {
|
|||||||
self.executor_ctx.read().await
|
self.executor_ctx.read().await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_semantic_tokens(&self, tokens: &[crate::parsing::token::Token], params: &TextDocumentItem) {
|
async fn update_semantic_tokens(&self, tokens: &TokenStream, params: &TextDocumentItem) {
|
||||||
// Update the semantic tokens map.
|
// Update the semantic tokens map.
|
||||||
let mut semantic_tokens = vec![];
|
let mut semantic_tokens = vec![];
|
||||||
let mut last_position = Position::new(0, 0);
|
let mut last_position = Position::new(0, 0);
|
||||||
for token in tokens {
|
for token in tokens.as_slice() {
|
||||||
let Ok(token_type) = SemanticTokenType::try_from(token.token_type) else {
|
let Ok(token_type) = SemanticTokenType::try_from(token.token_type) else {
|
||||||
// We continue here because not all tokens can be converted this way, we will get
|
// We continue here because not all tokens can be converted this way, we will get
|
||||||
// the rest from the ast.
|
// the rest from the ast.
|
||||||
@ -444,8 +438,11 @@ impl Backend {
|
|||||||
let token_modifiers_bitset = if let Some(ast) = self.ast_map.get(params.uri.as_str()) {
|
let token_modifiers_bitset = if let Some(ast) = self.ast_map.get(params.uri.as_str()) {
|
||||||
let token_index = Arc::new(Mutex::new(token_type_index));
|
let token_index = Arc::new(Mutex::new(token_type_index));
|
||||||
let modifier_index: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));
|
let modifier_index: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));
|
||||||
crate::walk::walk(&ast, &|node: crate::walk::Node| {
|
crate::walk::walk(&ast, |node: crate::walk::Node| {
|
||||||
let node_range: SourceRange = (&node).into();
|
let Ok(node_range): Result<SourceRange, _> = (&node).try_into() else {
|
||||||
|
return Ok(true);
|
||||||
|
};
|
||||||
|
|
||||||
if !node_range.contains(source_range.start()) {
|
if !node_range.contains(source_range.start()) {
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
@ -563,7 +560,7 @@ impl Backend {
|
|||||||
let semantic_token = SemanticToken {
|
let semantic_token = SemanticToken {
|
||||||
delta_line: position.line - last_position.line + 1,
|
delta_line: position.line - last_position.line + 1,
|
||||||
delta_start: 0,
|
delta_start: 0,
|
||||||
length: token.value.len() as u32,
|
length: (token.end - token.start) as u32,
|
||||||
token_type: token_type_index,
|
token_type: token_type_index,
|
||||||
token_modifiers_bitset,
|
token_modifiers_bitset,
|
||||||
};
|
};
|
||||||
@ -582,7 +579,7 @@ impl Backend {
|
|||||||
} else {
|
} else {
|
||||||
position.character - last_position.character
|
position.character - last_position.character
|
||||||
},
|
},
|
||||||
length: token.value.len() as u32,
|
length: (token.end - token.start) as u32,
|
||||||
token_type: token_type_index,
|
token_type: token_type_index,
|
||||||
token_modifiers_bitset,
|
token_modifiers_bitset,
|
||||||
};
|
};
|
||||||
@ -963,8 +960,8 @@ impl LanguageServer for Backend {
|
|||||||
semantic_tokens_options: SemanticTokensOptions {
|
semantic_tokens_options: SemanticTokensOptions {
|
||||||
work_done_progress_options: WorkDoneProgressOptions::default(),
|
work_done_progress_options: WorkDoneProgressOptions::default(),
|
||||||
legend: SemanticTokensLegend {
|
legend: SemanticTokensLegend {
|
||||||
token_types: SEMANTIC_TOKEN_TYPES.clone(),
|
token_types: SEMANTIC_TOKEN_TYPES.to_vec(),
|
||||||
token_modifiers: SEMANTIC_TOKEN_MODIFIERS.clone(),
|
token_modifiers: SEMANTIC_TOKEN_MODIFIERS.to_vec(),
|
||||||
},
|
},
|
||||||
range: Some(false),
|
range: Some(false),
|
||||||
full: Some(SemanticTokensFullOptions::Bool(true)),
|
full: Some(SemanticTokensFullOptions::Bool(true)),
|
||||||
|
@ -1082,7 +1082,7 @@ fn myFn = (param1) => {
|
|||||||
|
|
||||||
// Get the token map.
|
// Get the token map.
|
||||||
let token_map = server.token_map.get("file:///test.kcl").unwrap().clone();
|
let token_map = server.token_map.get("file:///test.kcl").unwrap().clone();
|
||||||
assert!(token_map != vec![]);
|
assert!(!token_map.is_empty());
|
||||||
|
|
||||||
// Get the ast.
|
// Get the ast.
|
||||||
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
|
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
|
||||||
@ -2206,7 +2206,7 @@ part001 = cube([0,0], 20)
|
|||||||
|
|
||||||
// Get the tokens.
|
// Get the tokens.
|
||||||
let tokens = server.token_map.get("file:///test.kcl").unwrap().clone();
|
let tokens = server.token_map.get("file:///test.kcl").unwrap().clone();
|
||||||
assert_eq!(tokens.len(), 120);
|
assert_eq!(tokens.as_slice().len(), 120);
|
||||||
|
|
||||||
// Get the ast.
|
// Get the ast.
|
||||||
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
|
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
|
||||||
@ -3379,11 +3379,11 @@ part001 = startSketchOn('XY')
|
|||||||
|
|
||||||
// Get the symbols map.
|
// Get the symbols map.
|
||||||
let symbols_map = server.symbols_map.get("file:///test.kcl").unwrap().clone();
|
let symbols_map = server.symbols_map.get("file:///test.kcl").unwrap().clone();
|
||||||
assert!(symbols_map != vec![]);
|
assert!(!symbols_map.is_empty());
|
||||||
|
|
||||||
// Get the semantic tokens map.
|
// Get the semantic tokens map.
|
||||||
let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone();
|
let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone();
|
||||||
assert!(semantic_tokens_map != vec![]);
|
assert!(!semantic_tokens_map.is_empty());
|
||||||
|
|
||||||
// Get the memory.
|
// Get the memory.
|
||||||
let memory = server.memory_map.get("file:///test.kcl").unwrap().clone();
|
let memory = server.memory_map.get("file:///test.kcl").unwrap().clone();
|
||||||
@ -3422,7 +3422,7 @@ NEW_LINT = 1"#
|
|||||||
|
|
||||||
// Get the semantic tokens map.
|
// Get the semantic tokens map.
|
||||||
let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone();
|
let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone();
|
||||||
assert!(semantic_tokens_map != vec![]);
|
assert!(!semantic_tokens_map.is_empty());
|
||||||
|
|
||||||
// Get the memory.
|
// Get the memory.
|
||||||
let memory = server.memory_map.get("file:///test.kcl");
|
let memory = server.memory_map.get("file:///test.kcl");
|
||||||
@ -3466,7 +3466,7 @@ part001 = startSketchOn('XY')
|
|||||||
|
|
||||||
// Get the token map.
|
// Get the token map.
|
||||||
let token_map = server.token_map.get("file:///test.kcl").unwrap().clone();
|
let token_map = server.token_map.get("file:///test.kcl").unwrap().clone();
|
||||||
assert!(token_map != vec![]);
|
assert!(!token_map.is_empty());
|
||||||
|
|
||||||
// Get the ast.
|
// Get the ast.
|
||||||
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
|
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
|
||||||
@ -3474,11 +3474,11 @@ part001 = startSketchOn('XY')
|
|||||||
|
|
||||||
// Get the symbols map.
|
// Get the symbols map.
|
||||||
let symbols_map = server.symbols_map.get("file:///test.kcl").unwrap().clone();
|
let symbols_map = server.symbols_map.get("file:///test.kcl").unwrap().clone();
|
||||||
assert!(symbols_map != vec![]);
|
assert!(!symbols_map.is_empty());
|
||||||
|
|
||||||
// Get the semantic tokens map.
|
// Get the semantic tokens map.
|
||||||
let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone();
|
let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone();
|
||||||
assert!(semantic_tokens_map != vec![]);
|
assert!(!semantic_tokens_map.is_empty());
|
||||||
|
|
||||||
// Get the memory.
|
// Get the memory.
|
||||||
let memory = server.memory_map.get("file:///test.kcl").unwrap().clone();
|
let memory = server.memory_map.get("file:///test.kcl").unwrap().clone();
|
||||||
@ -3509,7 +3509,7 @@ part001 = startSketchOn('XY')
|
|||||||
|
|
||||||
// Get the token map.
|
// Get the token map.
|
||||||
let token_map = server.token_map.get("file:///test.kcl").unwrap().clone();
|
let token_map = server.token_map.get("file:///test.kcl").unwrap().clone();
|
||||||
assert!(token_map != vec![]);
|
assert!(!token_map.is_empty());
|
||||||
|
|
||||||
// Get the ast.
|
// Get the ast.
|
||||||
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
|
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
|
||||||
@ -3517,11 +3517,11 @@ part001 = startSketchOn('XY')
|
|||||||
|
|
||||||
// Get the symbols map.
|
// Get the symbols map.
|
||||||
let symbols_map = server.symbols_map.get("file:///test.kcl").unwrap().clone();
|
let symbols_map = server.symbols_map.get("file:///test.kcl").unwrap().clone();
|
||||||
assert!(symbols_map != vec![]);
|
assert!(!symbols_map.is_empty());
|
||||||
|
|
||||||
// Get the semantic tokens map.
|
// Get the semantic tokens map.
|
||||||
let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone();
|
let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone();
|
||||||
assert!(semantic_tokens_map != vec![]);
|
assert!(!semantic_tokens_map.is_empty());
|
||||||
|
|
||||||
// Get the memory.
|
// Get the memory.
|
||||||
let memory = server.memory_map.get("file:///test.kcl");
|
let memory = server.memory_map.get("file:///test.kcl");
|
||||||
|
@ -1,373 +0,0 @@
|
|||||||
//! Functions for helping with caching an ast and finding the parts the changed.
|
|
||||||
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
execution::ExecState,
|
|
||||||
parsing::ast::types::{Node, Program},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Information for the caching an AST and smartly re-executing it if we can.
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
|
||||||
#[ts(export)]
|
|
||||||
pub struct CacheInformation {
|
|
||||||
/// The old information.
|
|
||||||
pub old: Option<OldAstState>,
|
|
||||||
/// The new ast to executed.
|
|
||||||
pub new_ast: Node<Program>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The old ast and program memory.
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
|
||||||
#[ts(export)]
|
|
||||||
pub struct OldAstState {
|
|
||||||
/// The ast.
|
|
||||||
pub ast: Node<Program>,
|
|
||||||
/// The exec state.
|
|
||||||
pub exec_state: ExecState,
|
|
||||||
/// The last settings used for execution.
|
|
||||||
pub settings: crate::execution::ExecutorSettings,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<crate::Program> for CacheInformation {
|
|
||||||
fn from(program: crate::Program) -> Self {
|
|
||||||
CacheInformation {
|
|
||||||
old: None,
|
|
||||||
new_ast: program.ast,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The result of a cache check.
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
|
||||||
#[ts(export)]
|
|
||||||
pub struct CacheResult {
|
|
||||||
/// Should we clear the scene and start over?
|
|
||||||
pub clear_scene: bool,
|
|
||||||
/// The program that needs to be executed.
|
|
||||||
pub program: Node<Program>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given an old ast, old program memory and new ast, find the parts of the code that need to be
|
|
||||||
// re-executed.
|
|
||||||
// This function should never error, because in the case of any internal error, we should just pop
|
|
||||||
// the cache.
|
|
||||||
pub fn get_changed_program(
|
|
||||||
info: CacheInformation,
|
|
||||||
new_settings: &crate::execution::ExecutorSettings,
|
|
||||||
) -> Option<CacheResult> {
|
|
||||||
let Some(old) = info.old else {
|
|
||||||
// We have no old info, we need to re-execute the whole thing.
|
|
||||||
return Some(CacheResult {
|
|
||||||
clear_scene: true,
|
|
||||||
program: info.new_ast,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// If the settings are different we need to bust the cache.
|
|
||||||
// We specifically do this before checking if they are the exact same.
|
|
||||||
if old.settings != *new_settings {
|
|
||||||
return Some(CacheResult {
|
|
||||||
clear_scene: true,
|
|
||||||
program: info.new_ast,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the ASTs are the EXACT same we return None.
|
|
||||||
// We don't even need to waste time computing the digests.
|
|
||||||
if old.ast == info.new_ast {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut old_ast = old.ast.inner;
|
|
||||||
old_ast.compute_digest();
|
|
||||||
let mut new_ast = info.new_ast.inner.clone();
|
|
||||||
new_ast.compute_digest();
|
|
||||||
|
|
||||||
// Check if the digest is the same.
|
|
||||||
if old_ast.digest == new_ast.digest {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the changes were only to Non-code areas, like comments or whitespace.
|
|
||||||
|
|
||||||
// For any unhandled cases just re-execute the whole thing.
|
|
||||||
Some(CacheResult {
|
|
||||||
clear_scene: true,
|
|
||||||
program: info.new_ast,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use pretty_assertions::assert_eq;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
async fn execute(program: &crate::Program) -> Result<ExecState> {
|
|
||||||
let ctx = crate::execution::ExecutorContext {
|
|
||||||
engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await?)),
|
|
||||||
fs: Arc::new(crate::fs::FileManager::new()),
|
|
||||||
stdlib: Arc::new(crate::std::StdLib::new()),
|
|
||||||
settings: Default::default(),
|
|
||||||
context_type: crate::execution::ContextType::Mock,
|
|
||||||
};
|
|
||||||
let mut exec_state = crate::execution::ExecState::default();
|
|
||||||
ctx.run(program.clone().into(), &mut exec_state).await?;
|
|
||||||
|
|
||||||
Ok(exec_state)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Easy case where we have no old ast and memory.
|
|
||||||
// We need to re-execute everything.
|
|
||||||
#[test]
|
|
||||||
fn test_get_changed_program_no_old_information() {
|
|
||||||
let new = r#"// Remove the end face for the extrusion.
|
|
||||||
firstSketch = startSketchOn('XY')
|
|
||||||
|> startProfileAt([-12, 12], %)
|
|
||||||
|> line([24, 0], %)
|
|
||||||
|> line([0, -24], %)
|
|
||||||
|> line([-24, 0], %)
|
|
||||||
|> close(%)
|
|
||||||
|> extrude(6, %)
|
|
||||||
|
|
||||||
// Remove the end face for the extrusion.
|
|
||||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
|
||||||
let program = crate::Program::parse_no_errs(new).unwrap().ast;
|
|
||||||
|
|
||||||
let result = get_changed_program(
|
|
||||||
CacheInformation {
|
|
||||||
old: None,
|
|
||||||
new_ast: program.clone(),
|
|
||||||
},
|
|
||||||
&Default::default(),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(result.is_some());
|
|
||||||
|
|
||||||
let result = result.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(result.program, program);
|
|
||||||
assert!(result.clear_scene);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
|
||||||
async fn test_get_changed_program_same_code() {
|
|
||||||
let new = r#"// Remove the end face for the extrusion.
|
|
||||||
firstSketch = startSketchOn('XY')
|
|
||||||
|> startProfileAt([-12, 12], %)
|
|
||||||
|> line([24, 0], %)
|
|
||||||
|> line([0, -24], %)
|
|
||||||
|> line([-24, 0], %)
|
|
||||||
|> close(%)
|
|
||||||
|> extrude(6, %)
|
|
||||||
|
|
||||||
// Remove the end face for the extrusion.
|
|
||||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
|
||||||
let program = crate::Program::parse_no_errs(new).unwrap();
|
|
||||||
|
|
||||||
let executed = execute(&program).await.unwrap();
|
|
||||||
|
|
||||||
let result = get_changed_program(
|
|
||||||
CacheInformation {
|
|
||||||
old: Some(OldAstState {
|
|
||||||
ast: program.ast.clone(),
|
|
||||||
exec_state: executed,
|
|
||||||
settings: Default::default(),
|
|
||||||
}),
|
|
||||||
new_ast: program.ast.clone(),
|
|
||||||
},
|
|
||||||
&Default::default(),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(result, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
|
||||||
async fn test_get_changed_program_same_code_changed_whitespace() {
|
|
||||||
let old = r#" // Remove the end face for the extrusion.
|
|
||||||
firstSketch = startSketchOn('XY')
|
|
||||||
|> startProfileAt([-12, 12], %)
|
|
||||||
|> line([24, 0], %)
|
|
||||||
|> line([0, -24], %)
|
|
||||||
|> line([-24, 0], %)
|
|
||||||
|> close(%)
|
|
||||||
|> extrude(6, %)
|
|
||||||
|
|
||||||
// Remove the end face for the extrusion.
|
|
||||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
|
|
||||||
|
|
||||||
let new = r#"// Remove the end face for the extrusion.
|
|
||||||
firstSketch = startSketchOn('XY')
|
|
||||||
|> startProfileAt([-12, 12], %)
|
|
||||||
|> line([24, 0], %)
|
|
||||||
|> line([0, -24], %)
|
|
||||||
|> line([-24, 0], %)
|
|
||||||
|> close(%)
|
|
||||||
|> extrude(6, %)
|
|
||||||
|
|
||||||
// Remove the end face for the extrusion.
|
|
||||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
|
||||||
let program_old = crate::Program::parse_no_errs(old).unwrap();
|
|
||||||
|
|
||||||
let executed = execute(&program_old).await.unwrap();
|
|
||||||
|
|
||||||
let program_new = crate::Program::parse_no_errs(new).unwrap();
|
|
||||||
|
|
||||||
let result = get_changed_program(
|
|
||||||
CacheInformation {
|
|
||||||
old: Some(OldAstState {
|
|
||||||
ast: program_old.ast.clone(),
|
|
||||||
exec_state: executed,
|
|
||||||
settings: Default::default(),
|
|
||||||
}),
|
|
||||||
new_ast: program_new.ast.clone(),
|
|
||||||
},
|
|
||||||
&Default::default(),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(result, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
|
||||||
async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
|
|
||||||
let old = r#" // Removed the end face for the extrusion.
|
|
||||||
firstSketch = startSketchOn('XY')
|
|
||||||
|> startProfileAt([-12, 12], %)
|
|
||||||
|> line([24, 0], %)
|
|
||||||
|> line([0, -24], %)
|
|
||||||
|> line([-24, 0], %)
|
|
||||||
|> close(%)
|
|
||||||
|> extrude(6, %)
|
|
||||||
|
|
||||||
// Remove the end face for the extrusion.
|
|
||||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
|
|
||||||
|
|
||||||
let new = r#"// Remove the end face for the extrusion.
|
|
||||||
firstSketch = startSketchOn('XY')
|
|
||||||
|> startProfileAt([-12, 12], %)
|
|
||||||
|> line([24, 0], %)
|
|
||||||
|> line([0, -24], %)
|
|
||||||
|> line([-24, 0], %)
|
|
||||||
|> close(%)
|
|
||||||
|> extrude(6, %)
|
|
||||||
|
|
||||||
// Remove the end face for the extrusion.
|
|
||||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
|
||||||
let program_old = crate::Program::parse_no_errs(old).unwrap();
|
|
||||||
|
|
||||||
let executed = execute(&program_old).await.unwrap();
|
|
||||||
|
|
||||||
let program_new = crate::Program::parse_no_errs(new).unwrap();
|
|
||||||
|
|
||||||
let result = get_changed_program(
|
|
||||||
CacheInformation {
|
|
||||||
old: Some(OldAstState {
|
|
||||||
ast: program_old.ast.clone(),
|
|
||||||
exec_state: executed,
|
|
||||||
settings: Default::default(),
|
|
||||||
}),
|
|
||||||
new_ast: program_new.ast.clone(),
|
|
||||||
},
|
|
||||||
&Default::default(),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(result, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
|
||||||
async fn test_get_changed_program_same_code_changed_code_comments() {
|
|
||||||
let old = r#" // Removed the end face for the extrusion.
|
|
||||||
firstSketch = startSketchOn('XY')
|
|
||||||
|> startProfileAt([-12, 12], %)
|
|
||||||
|> line([24, 0], %)
|
|
||||||
|> line([0, -24], %)
|
|
||||||
|> line([-24, 0], %) // my thing
|
|
||||||
|> close(%)
|
|
||||||
|> extrude(6, %)
|
|
||||||
|
|
||||||
// Remove the end face for the extrusion.
|
|
||||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
|
|
||||||
|
|
||||||
let new = r#"// Remove the end face for the extrusion.
|
|
||||||
firstSketch = startSketchOn('XY')
|
|
||||||
|> startProfileAt([-12, 12], %)
|
|
||||||
|> line([24, 0], %)
|
|
||||||
|> line([0, -24], %)
|
|
||||||
|> line([-24, 0], %)
|
|
||||||
|> close(%)
|
|
||||||
|> extrude(6, %)
|
|
||||||
|
|
||||||
// Remove the end face for the extrusion.
|
|
||||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
|
||||||
let program_old = crate::Program::parse_no_errs(old).unwrap();
|
|
||||||
|
|
||||||
let executed = execute(&program_old).await.unwrap();
|
|
||||||
|
|
||||||
let program_new = crate::Program::parse_no_errs(new).unwrap();
|
|
||||||
|
|
||||||
let result = get_changed_program(
|
|
||||||
CacheInformation {
|
|
||||||
old: Some(OldAstState {
|
|
||||||
ast: program_old.ast.clone(),
|
|
||||||
exec_state: executed,
|
|
||||||
settings: Default::default(),
|
|
||||||
}),
|
|
||||||
new_ast: program_new.ast.clone(),
|
|
||||||
},
|
|
||||||
&Default::default(),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(result.is_some());
|
|
||||||
|
|
||||||
let result = result.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(result.program, program_new.ast);
|
|
||||||
assert!(result.clear_scene);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Changing the units with the exact same file should bust the cache.
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
|
||||||
async fn test_get_changed_program_same_code_but_different_units() {
|
|
||||||
let new = r#"// Remove the end face for the extrusion.
|
|
||||||
firstSketch = startSketchOn('XY')
|
|
||||||
|> startProfileAt([-12, 12], %)
|
|
||||||
|> line([24, 0], %)
|
|
||||||
|> line([0, -24], %)
|
|
||||||
|> line([-24, 0], %)
|
|
||||||
|> close(%)
|
|
||||||
|> extrude(6, %)
|
|
||||||
|
|
||||||
// Remove the end face for the extrusion.
|
|
||||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
|
||||||
let program = crate::Program::parse_no_errs(new).unwrap();
|
|
||||||
|
|
||||||
let executed = execute(&program).await.unwrap();
|
|
||||||
|
|
||||||
let result = get_changed_program(
|
|
||||||
CacheInformation {
|
|
||||||
old: Some(OldAstState {
|
|
||||||
ast: program.ast.clone(),
|
|
||||||
exec_state: executed,
|
|
||||||
settings: Default::default(),
|
|
||||||
}),
|
|
||||||
new_ast: program.ast.clone(),
|
|
||||||
},
|
|
||||||
&crate::ExecutorSettings {
|
|
||||||
units: crate::UnitLength::Cm,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(result.is_some());
|
|
||||||
|
|
||||||
let result = result.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(result.program, program.ast);
|
|
||||||
assert!(result.clear_scene);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +1,6 @@
|
|||||||
use sha2::{Digest as DigestTrait, Sha256};
|
use sha2::{Digest as DigestTrait, Sha256};
|
||||||
|
|
||||||
use super::types::{DefaultParamVal, ItemVisibility, VariableKind};
|
use super::types::{DefaultParamVal, ItemVisibility, LabelledExpression, VariableKind};
|
||||||
use crate::parsing::ast::types::{
|
use crate::parsing::ast::types::{
|
||||||
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, CallExpressionKw,
|
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, CallExpressionKw,
|
||||||
CommentStyle, ElseIf, Expr, ExpressionStatement, FnArgType, FunctionExpression, Identifier, IfExpression,
|
CommentStyle, ElseIf, Expr, ExpressionStatement, FnArgType, FunctionExpression, Identifier, IfExpression,
|
||||||
@ -115,6 +115,7 @@ impl Expr {
|
|||||||
Expr::MemberExpression(me) => me.compute_digest(),
|
Expr::MemberExpression(me) => me.compute_digest(),
|
||||||
Expr::UnaryExpression(ue) => ue.compute_digest(),
|
Expr::UnaryExpression(ue) => ue.compute_digest(),
|
||||||
Expr::IfExpression(e) => e.compute_digest(),
|
Expr::IfExpression(e) => e.compute_digest(),
|
||||||
|
Expr::LabelledExpression(e) => e.compute_digest(),
|
||||||
Expr::None(_) => {
|
Expr::None(_) => {
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
hasher.update(b"Value::None");
|
hasher.update(b"Value::None");
|
||||||
@ -396,6 +397,13 @@ impl UnaryExpression {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl LabelledExpression {
|
||||||
|
compute_digest!(|slf, hasher| {
|
||||||
|
hasher.update(slf.expr.compute_digest());
|
||||||
|
hasher.update(slf.label.compute_digest());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
impl PipeExpression {
|
impl PipeExpression {
|
||||||
compute_digest!(|slf, hasher| {
|
compute_digest!(|slf, hasher| {
|
||||||
hasher.update(slf.body.len().to_ne_bytes());
|
hasher.update(slf.body.len().to_ne_bytes());
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
pub(crate) mod cache;
|
|
||||||
pub(crate) mod digest;
|
pub(crate) mod digest;
|
||||||
pub mod modify;
|
pub mod modify;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
@ -37,6 +36,7 @@ impl Expr {
|
|||||||
Expr::MemberExpression(member_expression) => member_expression.module_id,
|
Expr::MemberExpression(member_expression) => member_expression.module_id,
|
||||||
Expr::UnaryExpression(unary_expression) => unary_expression.module_id,
|
Expr::UnaryExpression(unary_expression) => unary_expression.module_id,
|
||||||
Expr::IfExpression(expr) => expr.module_id,
|
Expr::IfExpression(expr) => expr.module_id,
|
||||||
|
Expr::LabelledExpression(expr) => expr.expr.module_id(),
|
||||||
Expr::None(none) => none.module_id,
|
Expr::None(none) => none.module_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -184,7 +184,7 @@ impl Node<Program> {
|
|||||||
/// Walk the ast and get all the variables and tags as completion items.
|
/// Walk the ast and get all the variables and tags as completion items.
|
||||||
pub fn completion_items<'a>(&'a self) -> Result<Vec<CompletionItem>> {
|
pub fn completion_items<'a>(&'a self) -> Result<Vec<CompletionItem>> {
|
||||||
let completions = Arc::new(Mutex::new(vec![]));
|
let completions = Arc::new(Mutex::new(vec![]));
|
||||||
crate::walk::walk(self, &|node: crate::walk::Node<'a>| {
|
crate::walk::walk(self, |node: crate::walk::Node<'a>| {
|
||||||
let mut findings = completions.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
|
let mut findings = completions.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
|
||||||
match node {
|
match node {
|
||||||
crate::walk::Node::TagDeclarator(tag) => {
|
crate::walk::Node::TagDeclarator(tag) => {
|
||||||
@ -195,7 +195,7 @@ impl Node<Program> {
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
Ok(true)
|
Ok::<bool, anyhow::Error>(true)
|
||||||
})?;
|
})?;
|
||||||
let x = completions.lock().unwrap();
|
let x = completions.lock().unwrap();
|
||||||
Ok(x.clone())
|
Ok(x.clone())
|
||||||
@ -204,7 +204,7 @@ impl Node<Program> {
|
|||||||
/// Returns all the lsp symbols in the program.
|
/// Returns all the lsp symbols in the program.
|
||||||
pub fn get_lsp_symbols<'a>(&'a self, code: &str) -> Result<Vec<DocumentSymbol>> {
|
pub fn get_lsp_symbols<'a>(&'a self, code: &str) -> Result<Vec<DocumentSymbol>> {
|
||||||
let symbols = Arc::new(Mutex::new(vec![]));
|
let symbols = Arc::new(Mutex::new(vec![]));
|
||||||
crate::walk::walk(self, &|node: crate::walk::Node<'a>| {
|
crate::walk::walk(self, |node: crate::walk::Node<'a>| {
|
||||||
let mut findings = symbols.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
|
let mut findings = symbols.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
|
||||||
match node {
|
match node {
|
||||||
crate::walk::Node::TagDeclarator(tag) => {
|
crate::walk::Node::TagDeclarator(tag) => {
|
||||||
@ -215,7 +215,7 @@ impl Node<Program> {
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
Ok(true)
|
Ok::<bool, anyhow::Error>(true)
|
||||||
})?;
|
})?;
|
||||||
let x = symbols.lock().unwrap();
|
let x = symbols.lock().unwrap();
|
||||||
Ok(x.clone())
|
Ok(x.clone())
|
||||||
@ -227,10 +227,10 @@ impl Node<Program> {
|
|||||||
RuleT: crate::lint::Rule<'a>,
|
RuleT: crate::lint::Rule<'a>,
|
||||||
{
|
{
|
||||||
let v = Arc::new(Mutex::new(vec![]));
|
let v = Arc::new(Mutex::new(vec![]));
|
||||||
crate::walk::walk(self, &|node: crate::walk::Node<'a>| {
|
crate::walk::walk(self, |node: crate::walk::Node<'a>| {
|
||||||
let mut findings = v.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
|
let mut findings = v.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
|
||||||
findings.append(&mut rule.check(node)?);
|
findings.append(&mut rule.check(node)?);
|
||||||
Ok(true)
|
Ok::<bool, anyhow::Error>(true)
|
||||||
})?;
|
})?;
|
||||||
let x = v.lock().unwrap();
|
let x = v.lock().unwrap();
|
||||||
Ok(x.clone())
|
Ok(x.clone())
|
||||||
@ -598,6 +598,7 @@ pub enum Expr {
|
|||||||
MemberExpression(BoxNode<MemberExpression>),
|
MemberExpression(BoxNode<MemberExpression>),
|
||||||
UnaryExpression(BoxNode<UnaryExpression>),
|
UnaryExpression(BoxNode<UnaryExpression>),
|
||||||
IfExpression(BoxNode<IfExpression>),
|
IfExpression(BoxNode<IfExpression>),
|
||||||
|
LabelledExpression(BoxNode<LabelledExpression>),
|
||||||
None(Node<KclNone>),
|
None(Node<KclNone>),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -640,6 +641,7 @@ impl Expr {
|
|||||||
Expr::UnaryExpression(_unary_exp) => None,
|
Expr::UnaryExpression(_unary_exp) => None,
|
||||||
Expr::PipeSubstitution(_pipe_substitution) => None,
|
Expr::PipeSubstitution(_pipe_substitution) => None,
|
||||||
Expr::IfExpression(_) => None,
|
Expr::IfExpression(_) => None,
|
||||||
|
Expr::LabelledExpression(expr) => expr.expr.get_non_code_meta(),
|
||||||
Expr::None(_none) => None,
|
Expr::None(_none) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -666,6 +668,7 @@ impl Expr {
|
|||||||
Expr::UnaryExpression(ref mut unary_exp) => unary_exp.replace_value(source_range, new_value),
|
Expr::UnaryExpression(ref mut unary_exp) => unary_exp.replace_value(source_range, new_value),
|
||||||
Expr::IfExpression(_) => {}
|
Expr::IfExpression(_) => {}
|
||||||
Expr::PipeSubstitution(_) => {}
|
Expr::PipeSubstitution(_) => {}
|
||||||
|
Expr::LabelledExpression(expr) => expr.expr.replace_value(source_range, new_value),
|
||||||
Expr::None(_) => {}
|
Expr::None(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -687,6 +690,7 @@ impl Expr {
|
|||||||
Expr::MemberExpression(member_expression) => member_expression.start,
|
Expr::MemberExpression(member_expression) => member_expression.start,
|
||||||
Expr::UnaryExpression(unary_expression) => unary_expression.start,
|
Expr::UnaryExpression(unary_expression) => unary_expression.start,
|
||||||
Expr::IfExpression(expr) => expr.start,
|
Expr::IfExpression(expr) => expr.start,
|
||||||
|
Expr::LabelledExpression(expr) => expr.start,
|
||||||
Expr::None(none) => none.start,
|
Expr::None(none) => none.start,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -708,6 +712,7 @@ impl Expr {
|
|||||||
Expr::MemberExpression(member_expression) => member_expression.end,
|
Expr::MemberExpression(member_expression) => member_expression.end,
|
||||||
Expr::UnaryExpression(unary_expression) => unary_expression.end,
|
Expr::UnaryExpression(unary_expression) => unary_expression.end,
|
||||||
Expr::IfExpression(expr) => expr.end,
|
Expr::IfExpression(expr) => expr.end,
|
||||||
|
Expr::LabelledExpression(expr) => expr.end,
|
||||||
Expr::None(none) => none.end,
|
Expr::None(none) => none.end,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -734,6 +739,8 @@ impl Expr {
|
|||||||
Expr::Literal(_) => None,
|
Expr::Literal(_) => None,
|
||||||
Expr::Identifier(_) => None,
|
Expr::Identifier(_) => None,
|
||||||
Expr::TagDeclarator(_) => None,
|
Expr::TagDeclarator(_) => None,
|
||||||
|
// TODO LSP hover info for tag
|
||||||
|
Expr::LabelledExpression(expr) => expr.expr.get_hover_value_for_position(pos, code),
|
||||||
// TODO: LSP hover information for symbols. https://github.com/KittyCAD/modeling-app/issues/1127
|
// TODO: LSP hover information for symbols. https://github.com/KittyCAD/modeling-app/issues/1127
|
||||||
Expr::PipeSubstitution(_) => None,
|
Expr::PipeSubstitution(_) => None,
|
||||||
}
|
}
|
||||||
@ -763,6 +770,7 @@ impl Expr {
|
|||||||
}
|
}
|
||||||
Expr::UnaryExpression(ref mut unary_expression) => unary_expression.rename_identifiers(old_name, new_name),
|
Expr::UnaryExpression(ref mut unary_expression) => unary_expression.rename_identifiers(old_name, new_name),
|
||||||
Expr::IfExpression(ref mut expr) => expr.rename_identifiers(old_name, new_name),
|
Expr::IfExpression(ref mut expr) => expr.rename_identifiers(old_name, new_name),
|
||||||
|
Expr::LabelledExpression(expr) => expr.expr.rename_identifiers(old_name, new_name),
|
||||||
Expr::None(_) => {}
|
Expr::None(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -788,9 +796,19 @@ impl Expr {
|
|||||||
Expr::MemberExpression(member_expression) => member_expression.get_constraint_level(),
|
Expr::MemberExpression(member_expression) => member_expression.get_constraint_level(),
|
||||||
Expr::UnaryExpression(unary_expression) => unary_expression.get_constraint_level(),
|
Expr::UnaryExpression(unary_expression) => unary_expression.get_constraint_level(),
|
||||||
Expr::IfExpression(expr) => expr.get_constraint_level(),
|
Expr::IfExpression(expr) => expr.get_constraint_level(),
|
||||||
|
Expr::LabelledExpression(expr) => expr.expr.get_constraint_level(),
|
||||||
Expr::None(none) => none.get_constraint_level(),
|
Expr::None(none) => none.get_constraint_level(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_substitution_arg(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Expr::CallExpression(call_expression) => call_expression.has_substitution_arg(),
|
||||||
|
Expr::CallExpressionKw(call_expression) => call_expression.has_substitution_arg(),
|
||||||
|
Expr::LabelledExpression(expr) => expr.expr.has_substitution_arg(),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Expr> for SourceRange {
|
impl From<Expr> for SourceRange {
|
||||||
@ -805,6 +823,36 @@ impl From<&Expr> for SourceRange {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||||
|
#[ts(export)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub struct LabelledExpression {
|
||||||
|
pub expr: Expr,
|
||||||
|
pub label: Node<Identifier>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
#[ts(optional)]
|
||||||
|
pub digest: Option<Digest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LabelledExpression {
|
||||||
|
pub(crate) fn new(expr: Expr, label: Node<Identifier>) -> Node<LabelledExpression> {
|
||||||
|
let start = expr.start();
|
||||||
|
let end = label.end;
|
||||||
|
let module_id = expr.module_id();
|
||||||
|
Node::new(
|
||||||
|
LabelledExpression {
|
||||||
|
expr,
|
||||||
|
label,
|
||||||
|
digest: None,
|
||||||
|
},
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
module_id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
@ -2810,7 +2858,8 @@ pub struct Parameter {
|
|||||||
pub identifier: Node<Identifier>,
|
pub identifier: Node<Identifier>,
|
||||||
/// The type of the parameter.
|
/// The type of the parameter.
|
||||||
/// This is optional if the user defines a type.
|
/// This is optional if the user defines a type.
|
||||||
#[serde(skip)]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
#[ts(skip)]
|
||||||
pub type_: Option<FnArgType>,
|
pub type_: Option<FnArgType>,
|
||||||
/// Is the parameter optional?
|
/// Is the parameter optional?
|
||||||
/// If so, what is its default value?
|
/// If so, what is its default value?
|
||||||
|
@ -2,7 +2,7 @@ use crate::{
|
|||||||
errors::{CompilationError, KclError, KclErrorDetails},
|
errors::{CompilationError, KclError, KclErrorDetails},
|
||||||
parsing::{
|
parsing::{
|
||||||
ast::types::{Node, Program},
|
ast::types::{Node, Program},
|
||||||
token::{Token, TokenType},
|
token::TokenStream,
|
||||||
},
|
},
|
||||||
source_range::{ModuleId, SourceRange},
|
source_range::{ModuleId, SourceRange},
|
||||||
};
|
};
|
||||||
@ -34,15 +34,13 @@ pub fn top_level_parse(code: &str) -> ParseResult {
|
|||||||
|
|
||||||
/// Parse the given KCL code into an AST.
|
/// Parse the given KCL code into an AST.
|
||||||
pub fn parse_str(code: &str, module_id: ModuleId) -> ParseResult {
|
pub fn parse_str(code: &str, module_id: ModuleId) -> ParseResult {
|
||||||
let tokens = pr_try!(crate::parsing::token::lexer(code, module_id));
|
let tokens = pr_try!(crate::parsing::token::lex(code, module_id));
|
||||||
parse_tokens(tokens)
|
parse_tokens(tokens)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse the supplied tokens into an AST.
|
/// Parse the supplied tokens into an AST.
|
||||||
pub fn parse_tokens(tokens: Vec<Token>) -> ParseResult {
|
pub fn parse_tokens(mut tokens: TokenStream) -> ParseResult {
|
||||||
let (tokens, unknown_tokens): (Vec<Token>, Vec<Token>) = tokens
|
let unknown_tokens = tokens.remove_unknown();
|
||||||
.into_iter()
|
|
||||||
.partition(|token| token.token_type != TokenType::Unknown);
|
|
||||||
|
|
||||||
if !unknown_tokens.is_empty() {
|
if !unknown_tokens.is_empty() {
|
||||||
let source_ranges = unknown_tokens.iter().map(SourceRange::from).collect();
|
let source_ranges = unknown_tokens.iter().map(SourceRange::from).collect();
|
||||||
@ -69,7 +67,7 @@ pub fn parse_tokens(tokens: Vec<Token>) -> ParseResult {
|
|||||||
return Node::<Program>::default().into();
|
return Node::<Program>::default().into();
|
||||||
}
|
}
|
||||||
|
|
||||||
parser::run_parser(&mut tokens.as_slice())
|
parser::run_parser(tokens.as_slice())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Result of parsing.
|
/// Result of parsing.
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,76 @@
|
|||||||
|
---
|
||||||
|
source: kcl/src/parsing/parser.rs
|
||||||
|
expression: actual
|
||||||
|
snapshot_kind: text
|
||||||
|
---
|
||||||
|
{
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"declaration": {
|
||||||
|
"end": 35,
|
||||||
|
"id": {
|
||||||
|
"end": 6,
|
||||||
|
"name": "foo",
|
||||||
|
"start": 3,
|
||||||
|
"type": "Identifier"
|
||||||
|
},
|
||||||
|
"init": {
|
||||||
|
"body": {
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"argument": {
|
||||||
|
"end": 33,
|
||||||
|
"raw": "1",
|
||||||
|
"start": 32,
|
||||||
|
"type": "Literal",
|
||||||
|
"type": "Literal",
|
||||||
|
"value": 1.0
|
||||||
|
},
|
||||||
|
"end": 33,
|
||||||
|
"start": 25,
|
||||||
|
"type": "ReturnStatement",
|
||||||
|
"type": "ReturnStatement"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"end": 35,
|
||||||
|
"start": 23
|
||||||
|
},
|
||||||
|
"end": 35,
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"type": "Parameter",
|
||||||
|
"identifier": {
|
||||||
|
"end": 8,
|
||||||
|
"name": "x",
|
||||||
|
"start": 7,
|
||||||
|
"type": "Identifier"
|
||||||
|
},
|
||||||
|
"type_": {
|
||||||
|
"type": "Primitive",
|
||||||
|
"type": "Number"
|
||||||
|
},
|
||||||
|
"default_value": {
|
||||||
|
"type": "Literal",
|
||||||
|
"type": "Literal",
|
||||||
|
"value": 2.0,
|
||||||
|
"raw": "2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start": 6,
|
||||||
|
"type": "FunctionExpression",
|
||||||
|
"type": "FunctionExpression"
|
||||||
|
},
|
||||||
|
"start": 3,
|
||||||
|
"type": "VariableDeclarator"
|
||||||
|
},
|
||||||
|
"end": 35,
|
||||||
|
"kind": "fn",
|
||||||
|
"start": 0,
|
||||||
|
"type": "VariableDeclaration",
|
||||||
|
"type": "VariableDeclaration"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"end": 35,
|
||||||
|
"start": 0
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
source: kcl/src/parsing/parser.rs
|
||||||
|
expression: actual
|
||||||
|
snapshot_kind: text
|
||||||
|
---
|
||||||
|
{
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"declaration": {
|
||||||
|
"end": 27,
|
||||||
|
"id": {
|
||||||
|
"end": 6,
|
||||||
|
"name": "foo",
|
||||||
|
"start": 3,
|
||||||
|
"type": "Identifier"
|
||||||
|
},
|
||||||
|
"init": {
|
||||||
|
"body": {
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"argument": {
|
||||||
|
"end": 25,
|
||||||
|
"raw": "1",
|
||||||
|
"start": 24,
|
||||||
|
"type": "Literal",
|
||||||
|
"type": "Literal",
|
||||||
|
"value": 1.0
|
||||||
|
},
|
||||||
|
"end": 25,
|
||||||
|
"start": 17,
|
||||||
|
"type": "ReturnStatement",
|
||||||
|
"type": "ReturnStatement"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"end": 27,
|
||||||
|
"start": 15
|
||||||
|
},
|
||||||
|
"end": 27,
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"type": "Parameter",
|
||||||
|
"identifier": {
|
||||||
|
"end": 8,
|
||||||
|
"name": "x",
|
||||||
|
"start": 7,
|
||||||
|
"type": "Identifier"
|
||||||
|
},
|
||||||
|
"default_value": {
|
||||||
|
"type": "Literal",
|
||||||
|
"type": "Literal",
|
||||||
|
"value": 2.0,
|
||||||
|
"raw": "2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start": 6,
|
||||||
|
"type": "FunctionExpression",
|
||||||
|
"type": "FunctionExpression"
|
||||||
|
},
|
||||||
|
"start": 3,
|
||||||
|
"type": "VariableDeclarator"
|
||||||
|
},
|
||||||
|
"end": 27,
|
||||||
|
"kind": "fn",
|
||||||
|
"start": 0,
|
||||||
|
"type": "VariableDeclaration",
|
||||||
|
"type": "VariableDeclaration"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"end": 27,
|
||||||
|
"start": 0
|
||||||
|
}
|
@ -1,11 +1,17 @@
|
|||||||
use std::str::FromStr;
|
// Clippy does not agree with rustc here for some reason.
|
||||||
|
#![allow(clippy::needless_lifetimes)]
|
||||||
|
|
||||||
|
use std::{fmt, iter::Enumerate, num::NonZeroUsize};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use parse_display::{Display, FromStr};
|
use parse_display::Display;
|
||||||
use schemars::JsonSchema;
|
use tokeniser::Input;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tower_lsp::lsp_types::SemanticTokenType;
|
use tower_lsp::lsp_types::SemanticTokenType;
|
||||||
use winnow::{error::ParseError, stream::ContainsToken};
|
use winnow::{
|
||||||
|
self,
|
||||||
|
error::ParseError,
|
||||||
|
stream::{ContainsToken, Stream},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
errors::KclError,
|
errors::KclError,
|
||||||
@ -15,14 +21,201 @@ use crate::{
|
|||||||
|
|
||||||
mod tokeniser;
|
mod tokeniser;
|
||||||
|
|
||||||
// Re-export
|
|
||||||
pub use tokeniser::Input;
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) use tokeniser::RESERVED_WORDS;
|
pub(crate) use tokeniser::RESERVED_WORDS;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub(crate) struct TokenStream {
|
||||||
|
tokens: Vec<Token>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TokenStream {
|
||||||
|
fn new(tokens: Vec<Token>) -> Self {
|
||||||
|
Self { tokens }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn remove_unknown(&mut self) -> Vec<Token> {
|
||||||
|
let tokens = std::mem::take(&mut self.tokens);
|
||||||
|
let (tokens, unknown_tokens): (Vec<Token>, Vec<Token>) = tokens
|
||||||
|
.into_iter()
|
||||||
|
.partition(|token| token.token_type != TokenType::Unknown);
|
||||||
|
self.tokens = tokens;
|
||||||
|
unknown_tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = &Token> {
|
||||||
|
self.tokens.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.tokens.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_slice(&self) -> TokenSlice {
|
||||||
|
TokenSlice::from(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a TokenStream> for TokenSlice<'a> {
|
||||||
|
fn from(stream: &'a TokenStream) -> Self {
|
||||||
|
TokenSlice {
|
||||||
|
start: 0,
|
||||||
|
end: stream.tokens.len(),
|
||||||
|
stream,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for TokenStream {
|
||||||
|
type Item = Token;
|
||||||
|
|
||||||
|
type IntoIter = std::vec::IntoIter<Token>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.tokens.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct TokenSlice<'a> {
|
||||||
|
stream: &'a TokenStream,
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> std::ops::Deref for TokenSlice<'a> {
|
||||||
|
type Target = [Token];
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.stream.tokens[self.start..self.end]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TokenSlice<'a> {
|
||||||
|
pub fn token(&self, i: usize) -> &Token {
|
||||||
|
&self.stream.tokens[i + self.start]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = &Token> {
|
||||||
|
(**self).iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn without_ends(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
start: self.start + 1,
|
||||||
|
end: self.end - 1,
|
||||||
|
stream: self.stream,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoIterator for TokenSlice<'a> {
|
||||||
|
type Item = &'a Token;
|
||||||
|
|
||||||
|
type IntoIter = std::slice::Iter<'a, Token>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.stream.tokens[self.start..self.end].iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Stream for TokenSlice<'a> {
|
||||||
|
type Token = Token;
|
||||||
|
type Slice = Self;
|
||||||
|
type IterOffsets = Enumerate<std::vec::IntoIter<Token>>;
|
||||||
|
type Checkpoint = Checkpoint;
|
||||||
|
|
||||||
|
fn iter_offsets(&self) -> Self::IterOffsets {
|
||||||
|
#[allow(clippy::unnecessary_to_owned)]
|
||||||
|
self.to_vec().into_iter().enumerate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eof_offset(&self) -> usize {
|
||||||
|
self.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_token(&mut self) -> Option<Self::Token> {
|
||||||
|
let token = self.first()?.clone();
|
||||||
|
self.start += 1;
|
||||||
|
Some(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn offset_for<P>(&self, predicate: P) -> Option<usize>
|
||||||
|
where
|
||||||
|
P: Fn(Self::Token) -> bool,
|
||||||
|
{
|
||||||
|
self.iter().position(|b| predicate(b.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn offset_at(&self, tokens: usize) -> Result<usize, winnow::error::Needed> {
|
||||||
|
if let Some(needed) = tokens.checked_sub(self.len()).and_then(NonZeroUsize::new) {
|
||||||
|
Err(winnow::error::Needed::Size(needed))
|
||||||
|
} else {
|
||||||
|
Ok(tokens)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_slice(&mut self, offset: usize) -> Self::Slice {
|
||||||
|
assert!(self.start + offset <= self.end);
|
||||||
|
|
||||||
|
let next = TokenSlice {
|
||||||
|
stream: self.stream,
|
||||||
|
start: self.start,
|
||||||
|
end: self.start + offset,
|
||||||
|
};
|
||||||
|
self.start += offset;
|
||||||
|
next
|
||||||
|
}
|
||||||
|
|
||||||
|
fn checkpoint(&self) -> Self::Checkpoint {
|
||||||
|
Checkpoint(self.start, self.end)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self, checkpoint: &Self::Checkpoint) {
|
||||||
|
self.start = checkpoint.0;
|
||||||
|
self.end = checkpoint.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw(&self) -> &dyn fmt::Debug {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> winnow::stream::Offset for TokenSlice<'a> {
|
||||||
|
fn offset_from(&self, start: &Self) -> usize {
|
||||||
|
self.start - start.start
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> winnow::stream::Offset<Checkpoint> for TokenSlice<'a> {
|
||||||
|
fn offset_from(&self, start: &Checkpoint) -> usize {
|
||||||
|
self.start - start.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl winnow::stream::Offset for Checkpoint {
|
||||||
|
fn offset_from(&self, start: &Self) -> usize {
|
||||||
|
self.0 - start.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> winnow::stream::StreamIsPartial for TokenSlice<'a> {
|
||||||
|
type PartialState = ();
|
||||||
|
|
||||||
|
fn complete(&mut self) -> Self::PartialState {}
|
||||||
|
|
||||||
|
fn restore_partial(&mut self, _: Self::PartialState) {}
|
||||||
|
|
||||||
|
fn is_partial_supported() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Checkpoint(usize, usize);
|
||||||
|
|
||||||
/// The types of tokens.
|
/// The types of tokens.
|
||||||
#[derive(Debug, PartialEq, Eq, Copy, Clone, Deserialize, Serialize, JsonSchema, FromStr, Display)]
|
#[derive(Debug, PartialEq, Eq, Copy, Clone, Display)]
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
#[display(style = "camelCase")]
|
#[display(style = "camelCase")]
|
||||||
pub enum TokenType {
|
pub enum TokenType {
|
||||||
/// A number.
|
/// A number.
|
||||||
@ -73,6 +266,8 @@ pub enum TokenType {
|
|||||||
impl TryFrom<TokenType> for SemanticTokenType {
|
impl TryFrom<TokenType> for SemanticTokenType {
|
||||||
type Error = anyhow::Error;
|
type Error = anyhow::Error;
|
||||||
fn try_from(token_type: TokenType) -> Result<Self> {
|
fn try_from(token_type: TokenType) -> Result<Self> {
|
||||||
|
// If you return a new kind of `SemanticTokenType`, make sure to update `SEMANTIC_TOKEN_TYPES`
|
||||||
|
// in the LSP implementation.
|
||||||
Ok(match token_type {
|
Ok(match token_type {
|
||||||
TokenType::Number => Self::NUMBER,
|
TokenType::Number => Self::NUMBER,
|
||||||
TokenType::Word => Self::VARIABLE,
|
TokenType::Word => Self::VARIABLE,
|
||||||
@ -102,52 +297,6 @@ impl TryFrom<TokenType> for SemanticTokenType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TokenType {
|
impl TokenType {
|
||||||
// This is for the lsp server.
|
|
||||||
// Don't call this function directly in the code use a lazy_static instead
|
|
||||||
// like we do in the lsp server.
|
|
||||||
pub fn all_semantic_token_types() -> Result<Vec<SemanticTokenType>> {
|
|
||||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
|
||||||
settings.inline_subschemas = true;
|
|
||||||
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
|
||||||
|
|
||||||
let schema = TokenType::json_schema(&mut generator);
|
|
||||||
let schemars::schema::Schema::Object(o) = &schema else {
|
|
||||||
anyhow::bail!("expected object schema: {:#?}", schema);
|
|
||||||
};
|
|
||||||
let Some(subschemas) = &o.subschemas else {
|
|
||||||
anyhow::bail!("expected subschemas: {:#?}", schema);
|
|
||||||
};
|
|
||||||
let Some(one_ofs) = &subschemas.one_of else {
|
|
||||||
anyhow::bail!("expected one_of: {:#?}", schema);
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut semantic_tokens = vec![];
|
|
||||||
for one_of in one_ofs {
|
|
||||||
let schemars::schema::Schema::Object(o) = one_of else {
|
|
||||||
anyhow::bail!("expected object one_of: {:#?}", one_of);
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(enum_values) = o.enum_values.as_ref() else {
|
|
||||||
anyhow::bail!("expected enum values: {:#?}", o);
|
|
||||||
};
|
|
||||||
|
|
||||||
if enum_values.len() > 1 {
|
|
||||||
anyhow::bail!("expected only one enum value: {:#?}", o);
|
|
||||||
}
|
|
||||||
|
|
||||||
if enum_values.is_empty() {
|
|
||||||
anyhow::bail!("expected at least one enum value: {:#?}", o);
|
|
||||||
}
|
|
||||||
|
|
||||||
let label = TokenType::from_str(&enum_values[0].to_string().replace('"', ""))?;
|
|
||||||
if let Ok(semantic_token_type) = SemanticTokenType::try_from(label) {
|
|
||||||
semantic_tokens.push(semantic_token_type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(semantic_tokens)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_whitespace(&self) -> bool {
|
pub fn is_whitespace(&self) -> bool {
|
||||||
matches!(self, Self::Whitespace)
|
matches!(self, Self::Whitespace)
|
||||||
}
|
}
|
||||||
@ -157,17 +306,15 @@ impl TokenType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct Token {
|
pub struct Token {
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub token_type: TokenType,
|
pub token_type: TokenType,
|
||||||
/// Offset in the source code where this token begins.
|
/// Offset in the source code where this token begins.
|
||||||
pub start: usize,
|
pub start: usize,
|
||||||
/// Offset in the source code where this token ends.
|
/// Offset in the source code where this token ends.
|
||||||
pub end: usize,
|
pub end: usize,
|
||||||
#[serde(default, skip_serializing_if = "ModuleId::is_top_level")]
|
pub(super) module_id: ModuleId,
|
||||||
pub module_id: ModuleId,
|
pub(super) value: String,
|
||||||
pub value: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContainsToken<Token> for (TokenType, &str) {
|
impl ContainsToken<Token> for (TokenType, &str) {
|
||||||
@ -249,7 +396,7 @@ impl From<&Token> for SourceRange {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lexer(s: &str, module_id: ModuleId) -> Result<Vec<Token>, KclError> {
|
pub fn lex(s: &str, module_id: ModuleId) -> Result<TokenStream, KclError> {
|
||||||
tokeniser::lex(s, module_id).map_err(From::from)
|
tokeniser::lex(s, module_id).map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,15 +428,3 @@ impl From<ParseError<Input<'_>, winnow::error::ContextError>> for KclError {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
// We have this as a test so we can ensure it never panics with an unwrap in the server.
|
|
||||||
#[test]
|
|
||||||
fn test_token_type_to_semantic_token_type() {
|
|
||||||
let semantic_types = TokenType::all_semantic_token_types().unwrap();
|
|
||||||
assert!(!semantic_types.is_empty());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -47,7 +47,7 @@ fn read(filename: &'static str, test_name: &str) -> String {
|
|||||||
|
|
||||||
fn parse(test_name: &str) {
|
fn parse(test_name: &str) {
|
||||||
let input = read("input.kcl", test_name);
|
let input = read("input.kcl", test_name);
|
||||||
let tokens = crate::parsing::token::lexer(&input, ModuleId::default()).unwrap();
|
let tokens = crate::parsing::token::lex(&input, ModuleId::default()).unwrap();
|
||||||
|
|
||||||
// Parse the tokens into an AST.
|
// Parse the tokens into an AST.
|
||||||
let parse_res = Result::<_, KclError>::Ok(crate::parsing::parse_tokens(tokens).unwrap());
|
let parse_res = Result::<_, KclError>::Ok(crate::parsing::parse_tokens(tokens).unwrap());
|
||||||
@ -1502,3 +1502,108 @@ mod kw_fn {
|
|||||||
super::execute(TEST_NAME, true).await
|
super::execute(TEST_NAME, true).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
mod tag_can_be_proxied_through_parameter {
|
||||||
|
const TEST_NAME: &str = "tag_can_be_proxied_through_parameter";
|
||||||
|
|
||||||
|
/// 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, false).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mod tag_proxied_through_function_does_not_define_var {
|
||||||
|
const TEST_NAME: &str = "tag_proxied_through_function_does_not_define_var";
|
||||||
|
|
||||||
|
/// 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, false).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mod kw_fn_too_few_args {
|
||||||
|
const TEST_NAME: &str = "kw_fn_too_few_args";
|
||||||
|
|
||||||
|
/// 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, false).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mod kw_fn_unlabeled_but_has_label {
|
||||||
|
const TEST_NAME: &str = "kw_fn_unlabeled_but_has_label";
|
||||||
|
|
||||||
|
/// 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, false).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mod kw_fn_with_defaults {
|
||||||
|
const TEST_NAME: &str = "kw_fn_with_defaults";
|
||||||
|
|
||||||
|
/// 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, false).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
263
src/wasm-lib/kcl/src/std/appearance.rs
Normal file
263
src/wasm-lib/kcl/src/std/appearance.rs
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
//! Standard library appearance.
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use derive_docs::stdlib;
|
||||||
|
use kcmc::{each_cmd as mcmd, ModelingCmd};
|
||||||
|
use kittycad_modeling_cmds::{self as kcmc, shared::Color};
|
||||||
|
use regex::Regex;
|
||||||
|
use rgba_simple::Hex;
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::{KclError, KclErrorDetails},
|
||||||
|
execution::{ExecState, KclValue, Solid, SolidSet},
|
||||||
|
std::Args,
|
||||||
|
};
|
||||||
|
|
||||||
|
lazy_static::lazy_static! {
|
||||||
|
static ref HEX_REGEX: Regex = Regex::new(r"^#[0-9a-fA-F]{6}$").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Data for appearance.
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Validate)]
|
||||||
|
#[ts(export)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AppearanceData {
|
||||||
|
/// Color of the new material, a hex string like "#ff0000".
|
||||||
|
#[schemars(regex(pattern = "#[0-9a-fA-F]{6}"))]
|
||||||
|
pub color: String,
|
||||||
|
/// Metalness of the new material, a percentage like 95.7.
|
||||||
|
#[validate(range(min = 0.0, max = 100.0))]
|
||||||
|
pub metalness: Option<f64>,
|
||||||
|
/// Roughness of the new material, a percentage like 95.7.
|
||||||
|
#[validate(range(min = 0.0, max = 100.0))]
|
||||||
|
pub roughness: Option<f64>,
|
||||||
|
// TODO(jess): we can also ambient occlusion here I just don't know what it is.
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the appearance of a solid. This only works on solids, not sketches or individual paths.
|
||||||
|
pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
|
let (data, solid_set): (AppearanceData, SolidSet) = args.get_data_and_solid_set()?;
|
||||||
|
|
||||||
|
// Validate the data.
|
||||||
|
data.validate().map_err(|err| {
|
||||||
|
KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!("Invalid appearance data: {}", err),
|
||||||
|
source_ranges: vec![args.source_range],
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Make sure the color if set is valid.
|
||||||
|
if !HEX_REGEX.is_match(&data.color) {
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!("Invalid hex color (`{}`), try something like `#fff000`", data.color),
|
||||||
|
source_ranges: vec![args.source_range],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = inner_appearance(data, solid_set, args).await?;
|
||||||
|
Ok(result.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the appearance of a solid. This only works on solids, not sketches or individual paths.
|
||||||
|
///
|
||||||
|
/// This will work on any solid, including extruded solids, revolved solids, and shelled solids.
|
||||||
|
/// ```no_run
|
||||||
|
/// /// Add color to an extruded solid.
|
||||||
|
/// exampleSketch = startSketchOn("XZ")
|
||||||
|
/// |> startProfileAt([0, 0], %)
|
||||||
|
/// |> lineTo([10, 0], %)
|
||||||
|
/// |> lineTo([0, 10], %)
|
||||||
|
/// |> lineTo([-10, 0], %)
|
||||||
|
/// |> close(%)
|
||||||
|
///
|
||||||
|
/// example = extrude(5, exampleSketch)
|
||||||
|
/// |> appearance({color= '#ff0000', metalness= 50, roughness= 50}, %)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// /// Add color to a revolved solid.
|
||||||
|
/// sketch001 = startSketchOn('XY')
|
||||||
|
/// |> circle({ center = [15, 0], radius = 5 }, %)
|
||||||
|
/// |> revolve({ angle = 360, axis = 'y' }, %)
|
||||||
|
/// |> appearance({
|
||||||
|
/// color = '#ff0000',
|
||||||
|
/// metalness = 90,
|
||||||
|
/// roughness = 90
|
||||||
|
/// }, %)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// /// Add color to different solids.
|
||||||
|
/// fn cube(center) {
|
||||||
|
/// return startSketchOn('XY')
|
||||||
|
/// |> startProfileAt([center[0] - 10, center[1] - 10], %)
|
||||||
|
/// |> lineTo([center[0] + 10, center[1] - 10], %)
|
||||||
|
/// |> lineTo([center[0] + 10, center[1] + 10], %)
|
||||||
|
/// |> lineTo([center[0] - 10, center[1] + 10], %)
|
||||||
|
/// |> close(%)
|
||||||
|
/// |> extrude(10, %)
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// example0 = cube([0, 0])
|
||||||
|
/// example1 = cube([20, 0])
|
||||||
|
/// example2 = cube([40, 0])
|
||||||
|
///
|
||||||
|
/// appearance({color= '#ff0000', metalness= 50, roughness= 50}, [example0, example1])
|
||||||
|
/// appearance({color= '#00ff00', metalness= 50, roughness= 50}, example2)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// /// You can set the appearance before or after you shell it will yield the same result.
|
||||||
|
/// /// This example shows setting the appearance _after_ the shell.
|
||||||
|
/// firstSketch = startSketchOn('XY')
|
||||||
|
/// |> startProfileAt([-12, 12], %)
|
||||||
|
/// |> line([24, 0], %)
|
||||||
|
/// |> line([0, -24], %)
|
||||||
|
/// |> line([-24, 0], %)
|
||||||
|
/// |> close(%)
|
||||||
|
/// |> extrude(6, %)
|
||||||
|
///
|
||||||
|
/// shell({
|
||||||
|
/// faces = ['end'],
|
||||||
|
/// thickness = 0.25,
|
||||||
|
/// }, firstSketch)
|
||||||
|
/// |> appearance({
|
||||||
|
/// color = '#ff0000',
|
||||||
|
/// metalness = 90,
|
||||||
|
/// roughness = 90
|
||||||
|
/// }, %)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// /// You can set the appearance before or after you shell it will yield the same result.
|
||||||
|
/// /// This example shows setting the appearance _before_ the shell.
|
||||||
|
/// firstSketch = startSketchOn('XY')
|
||||||
|
/// |> startProfileAt([-12, 12], %)
|
||||||
|
/// |> line([24, 0], %)
|
||||||
|
/// |> line([0, -24], %)
|
||||||
|
/// |> line([-24, 0], %)
|
||||||
|
/// |> close(%)
|
||||||
|
/// |> extrude(6, %)
|
||||||
|
/// |> appearance({
|
||||||
|
/// color = '#ff0000',
|
||||||
|
/// metalness = 90,
|
||||||
|
/// roughness = 90
|
||||||
|
/// }, %)
|
||||||
|
///
|
||||||
|
/// shell({
|
||||||
|
/// faces = ['end'],
|
||||||
|
/// thickness = 0.25,
|
||||||
|
/// }, firstSketch)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// /// Setting the appearance of a 3D pattern can be done _before_ or _after_ the pattern.
|
||||||
|
/// /// This example shows _before_ the pattern.
|
||||||
|
/// exampleSketch = startSketchOn('XZ')
|
||||||
|
/// |> startProfileAt([0, 0], %)
|
||||||
|
/// |> line([0, 2], %)
|
||||||
|
/// |> line([3, 1], %)
|
||||||
|
/// |> line([0, -4], %)
|
||||||
|
/// |> close(%)
|
||||||
|
///
|
||||||
|
/// example = extrude(1, exampleSketch)
|
||||||
|
/// |> appearance({
|
||||||
|
/// color = '#ff0000',
|
||||||
|
/// metalness = 90,
|
||||||
|
/// roughness = 90
|
||||||
|
/// }, %)
|
||||||
|
/// |> patternLinear3d({
|
||||||
|
/// axis = [1, 0, 1],
|
||||||
|
/// instances = 7,
|
||||||
|
/// distance = 6
|
||||||
|
/// }, %)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// /// Setting the appearance of a 3D pattern can be done _before_ or _after_ the pattern.
|
||||||
|
/// /// This example shows _after_ the pattern.
|
||||||
|
/// exampleSketch = startSketchOn('XZ')
|
||||||
|
/// |> startProfileAt([0, 0], %)
|
||||||
|
/// |> line([0, 2], %)
|
||||||
|
/// |> line([3, 1], %)
|
||||||
|
/// |> line([0, -4], %)
|
||||||
|
/// |> close(%)
|
||||||
|
///
|
||||||
|
/// example = extrude(1, exampleSketch)
|
||||||
|
/// |> patternLinear3d({
|
||||||
|
/// axis = [1, 0, 1],
|
||||||
|
/// instances = 7,
|
||||||
|
/// distance = 6
|
||||||
|
/// }, %)
|
||||||
|
/// |> appearance({
|
||||||
|
/// color = '#ff0000',
|
||||||
|
/// metalness = 90,
|
||||||
|
/// roughness = 90
|
||||||
|
/// }, %)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// /// Color the result of a 2D pattern that was extruded.
|
||||||
|
/// exampleSketch = startSketchOn('XZ')
|
||||||
|
/// |> startProfileAt([.5, 25], %)
|
||||||
|
/// |> line([0, 5], %)
|
||||||
|
/// |> line([-1, 0], %)
|
||||||
|
/// |> line([0, -5], %)
|
||||||
|
/// |> close(%)
|
||||||
|
/// |> patternCircular2d({
|
||||||
|
/// center = [0, 0],
|
||||||
|
/// instances = 13,
|
||||||
|
/// arcDegrees = 360,
|
||||||
|
/// rotateDuplicates = true
|
||||||
|
/// }, %)
|
||||||
|
///
|
||||||
|
/// example = extrude(1, exampleSketch)
|
||||||
|
/// |> appearance({
|
||||||
|
/// color = '#ff0000',
|
||||||
|
/// metalness = 90,
|
||||||
|
/// roughness = 90
|
||||||
|
/// }, %)
|
||||||
|
/// ```
|
||||||
|
#[stdlib {
|
||||||
|
name = "appearance",
|
||||||
|
}]
|
||||||
|
async fn inner_appearance(data: AppearanceData, solid_set: SolidSet, args: Args) -> Result<SolidSet, KclError> {
|
||||||
|
let solids: Vec<Box<Solid>> = solid_set.into();
|
||||||
|
|
||||||
|
for solid in &solids {
|
||||||
|
// Set the material properties.
|
||||||
|
let rgb = rgba_simple::RGB::<f32>::from_hex(&data.color).map_err(|err| {
|
||||||
|
KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!("Invalid hex color (`{}`): {}", data.color, err),
|
||||||
|
source_ranges: vec![args.source_range],
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let color = Color {
|
||||||
|
r: rgb.red,
|
||||||
|
g: rgb.green,
|
||||||
|
b: rgb.blue,
|
||||||
|
a: 100.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
args.batch_modeling_cmd(
|
||||||
|
uuid::Uuid::new_v4(),
|
||||||
|
ModelingCmd::from(mcmd::ObjectSetMaterialParamsPbr {
|
||||||
|
object_id: solid.id,
|
||||||
|
color,
|
||||||
|
metalness: data.metalness.unwrap_or_default() as f32 / 100.0,
|
||||||
|
roughness: data.roughness.unwrap_or_default() as f32 / 100.0,
|
||||||
|
ambient_occlusion: 0.0,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Idk if we want to actually modify the memory for the colors, but I'm not right now since
|
||||||
|
// I can't think of a use case for it.
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(SolidSet::from(solids))
|
||||||
|
}
|
@ -50,6 +50,13 @@ pub struct KwArgs {
|
|||||||
pub labeled: HashMap<String, Arg>,
|
pub labeled: HashMap<String, Arg>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl KwArgs {
|
||||||
|
/// How many arguments are there?
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.labeled.len() + if self.unlabeled.is_some() { 1 } else { 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
/// Positional args.
|
/// Positional args.
|
||||||
@ -1089,6 +1096,20 @@ impl<'a> FromKclValue<'a> for super::fillet::FilletData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> FromKclValue<'a> for super::appearance::AppearanceData {
|
||||||
|
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
|
||||||
|
let obj = arg.as_object()?;
|
||||||
|
let_field_of!(obj, color);
|
||||||
|
let_field_of!(obj, metalness?);
|
||||||
|
let_field_of!(obj, roughness?);
|
||||||
|
Some(Self {
|
||||||
|
color,
|
||||||
|
metalness,
|
||||||
|
roughness,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> FromKclValue<'a> for super::helix::HelixData {
|
impl<'a> FromKclValue<'a> for super::helix::HelixData {
|
||||||
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
|
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
|
||||||
let obj = arg.as_object()?;
|
let obj = arg.as_object()?;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
//! Functions implemented for language execution.
|
//! Functions implemented for language execution.
|
||||||
|
|
||||||
|
pub mod appearance;
|
||||||
pub mod args;
|
pub mod args;
|
||||||
pub mod array;
|
pub mod array;
|
||||||
pub mod assert;
|
pub mod assert;
|
||||||
@ -50,6 +51,7 @@ lazy_static! {
|
|||||||
Box::new(LegLen),
|
Box::new(LegLen),
|
||||||
Box::new(LegAngX),
|
Box::new(LegAngX),
|
||||||
Box::new(LegAngY),
|
Box::new(LegAngY),
|
||||||
|
Box::new(crate::std::appearance::Appearance),
|
||||||
Box::new(crate::std::convert::Int),
|
Box::new(crate::std::convert::Int),
|
||||||
Box::new(crate::std::extrude::Extrude),
|
Box::new(crate::std::extrude::Extrude),
|
||||||
Box::new(crate::std::segment::SegEnd),
|
Box::new(crate::std::segment::SegEnd),
|
||||||
|
@ -55,7 +55,11 @@ async fn do_execute_and_snapshot(
|
|||||||
program: Program,
|
program: Program,
|
||||||
) -> Result<(crate::execution::ExecState, image::DynamicImage), ExecError> {
|
) -> Result<(crate::execution::ExecState, image::DynamicImage), ExecError> {
|
||||||
let mut exec_state = Default::default();
|
let mut exec_state = Default::default();
|
||||||
let snapshot_png_bytes = ctx.execute_and_prepare(&program, &mut exec_state).await?.contents.0;
|
let snapshot_png_bytes = ctx
|
||||||
|
.execute_and_prepare_snapshot(&program, &mut exec_state)
|
||||||
|
.await?
|
||||||
|
.contents
|
||||||
|
.0;
|
||||||
|
|
||||||
// Decode the snapshot, return it.
|
// Decode the snapshot, return it.
|
||||||
let img = image::ImageReader::new(std::io::Cursor::new(snapshot_png_bytes))
|
let img = image::ImageReader::new(std::io::Cursor::new(snapshot_png_bytes))
|
||||||
|
@ -3,10 +3,10 @@ use std::fmt::Write;
|
|||||||
use crate::parsing::{
|
use crate::parsing::{
|
||||||
ast::types::{
|
ast::types::{
|
||||||
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression,
|
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression,
|
||||||
CallExpressionKw, Expr, FnArgType, FormatOptions, FunctionExpression, IfExpression, ImportSelector,
|
CallExpressionKw, DefaultParamVal, Expr, FnArgType, FormatOptions, FunctionExpression, IfExpression,
|
||||||
ImportStatement, ItemVisibility, LabeledArg, Literal, LiteralIdentifier, LiteralValue, MemberExpression,
|
ImportSelector, ImportStatement, ItemVisibility, LabeledArg, Literal, LiteralIdentifier, LiteralValue,
|
||||||
MemberObject, Node, NonCodeValue, ObjectExpression, Parameter, PipeExpression, Program, TagDeclarator,
|
MemberExpression, MemberObject, Node, NonCodeValue, ObjectExpression, Parameter, PipeExpression, Program,
|
||||||
UnaryExpression, VariableDeclaration, VariableKind,
|
TagDeclarator, UnaryExpression, VariableDeclaration, VariableKind,
|
||||||
},
|
},
|
||||||
PIPE_OPERATOR,
|
PIPE_OPERATOR,
|
||||||
};
|
};
|
||||||
@ -166,7 +166,14 @@ pub(crate) enum ExprContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Expr {
|
impl Expr {
|
||||||
pub(crate) fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
|
pub(crate) fn recast(&self, options: &FormatOptions, indentation_level: usize, mut ctxt: ExprContext) -> String {
|
||||||
|
let is_decl = matches!(ctxt, ExprContext::Decl);
|
||||||
|
if is_decl {
|
||||||
|
// Just because this expression is being bound to a variable, doesn't mean that every child
|
||||||
|
// expression is being bound. So, reset the expression context if necessary.
|
||||||
|
// This will still preserve the "::Pipe" context though.
|
||||||
|
ctxt = ExprContext::Other;
|
||||||
|
}
|
||||||
match &self {
|
match &self {
|
||||||
Expr::BinaryExpression(bin_exp) => bin_exp.recast(options),
|
Expr::BinaryExpression(bin_exp) => bin_exp.recast(options),
|
||||||
Expr::ArrayExpression(array_exp) => array_exp.recast(options, indentation_level, ctxt),
|
Expr::ArrayExpression(array_exp) => array_exp.recast(options, indentation_level, ctxt),
|
||||||
@ -175,11 +182,7 @@ impl Expr {
|
|||||||
Expr::MemberExpression(mem_exp) => mem_exp.recast(),
|
Expr::MemberExpression(mem_exp) => mem_exp.recast(),
|
||||||
Expr::Literal(literal) => literal.recast(),
|
Expr::Literal(literal) => literal.recast(),
|
||||||
Expr::FunctionExpression(func_exp) => {
|
Expr::FunctionExpression(func_exp) => {
|
||||||
let mut result = if ctxt == ExprContext::Decl {
|
let mut result = if is_decl { String::new() } else { "fn".to_owned() };
|
||||||
String::new()
|
|
||||||
} else {
|
|
||||||
"fn".to_owned()
|
|
||||||
};
|
|
||||||
result += &func_exp.recast(options, indentation_level);
|
result += &func_exp.recast(options, indentation_level);
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
@ -191,6 +194,12 @@ impl Expr {
|
|||||||
Expr::UnaryExpression(unary_exp) => unary_exp.recast(options),
|
Expr::UnaryExpression(unary_exp) => unary_exp.recast(options),
|
||||||
Expr::IfExpression(e) => e.recast(options, indentation_level, ctxt),
|
Expr::IfExpression(e) => e.recast(options, indentation_level, ctxt),
|
||||||
Expr::PipeSubstitution(_) => crate::parsing::PIPE_SUBSTITUTION_OPERATOR.to_string(),
|
Expr::PipeSubstitution(_) => crate::parsing::PIPE_SUBSTITUTION_OPERATOR.to_string(),
|
||||||
|
Expr::LabelledExpression(e) => {
|
||||||
|
let mut result = e.expr.recast(options, indentation_level, ctxt);
|
||||||
|
result += " as ";
|
||||||
|
result += &e.label.name;
|
||||||
|
result
|
||||||
|
}
|
||||||
Expr::None(_) => {
|
Expr::None(_) => {
|
||||||
unimplemented!("there is no literal None, see https://github.com/KittyCAD/modeling-app/issues/1115")
|
unimplemented!("there is no literal None, see https://github.com/KittyCAD/modeling-app/issues/1115")
|
||||||
}
|
}
|
||||||
@ -404,7 +413,8 @@ fn expr_is_trivial(expr: &Expr) -> bool {
|
|||||||
| Expr::ObjectExpression(_)
|
| Expr::ObjectExpression(_)
|
||||||
| Expr::MemberExpression(_)
|
| Expr::MemberExpression(_)
|
||||||
| Expr::UnaryExpression(_)
|
| Expr::UnaryExpression(_)
|
||||||
| Expr::IfExpression(_) => false,
|
| Expr::IfExpression(_)
|
||||||
|
| Expr::LabelledExpression(_) => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -659,15 +669,18 @@ impl FunctionExpression {
|
|||||||
|
|
||||||
impl Parameter {
|
impl Parameter {
|
||||||
pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
|
pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
|
||||||
let mut result = format!(
|
let at_sign = if self.labeled { "" } else { "@" };
|
||||||
"{}{}",
|
let identifier = &self.identifier.name;
|
||||||
if self.labeled { "" } else { "@" },
|
let question_mark = if self.default_value.is_some() { "?" } else { "" };
|
||||||
self.identifier.name.clone()
|
let mut result = format!("{at_sign}{identifier}{question_mark}");
|
||||||
);
|
|
||||||
if let Some(ty) = &self.type_ {
|
if let Some(ty) = &self.type_ {
|
||||||
result += ": ";
|
result += ": ";
|
||||||
result += &ty.recast(options, indentation_level);
|
result += &ty.recast(options, indentation_level);
|
||||||
}
|
}
|
||||||
|
if let Some(DefaultParamVal::Literal(ref literal)) = self.default_value {
|
||||||
|
let lit = literal.recast();
|
||||||
|
result.push_str(&format!(" = {lit}"));
|
||||||
|
};
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
@ -1513,6 +1526,28 @@ tabs_l = startSketchOn({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_as() {
|
||||||
|
let some_program_string = r#"fn cube(pos, scale) {
|
||||||
|
x = dfsfs + dfsfsd as y
|
||||||
|
|
||||||
|
sg = startSketchOn('XY')
|
||||||
|
|> startProfileAt(pos, %) as foo
|
||||||
|
|> line([0, scale], %)
|
||||||
|
|> line([scale, 0], %) as bar
|
||||||
|
|> line([0 as baz, -scale] as qux, %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(scale, %)
|
||||||
|
}
|
||||||
|
|
||||||
|
cube(0, 0) as cub
|
||||||
|
"#;
|
||||||
|
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
|
||||||
|
|
||||||
|
let recasted = program.recast(&Default::default(), 0);
|
||||||
|
assert_eq!(recasted, some_program_string,);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_recast_with_bad_indentation() {
|
fn test_recast_with_bad_indentation() {
|
||||||
let some_program_string = r#"part001 = startSketchOn('XY')
|
let some_program_string = r#"part001 = startSketchOn('XY')
|
||||||
@ -2134,8 +2169,10 @@ fn f() {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
{
|
{
|
||||||
let tokens = crate::parsing::token::lexer(raw, ModuleId::default()).unwrap();
|
let tokens = crate::parsing::token::lex(raw, ModuleId::default()).unwrap();
|
||||||
let literal = crate::parsing::parser::unsigned_number_literal.parse(&tokens).unwrap();
|
let literal = crate::parsing::parser::unsigned_number_literal
|
||||||
|
.parse(tokens.as_slice())
|
||||||
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
literal.recast(),
|
literal.recast(),
|
||||||
expected,
|
expected,
|
||||||
@ -2170,6 +2207,28 @@ sketch002 = startSketchOn({
|
|||||||
assert_eq!(actual, expected);
|
assert_eq!(actual, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unparse_fn_unnamed() {
|
||||||
|
let input = r#"squares_out = reduce(arr, 0, fn(i, squares) {
|
||||||
|
return 1
|
||||||
|
})
|
||||||
|
"#;
|
||||||
|
let ast = crate::parsing::top_level_parse(input).unwrap();
|
||||||
|
let actual = ast.recast(&FormatOptions::new(), 0);
|
||||||
|
assert_eq!(actual, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unparse_fn_named() {
|
||||||
|
let input = r#"fn f(x) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let ast = crate::parsing::top_level_parse(input).unwrap();
|
||||||
|
let actual = ast.recast(&FormatOptions::new(), 0);
|
||||||
|
assert_eq!(actual, input);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn recast_objects_with_comments() {
|
fn recast_objects_with_comments() {
|
||||||
use winnow::Parser;
|
use winnow::Parser;
|
||||||
@ -2191,9 +2250,9 @@ sketch002 = startSketchOn({
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
{
|
{
|
||||||
let tokens = crate::parsing::token::lexer(input, ModuleId::default()).unwrap();
|
let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
|
||||||
crate::parsing::parser::print_tokens(&tokens);
|
crate::parsing::parser::print_tokens(tokens.as_slice());
|
||||||
let expr = crate::parsing::parser::object.parse(&tokens).unwrap();
|
let expr = crate::parsing::parser::object.parse(tokens.as_slice()).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
expr.recast(&FormatOptions::new(), 0, ExprContext::Other),
|
expr.recast(&FormatOptions::new(), 0, ExprContext::Other),
|
||||||
expected,
|
expected,
|
||||||
@ -2289,8 +2348,10 @@ sketch002 = startSketchOn({
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
{
|
{
|
||||||
let tokens = crate::parsing::token::lexer(input, ModuleId::default()).unwrap();
|
let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
|
||||||
let expr = crate::parsing::parser::array_elem_by_elem.parse(&tokens).unwrap();
|
let expr = crate::parsing::parser::array_elem_by_elem
|
||||||
|
.parse(tokens.as_slice())
|
||||||
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
expr.recast(&FormatOptions::new(), 0, ExprContext::Other),
|
expr.recast(&FormatOptions::new(), 0, ExprContext::Other),
|
||||||
expected,
|
expected,
|
||||||
|
@ -5,7 +5,7 @@ use crate::{
|
|||||||
|
|
||||||
/// The "Node" type wraps all the AST elements we're able to find in a KCL
|
/// The "Node" type wraps all the AST elements we're able to find in a KCL
|
||||||
/// file. Tokens we walk through will be one of these.
|
/// file. Tokens we walk through will be one of these.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub enum Node<'a> {
|
pub enum Node<'a> {
|
||||||
Program(NodeRef<'a, types::Program>),
|
Program(NodeRef<'a, types::Program>),
|
||||||
|
|
||||||
@ -31,6 +31,8 @@ pub enum Node<'a> {
|
|||||||
MemberExpression(NodeRef<'a, types::MemberExpression>),
|
MemberExpression(NodeRef<'a, types::MemberExpression>),
|
||||||
UnaryExpression(NodeRef<'a, types::UnaryExpression>),
|
UnaryExpression(NodeRef<'a, types::UnaryExpression>),
|
||||||
IfExpression(NodeRef<'a, types::IfExpression>),
|
IfExpression(NodeRef<'a, types::IfExpression>),
|
||||||
|
ElseIf(&'a types::ElseIf),
|
||||||
|
LabelledExpression(NodeRef<'a, types::LabelledExpression>),
|
||||||
|
|
||||||
Parameter(&'a types::Parameter),
|
Parameter(&'a types::Parameter),
|
||||||
|
|
||||||
@ -38,11 +40,22 @@ pub enum Node<'a> {
|
|||||||
|
|
||||||
MemberObject(&'a types::MemberObject),
|
MemberObject(&'a types::MemberObject),
|
||||||
LiteralIdentifier(&'a types::LiteralIdentifier),
|
LiteralIdentifier(&'a types::LiteralIdentifier),
|
||||||
|
|
||||||
|
KclNone(&'a types::KclNone),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&Node<'_>> for SourceRange {
|
/// Returned during source_range conversion.
|
||||||
fn from(node: &Node) -> Self {
|
#[derive(Debug)]
|
||||||
match node {
|
pub enum AstNodeError {
|
||||||
|
/// Returned if we try and [SourceRange] a [types::KclNone].
|
||||||
|
NoSourceForAKclNone,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&Node<'_>> for SourceRange {
|
||||||
|
type Error = AstNodeError;
|
||||||
|
|
||||||
|
fn try_from(node: &Node) -> Result<Self, Self::Error> {
|
||||||
|
Ok(match node {
|
||||||
Node::Program(n) => SourceRange::from(*n),
|
Node::Program(n) => SourceRange::from(*n),
|
||||||
Node::ImportStatement(n) => SourceRange::from(*n),
|
Node::ImportStatement(n) => SourceRange::from(*n),
|
||||||
Node::ExpressionStatement(n) => SourceRange::from(*n),
|
Node::ExpressionStatement(n) => SourceRange::from(*n),
|
||||||
@ -67,7 +80,65 @@ impl From<&Node<'_>> for SourceRange {
|
|||||||
Node::ObjectProperty(n) => SourceRange::from(*n),
|
Node::ObjectProperty(n) => SourceRange::from(*n),
|
||||||
Node::MemberObject(m) => SourceRange::new(m.start(), m.end(), m.module_id()),
|
Node::MemberObject(m) => SourceRange::new(m.start(), m.end(), m.module_id()),
|
||||||
Node::IfExpression(n) => SourceRange::from(*n),
|
Node::IfExpression(n) => SourceRange::from(*n),
|
||||||
|
Node::LabelledExpression(n) => SourceRange::from(*n),
|
||||||
Node::LiteralIdentifier(l) => SourceRange::new(l.start(), l.end(), l.module_id()),
|
Node::LiteralIdentifier(l) => SourceRange::new(l.start(), l.end(), l.module_id()),
|
||||||
|
|
||||||
|
// This is broken too
|
||||||
|
Node::ElseIf(n) => SourceRange::new(n.cond.start(), n.cond.end(), n.cond.module_id()),
|
||||||
|
|
||||||
|
// The KclNone type here isn't an actual node, so it has no
|
||||||
|
// start/end information.
|
||||||
|
Node::KclNone(_) => return Err(Self::Error::NoSourceForAKclNone),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'tree> From<&'tree types::BodyItem> for Node<'tree> {
|
||||||
|
fn from(node: &'tree types::BodyItem) -> Self {
|
||||||
|
match node {
|
||||||
|
types::BodyItem::ImportStatement(v) => v.as_ref().into(),
|
||||||
|
types::BodyItem::ExpressionStatement(v) => v.into(),
|
||||||
|
types::BodyItem::VariableDeclaration(v) => v.as_ref().into(),
|
||||||
|
types::BodyItem::ReturnStatement(v) => v.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'tree> From<&'tree types::Expr> for Node<'tree> {
|
||||||
|
fn from(node: &'tree types::Expr) -> Self {
|
||||||
|
match node {
|
||||||
|
types::Expr::Literal(lit) => lit.as_ref().into(),
|
||||||
|
types::Expr::TagDeclarator(tag) => tag.as_ref().into(),
|
||||||
|
types::Expr::Identifier(id) => id.as_ref().into(),
|
||||||
|
types::Expr::BinaryExpression(be) => be.as_ref().into(),
|
||||||
|
types::Expr::FunctionExpression(fe) => fe.as_ref().into(),
|
||||||
|
types::Expr::CallExpression(ce) => ce.as_ref().into(),
|
||||||
|
types::Expr::CallExpressionKw(ce) => ce.as_ref().into(),
|
||||||
|
types::Expr::PipeExpression(pe) => pe.as_ref().into(),
|
||||||
|
types::Expr::PipeSubstitution(ps) => ps.as_ref().into(),
|
||||||
|
types::Expr::ArrayExpression(ae) => ae.as_ref().into(),
|
||||||
|
types::Expr::ArrayRangeExpression(are) => are.as_ref().into(),
|
||||||
|
types::Expr::ObjectExpression(oe) => oe.as_ref().into(),
|
||||||
|
types::Expr::MemberExpression(me) => me.as_ref().into(),
|
||||||
|
types::Expr::UnaryExpression(ue) => ue.as_ref().into(),
|
||||||
|
types::Expr::IfExpression(e) => e.as_ref().into(),
|
||||||
|
types::Expr::LabelledExpression(e) => e.as_ref().into(),
|
||||||
|
types::Expr::None(n) => n.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'tree> From<&'tree types::BinaryPart> for Node<'tree> {
|
||||||
|
fn from(node: &'tree types::BinaryPart) -> Self {
|
||||||
|
match node {
|
||||||
|
types::BinaryPart::Literal(lit) => lit.as_ref().into(),
|
||||||
|
types::BinaryPart::Identifier(id) => id.as_ref().into(),
|
||||||
|
types::BinaryPart::BinaryExpression(be) => be.as_ref().into(),
|
||||||
|
types::BinaryPart::CallExpression(ce) => ce.as_ref().into(),
|
||||||
|
types::BinaryPart::CallExpressionKw(ce) => ce.as_ref().into(),
|
||||||
|
types::BinaryPart::UnaryExpression(ue) => ue.as_ref().into(),
|
||||||
|
types::BinaryPart::MemberExpression(me) => me.as_ref().into(),
|
||||||
|
types::BinaryPart::IfExpression(e) => e.as_ref().into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -116,4 +187,7 @@ impl_from!(Node, ObjectProperty);
|
|||||||
impl_from_ref!(Node, Parameter);
|
impl_from_ref!(Node, Parameter);
|
||||||
impl_from_ref!(Node, MemberObject);
|
impl_from_ref!(Node, MemberObject);
|
||||||
impl_from!(Node, IfExpression);
|
impl_from!(Node, IfExpression);
|
||||||
|
impl_from!(Node, ElseIf);
|
||||||
|
impl_from!(Node, LabelledExpression);
|
||||||
impl_from_ref!(Node, LiteralIdentifier);
|
impl_from_ref!(Node, LiteralIdentifier);
|
||||||
|
impl_from!(Node, KclNone);
|
||||||
|
201
src/wasm-lib/kcl/src/walk/ast_visitor.rs
Normal file
201
src/wasm-lib/kcl/src/walk/ast_visitor.rs
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate::walk::Node;
|
||||||
|
|
||||||
|
/// Walk-specific trait adding the ability to traverse the KCL AST.
|
||||||
|
///
|
||||||
|
/// This trait is implemented on [Node] to handle the fairly tricky bit of
|
||||||
|
/// recursing into the AST in a single place, as well as helpers for traversing
|
||||||
|
/// the tree. for callers to use.
|
||||||
|
pub trait Visitable<'tree> {
|
||||||
|
/// Return a `Vec<Node>` for all *direct* children of this AST node. This
|
||||||
|
/// should only contain direct descendants.
|
||||||
|
fn children(&self) -> Vec<Node<'tree>>;
|
||||||
|
|
||||||
|
/// Return `self` as a [Node]. Generally speaking, the [Visitable] trait
|
||||||
|
/// is only going to be implemented on [Node], so this is purely used by
|
||||||
|
/// helpers that are generic over a [Visitable] and want to deref back
|
||||||
|
/// into a [Node].
|
||||||
|
fn node(&self) -> Node<'tree>;
|
||||||
|
|
||||||
|
/// Call the provided [Visitor] in order to Visit `self`. This will
|
||||||
|
/// only be called on `self` -- the [Visitor] is responsible for
|
||||||
|
/// recursing into any children, if desired.
|
||||||
|
fn visit<VisitorT>(&self, visitor: VisitorT) -> Result<bool, VisitorT::Error>
|
||||||
|
where
|
||||||
|
VisitorT: Visitor<'tree>,
|
||||||
|
{
|
||||||
|
visitor.visit_node(self.node())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait used to enable visiting members of KCL AST.
|
||||||
|
///
|
||||||
|
/// Implementing this trait enables the implementer to be invoked over
|
||||||
|
/// members of KCL AST by using the [Visitable::visit] function on
|
||||||
|
/// a [Node].
|
||||||
|
pub trait Visitor<'tree> {
|
||||||
|
/// Error type returned by the [Self::visit] function.
|
||||||
|
type Error;
|
||||||
|
|
||||||
|
/// Visit a KCL AST [Node].
|
||||||
|
///
|
||||||
|
/// In general, implementers likely wish to check to see if a Node is what
|
||||||
|
/// they're looking for, and either descend into that [Node]'s children (by
|
||||||
|
/// calling [Visitable::children] on [Node] to get children nodes,
|
||||||
|
/// calling [Visitable::visit] on each node of interest), or perform
|
||||||
|
/// some action.
|
||||||
|
fn visit_node(&self, node: Node<'tree>) -> Result<bool, Self::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, FnT, ErrorT> Visitor<'a> for FnT
|
||||||
|
where
|
||||||
|
FnT: Fn(Node<'a>) -> Result<bool, ErrorT>,
|
||||||
|
{
|
||||||
|
type Error = ErrorT;
|
||||||
|
|
||||||
|
fn visit_node(&self, n: Node<'a>) -> Result<bool, ErrorT> {
|
||||||
|
self(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'tree> Visitable<'tree> for Node<'tree> {
|
||||||
|
fn node(&self) -> Node<'tree> {
|
||||||
|
*self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children(&self) -> Vec<Node<'tree>> {
|
||||||
|
match self {
|
||||||
|
Node::Program(n) => n.body.iter().map(|node| node.into()).collect(),
|
||||||
|
Node::ExpressionStatement(n) => {
|
||||||
|
vec![(&n.expression).into()]
|
||||||
|
}
|
||||||
|
Node::BinaryExpression(n) => {
|
||||||
|
vec![(&n.left).into(), (&n.right).into()]
|
||||||
|
}
|
||||||
|
Node::FunctionExpression(n) => {
|
||||||
|
let mut children = n.params.iter().map(|v| v.into()).collect::<Vec<Node>>();
|
||||||
|
children.push((&n.body).into());
|
||||||
|
children
|
||||||
|
}
|
||||||
|
Node::CallExpression(n) => {
|
||||||
|
let mut children = n.arguments.iter().map(|v| v.into()).collect::<Vec<Node>>();
|
||||||
|
children.insert(0, (&n.callee).into());
|
||||||
|
children
|
||||||
|
}
|
||||||
|
Node::CallExpressionKw(n) => {
|
||||||
|
let mut children = n.unlabeled.iter().map(|v| v.into()).collect::<Vec<Node>>();
|
||||||
|
|
||||||
|
// TODO: this is wrong but it's what the old walk code was doing.
|
||||||
|
// We likely need a real LabeledArg AST node, but I don't
|
||||||
|
// want to tango with it since it's a lot deeper than
|
||||||
|
// adding it to the enum.
|
||||||
|
children.extend(n.arguments.iter().map(|v| (&v.arg).into()).collect::<Vec<Node>>());
|
||||||
|
children
|
||||||
|
}
|
||||||
|
Node::PipeExpression(n) => n.body.iter().map(|v| v.into()).collect(),
|
||||||
|
Node::ArrayExpression(n) => n.elements.iter().map(|v| v.into()).collect(),
|
||||||
|
Node::ArrayRangeExpression(n) => {
|
||||||
|
vec![(&n.start_element).into(), (&n.end_element).into()]
|
||||||
|
}
|
||||||
|
Node::ObjectExpression(n) => n.properties.iter().map(|v| v.into()).collect(),
|
||||||
|
Node::MemberExpression(n) => {
|
||||||
|
vec![(&n.object).into(), (&n.property).into()]
|
||||||
|
}
|
||||||
|
Node::IfExpression(n) => {
|
||||||
|
let mut children = n.else_ifs.iter().map(|v| v.into()).collect::<Vec<Node>>();
|
||||||
|
children.insert(0, n.cond.as_ref().into());
|
||||||
|
children.push(n.final_else.as_ref().into());
|
||||||
|
children
|
||||||
|
}
|
||||||
|
Node::VariableDeclaration(n) => vec![(&n.declaration).into()],
|
||||||
|
Node::ReturnStatement(n) => {
|
||||||
|
vec![(&n.argument).into()]
|
||||||
|
}
|
||||||
|
Node::VariableDeclarator(n) => {
|
||||||
|
vec![(&n.id).into(), (&n.init).into()]
|
||||||
|
}
|
||||||
|
Node::UnaryExpression(n) => {
|
||||||
|
vec![(&n.argument).into()]
|
||||||
|
}
|
||||||
|
Node::Parameter(n) => {
|
||||||
|
vec![(&n.identifier).into()]
|
||||||
|
}
|
||||||
|
Node::ObjectProperty(n) => {
|
||||||
|
vec![(&n.value).into()]
|
||||||
|
}
|
||||||
|
Node::ElseIf(n) => {
|
||||||
|
vec![(&n.cond).into(), n.then_val.as_ref().into()]
|
||||||
|
}
|
||||||
|
Node::LabelledExpression(e) => {
|
||||||
|
vec![(&e.expr).into(), (&e.label).into()]
|
||||||
|
}
|
||||||
|
Node::PipeSubstitution(_)
|
||||||
|
| Node::TagDeclarator(_)
|
||||||
|
| Node::Identifier(_)
|
||||||
|
| Node::ImportStatement(_)
|
||||||
|
| Node::MemberObject(_)
|
||||||
|
| Node::LiteralIdentifier(_)
|
||||||
|
| Node::KclNone(_)
|
||||||
|
| Node::Literal(_) => vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
macro_rules! kcl {
|
||||||
|
( $kcl:expr ) => {{
|
||||||
|
$crate::parsing::top_level_parse($kcl).unwrap()
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn count_crows() {
|
||||||
|
let program = kcl!(
|
||||||
|
"\
|
||||||
|
const crow1 = 1
|
||||||
|
const crow2 = 2
|
||||||
|
|
||||||
|
fn crow3() {
|
||||||
|
const crow4 = 3
|
||||||
|
crow5()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
);
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct CountCrows {
|
||||||
|
n: Box<Mutex<usize>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'tree> Visitor<'tree> for &CountCrows {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn visit_node(&self, node: Node<'tree>) -> Result<bool, Self::Error> {
|
||||||
|
if let Node::VariableDeclarator(vd) = node {
|
||||||
|
if vd.id.name.starts_with("crow") {
|
||||||
|
*self.n.lock().unwrap() += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for child in node.children().iter() {
|
||||||
|
if !child.visit(*self)? {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let prog: Node = (&program).into();
|
||||||
|
let count_crows: CountCrows = Default::default();
|
||||||
|
Visitable::visit(&prog, &count_crows).unwrap();
|
||||||
|
assert_eq!(*count_crows.n.lock().unwrap(), 4);
|
||||||
|
}
|
||||||
|
}
|
@ -1,329 +1,55 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use super::ast_visitor::{Visitable, Visitor};
|
||||||
use crate::{
|
use crate::{
|
||||||
parsing::ast::types::{
|
parsing::ast::types::{NodeRef, Program},
|
||||||
BinaryPart, BodyItem, Expr, IfExpression, LiteralIdentifier, MemberExpression, MemberObject, NodeRef,
|
|
||||||
ObjectExpression, ObjectProperty, Parameter, Program, UnaryExpression, VariableDeclarator,
|
|
||||||
},
|
|
||||||
walk::Node,
|
walk::Node,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Walker is implemented by things that are able to walk an AST tree to
|
/// *DEPRECATED* Walk trait.
|
||||||
/// produce lints. This trait is implemented automatically for a few of the
|
///
|
||||||
/// common types, but can be manually implemented too.
|
/// This was written before [Visitor], which is the better way to traverse
|
||||||
|
/// a AST.
|
||||||
|
///
|
||||||
|
/// This trait continues to exist in order to not change all the linter
|
||||||
|
/// as we refine the walk code.
|
||||||
|
///
|
||||||
|
/// This, internally, uses the new [Visitor] trait, and is only provided as
|
||||||
|
/// a stub until we migrate all existing code off this trait.
|
||||||
pub trait Walker<'a> {
|
pub trait Walker<'a> {
|
||||||
/// Walk will visit every element of the AST.
|
/// Walk will visit every element of the AST, recursing through the
|
||||||
|
/// whole tree.
|
||||||
fn walk(&self, n: Node<'a>) -> Result<bool>;
|
fn walk(&self, n: Node<'a>) -> Result<bool>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, FnT> Walker<'a> for FnT
|
impl<'tree, VisitorT> Walker<'tree> for VisitorT
|
||||||
where
|
where
|
||||||
FnT: Fn(Node<'a>) -> Result<bool>,
|
VisitorT: Visitor<'tree>,
|
||||||
|
VisitorT: Clone,
|
||||||
|
anyhow::Error: From<VisitorT::Error>,
|
||||||
|
VisitorT::Error: Send,
|
||||||
|
VisitorT::Error: Sync,
|
||||||
{
|
{
|
||||||
fn walk(&self, n: Node<'a>) -> Result<bool> {
|
fn walk(&self, n: Node<'tree>) -> Result<bool> {
|
||||||
self(n)
|
if !n.visit(self.clone())? {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
for child in n.children() {
|
||||||
|
if !Self::walk(self, child)? {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the Walker against all [Node]s in a [Program].
|
/// Run the Walker against all [Node]s in a [Program].
|
||||||
pub fn walk<'a, WalkT>(prog: NodeRef<'a, Program>, f: &WalkT) -> Result<bool>
|
pub fn walk<'a, WalkT>(prog: NodeRef<'a, Program>, f: WalkT) -> Result<bool>
|
||||||
where
|
where
|
||||||
WalkT: Walker<'a>,
|
WalkT: Walker<'a>,
|
||||||
{
|
{
|
||||||
if !f.walk(prog.into())? {
|
let prog: Node = prog.into();
|
||||||
return Ok(false);
|
f.walk(prog)
|
||||||
}
|
|
||||||
|
|
||||||
for bi in &prog.body {
|
|
||||||
if !walk_body_item(bi, f)? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn walk_variable_declarator<'a, WalkT>(node: NodeRef<'a, VariableDeclarator>, f: &WalkT) -> Result<bool>
|
|
||||||
where
|
|
||||||
WalkT: Walker<'a>,
|
|
||||||
{
|
|
||||||
if !f.walk(node.into())? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
if !f.walk((&node.id).into())? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
walk_value(&node.init, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn walk_parameter<'a, WalkT>(node: &'a Parameter, f: &WalkT) -> Result<bool>
|
|
||||||
where
|
|
||||||
WalkT: Walker<'a>,
|
|
||||||
{
|
|
||||||
if !f.walk(node.into())? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
f.walk((&node.identifier).into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn walk_member_object<'a, WalkT>(node: &'a MemberObject, f: &WalkT) -> Result<bool>
|
|
||||||
where
|
|
||||||
WalkT: Walker<'a>,
|
|
||||||
{
|
|
||||||
f.walk(node.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn walk_literal_identifier<'a, WalkT>(node: &'a LiteralIdentifier, f: &WalkT) -> Result<bool>
|
|
||||||
where
|
|
||||||
WalkT: Walker<'a>,
|
|
||||||
{
|
|
||||||
f.walk(node.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn walk_member_expression<'a, WalkT>(node: NodeRef<'a, MemberExpression>, f: &WalkT) -> Result<bool>
|
|
||||||
where
|
|
||||||
WalkT: Walker<'a>,
|
|
||||||
{
|
|
||||||
if !f.walk(node.into())? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !walk_member_object(&node.object, f)? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
walk_literal_identifier(&node.property, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn walk_binary_part<'a, WalkT>(node: &'a BinaryPart, f: &WalkT) -> Result<bool>
|
|
||||||
where
|
|
||||||
WalkT: Walker<'a>,
|
|
||||||
{
|
|
||||||
match node {
|
|
||||||
BinaryPart::Literal(lit) => f.walk(lit.as_ref().into()),
|
|
||||||
BinaryPart::Identifier(id) => f.walk(id.as_ref().into()),
|
|
||||||
BinaryPart::BinaryExpression(be) => f.walk(be.as_ref().into()),
|
|
||||||
BinaryPart::CallExpression(ce) => f.walk(ce.as_ref().into()),
|
|
||||||
BinaryPart::CallExpressionKw(ce) => f.walk(ce.as_ref().into()),
|
|
||||||
BinaryPart::UnaryExpression(ue) => walk_unary_expression(ue, f),
|
|
||||||
BinaryPart::MemberExpression(me) => walk_member_expression(me, f),
|
|
||||||
BinaryPart::IfExpression(e) => walk_if_expression(e, f),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Rename this to walk_expr
|
|
||||||
fn walk_value<'a, WalkT>(node: &'a Expr, f: &WalkT) -> Result<bool>
|
|
||||||
where
|
|
||||||
WalkT: Walker<'a>,
|
|
||||||
{
|
|
||||||
match node {
|
|
||||||
Expr::Literal(lit) => f.walk(lit.as_ref().into()),
|
|
||||||
Expr::TagDeclarator(tag) => f.walk(tag.as_ref().into()),
|
|
||||||
|
|
||||||
Expr::Identifier(id) => {
|
|
||||||
// sometimes there's a bare Identifier without a Value::Identifier.
|
|
||||||
f.walk(id.as_ref().into())
|
|
||||||
}
|
|
||||||
|
|
||||||
Expr::BinaryExpression(be) => {
|
|
||||||
if !f.walk(be.as_ref().into())? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
if !walk_binary_part(&be.left, f)? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
walk_binary_part(&be.right, f)
|
|
||||||
}
|
|
||||||
Expr::FunctionExpression(fe) => {
|
|
||||||
if !f.walk(fe.as_ref().into())? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
for arg in &fe.params {
|
|
||||||
if !walk_parameter(arg, f)? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
walk(&fe.body, f)
|
|
||||||
}
|
|
||||||
Expr::CallExpression(ce) => {
|
|
||||||
if !f.walk(ce.as_ref().into())? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !f.walk((&ce.callee).into())? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
for e in &ce.arguments {
|
|
||||||
if !walk_value::<WalkT>(e, f)? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
Expr::CallExpressionKw(ce) => {
|
|
||||||
if !f.walk(ce.as_ref().into())? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !f.walk((&ce.callee).into())? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
if let Some(ref e) = ce.unlabeled {
|
|
||||||
if !walk_value::<WalkT>(e, f)? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for e in &ce.arguments {
|
|
||||||
if !walk_value::<WalkT>(&e.arg, f)? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
Expr::PipeExpression(pe) => {
|
|
||||||
if !f.walk(pe.as_ref().into())? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
for e in &pe.body {
|
|
||||||
if !walk_value::<WalkT>(e, f)? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
Expr::PipeSubstitution(ps) => f.walk(ps.as_ref().into()),
|
|
||||||
Expr::ArrayExpression(ae) => {
|
|
||||||
if !f.walk(ae.as_ref().into())? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
for e in &ae.elements {
|
|
||||||
if !walk_value::<WalkT>(e, f)? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
Expr::ArrayRangeExpression(are) => {
|
|
||||||
if !f.walk(are.as_ref().into())? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
if !walk_value::<WalkT>(&are.start_element, f)? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
if !walk_value::<WalkT>(&are.end_element, f)? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
Expr::ObjectExpression(oe) => walk_object_expression(oe, f),
|
|
||||||
Expr::MemberExpression(me) => walk_member_expression(me, f),
|
|
||||||
Expr::UnaryExpression(ue) => walk_unary_expression(ue, f),
|
|
||||||
Expr::IfExpression(e) => walk_if_expression(e, f),
|
|
||||||
Expr::None(_) => Ok(true),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Walk through an [ObjectProperty].
|
|
||||||
fn walk_object_property<'a, WalkT>(node: NodeRef<'a, ObjectProperty>, f: &WalkT) -> Result<bool>
|
|
||||||
where
|
|
||||||
WalkT: Walker<'a>,
|
|
||||||
{
|
|
||||||
if !f.walk(node.into())? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
walk_value(&node.value, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Walk through an [ObjectExpression].
|
|
||||||
fn walk_object_expression<'a, WalkT>(node: NodeRef<'a, ObjectExpression>, f: &WalkT) -> Result<bool>
|
|
||||||
where
|
|
||||||
WalkT: Walker<'a>,
|
|
||||||
{
|
|
||||||
if !f.walk(node.into())? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
for prop in &node.properties {
|
|
||||||
if !walk_object_property(prop, f)? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Walk through an [IfExpression].
|
|
||||||
fn walk_if_expression<'a, WalkT>(node: NodeRef<'a, IfExpression>, f: &WalkT) -> Result<bool>
|
|
||||||
where
|
|
||||||
WalkT: Walker<'a>,
|
|
||||||
{
|
|
||||||
if !f.walk(node.into())? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
if !walk_value(&node.cond, f)? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
for else_if in &node.else_ifs {
|
|
||||||
if !walk_value(&else_if.cond, f)? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
if !walk(&else_if.then_val, f)? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let final_else = &(*node.final_else);
|
|
||||||
if !f.walk(final_else.into())? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// walk through an [UnaryExpression].
|
|
||||||
fn walk_unary_expression<'a, WalkT>(node: NodeRef<'a, UnaryExpression>, f: &WalkT) -> Result<bool>
|
|
||||||
where
|
|
||||||
WalkT: Walker<'a>,
|
|
||||||
{
|
|
||||||
if !f.walk(node.into())? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
walk_binary_part(&node.argument, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// walk through a [BodyItem].
|
|
||||||
fn walk_body_item<'a, WalkT>(node: &'a BodyItem, f: &WalkT) -> Result<bool>
|
|
||||||
where
|
|
||||||
WalkT: Walker<'a>,
|
|
||||||
{
|
|
||||||
// We don't walk a BodyItem since it's an enum itself.
|
|
||||||
|
|
||||||
match node {
|
|
||||||
BodyItem::ImportStatement(xs) => {
|
|
||||||
if !f.walk(xs.as_ref().into())? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
BodyItem::ExpressionStatement(xs) => {
|
|
||||||
if !f.walk(xs.into())? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
walk_value(&xs.expression, f)
|
|
||||||
}
|
|
||||||
BodyItem::VariableDeclaration(vd) => {
|
|
||||||
if !f.walk(vd.as_ref().into())? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
walk_variable_declarator(&vd.declaration, f)
|
|
||||||
}
|
|
||||||
BodyItem::ReturnStatement(rs) => {
|
|
||||||
if !f.walk(rs.into())? {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
walk_value(&rs.argument, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -345,10 +71,10 @@ const bar = 2
|
|||||||
"
|
"
|
||||||
);
|
);
|
||||||
|
|
||||||
walk(&program, &|node| {
|
walk(&program, |node| {
|
||||||
if let Node::VariableDeclarator(vd) = node {
|
if let Node::VariableDeclarator(vd) = node {
|
||||||
if vd.id.name == "foo" {
|
if vd.id.name == "foo" {
|
||||||
return Ok(false);
|
return Ok::<bool, anyhow::Error>(false);
|
||||||
}
|
}
|
||||||
panic!("walk didn't stop");
|
panic!("walk didn't stop");
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user