Compare commits

..

82 Commits

Author SHA1 Message Date
9407162543 Merge branch 'main' into pierremtb/issue4472-Add-shell-point-and-click-operation-backup 2024-12-09 14:04:18 -05:00
067b83b468 Manual resolution of snapshot conflicts 2024-12-09 13:56:33 -05:00
c5b30341eb Add unit tests for doesSceneHaveExtrudedSketch 2024-12-09 12:26:21 -05:00
3e6441b563 Fix test annotations 2024-12-09 11:36:46 -05:00
acafcf2d4d Apply suggestions from Frank's review
Co-authored-by: Frank Noirot <frank@zoo.dev>
2024-12-09 11:33:41 -05:00
b9f31d94d5 Merge branch 'main' into pierremtb/issue4472-Add-shell-point-and-click-operation 2024-12-05 18:43:44 -05:00
9e03b58ae5 Cap and wall pw test 2024-12-05 18:21:20 -05:00
c591f73c70 Working multi-face shell across types 2024-12-05 17:56:18 -05:00
9330aaba13 Trigger CI 2024-12-05 17:27:16 -05:00
5a14f0189e Look at this (photo)Graph *in the voice of Nickelback* 2024-12-05 22:14:20 +00:00
9af001f22e Lint 2024-12-05 17:04:02 -05:00
435d1ea52e WIP circular dep 2024-12-05 16:53:57 -05:00
54847139f2 Merge branch 'main' into pierremtb/issue4472-Add-shell-point-and-click-operation 2024-12-05 15:00:34 -05:00
9369a17ea7 WIP mutliple faces 2024-12-05 14:59:44 -05:00
9ee2e7c3b0 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-05 19:42:39 +00:00
28ae261e5f Lint fix 2024-12-05 14:38:23 -05:00
e4b0de0ead Add selection guard and clean up 2024-12-05 14:06:09 -05:00
12859598a3 Merge branch 'main' into pierremtb/issue4472-Add-shell-point-and-click-operation 2024-12-05 13:02:35 -05:00
e9a334f433 Fix lint 2024-12-05 13:02:22 -05:00
e470a7b4af Add shell wall test 2024-12-05 12:33:46 -05:00
602c39f63c Add pw tests for cap shell 2024-12-05 12:03:45 -05:00
4994aa6f61 Handle walls 2024-12-05 11:38:03 -05:00
4aa07b81db Add extrude lookup for more generic shell 2024-12-05 10:26:58 -05:00
89ef4b3243 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-05 14:55:04 +00:00
001c9a8219 Update from main 2024-12-05 09:49:14 -05:00
4dde3f60e0 WIP: first time working shell mod 2024-12-04 18:41:45 -05:00
f407c53032 WIP: closer 2024-12-04 17:53:04 -05:00
9ba584487a WIP: more additions 2024-12-04 17:01:11 -05:00
96b66d6bca Merge branch 'pierremtb/issue4470-loft-ui' into pierremtb/issue4472-Add-shell-point-and-click-operation 2024-12-04 16:44:40 -05:00
8d66f3ffad Rollback pw values to pre cam change 2024-12-04 16:43:29 -05:00
f4c54cbbe4 WIP: initial shell code addition 2024-12-04 16:38:44 -05:00
5156b847f3 Trigger CI 2024-12-04 15:56:48 -05:00
ded9f2c56b A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) 2024-12-04 20:54:59 +00:00
5db5f79f9a Merge branch 'main' into pierremtb/issue4470-loft-ui 2024-12-04 15:46:18 -05:00
6a883f4a8d A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-04 20:43:39 +00:00
07b91f0fb1 Trigger CI 2024-12-04 15:39:26 -05:00
1b2e213afe A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) 2024-12-04 20:37:28 +00:00
f48a23c35e A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-04 20:36:20 +00:00
7e1d102496 Revert snapshots 2024-12-04 15:32:14 -05:00
94cb2535c0 Fix typo 2024-12-04 15:21:03 -05:00
9e08ec9096 A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) 2024-12-04 20:00:36 +00:00
17bd8ec32a A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-04 19:58:28 +00:00
d0f12e85e5 Trigger CI 2024-12-04 14:54:05 -05:00
94d185944e A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) 2024-12-04 19:52:22 +00:00
c38b2270c3 A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) 2024-12-04 19:51:34 +00:00
967ad66c98 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-04 19:47:04 +00:00
afeca9ca39 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-04 19:46:25 +00:00
61242282f0 Trigger CI 2024-12-04 14:42:00 -05:00
0065df13ce A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) 2024-12-04 16:55:39 +00:00
01c8d45c13 Remove comments 2024-12-04 11:44:49 -05:00
8b25527f21 Move error logic out of loftSketches, fix pw tests 2024-12-04 11:39:35 -05:00
2abd980de9 Move to fromPromise-based Actor 2024-12-04 11:02:51 -05:00
f783deb706 A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) 2024-12-04 15:25:38 +00:00
f4dd295ca1 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-04 15:17:19 +00:00
ceaa85fe3f A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-04 15:16:16 +00:00
3991bd9173 Trigger CI 2024-12-04 10:11:47 -05:00
b8f9da36c0 A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) 2024-12-04 10:54:14 +00:00
283315b5d2 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-04 10:44:01 +00:00
e204dfe564 A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) 2024-12-04 10:43:15 +00:00
208a36196b Merge branch 'main' into pierremtb/issue4470-loft-ui 2024-12-04 05:38:43 -05:00
660a349588 Add pw test for preselected sketches 2024-12-03 15:17:21 -05:00
56c37da317 Clean up loftSketches function 2024-12-03 15:02:56 -05:00
a46734b76d Add test for doesSceneHaveSweepableSketch with count = 2 2024-12-03 14:39:06 -05:00
4347e0cf84 Clean up and working pw test 2024-12-03 13:49:26 -05:00
df3e541cdf A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-03 17:51:57 +00:00
b1cec443b9 Merge branch 'main' into pierremtb/issue4470-loft-ui 2024-12-03 12:48:32 -05:00
967f49055d Lint 2024-12-02 14:30:43 -05:00
fe977524b5 First point-click loft test (not working locally, loft gets inserted at the wrong place) 2024-12-02 14:19:32 -05:00
d6f271fb0f Enable multiple selections after the button click 2024-12-01 07:00:13 -05:00
e851b2bcc4 Merge branch 'main' into pierremtb/issue4470-loft-ui 2024-11-29 20:01:32 -05:00
be569c91de Clean up 2024-11-29 19:51:56 -05:00
5080e304b9 Appends the loft line after the 'last' sketch in the code 2024-11-29 18:30:15 -05:00
f4e75b7b4f More checks 2024-11-29 13:38:52 -05:00
31cbc90f56 WIP selections 2024-11-29 12:37:53 -05:00
a7d3552472 WIP selections 2024-11-29 11:17:31 -05:00
e984b20664 First pass at handling more than 2 sketches 2024-11-29 10:09:10 -05:00
0c2cd24bda Working loft for two sketches in the right hardcoded order 2024-11-28 20:28:37 -05:00
9b2de237b8 Add selection guard 2024-11-28 16:45:41 -05:00
c79c02f18e A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) 2024-11-28 20:25:44 +00:00
4851aa2d71 A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) 2024-11-28 20:17:17 +00:00
76fafa6fd0 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-11-28 20:11:35 +00:00
647ca11e08 WIP: experimenting with Loft UI
Relates to #4470
2024-11-27 15:55:01 -05:00
85 changed files with 3016 additions and 5129 deletions

View File

@ -45,7 +45,7 @@ circles = map([1..3], drawCircle)
```js
r = 10 // radius
// Call `map`, using an anonymous function instead of a named one.
circles = map([1..3], fn(id) {
circles = map([1..3], (id) {
return startSketchOn("XY")
|> circle({ center = [id * 2 * r, 0], radius = r }, %)
})

View File

@ -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
// named function outside.
arr = [1, 2, 3]
sum = reduce(arr, 0, fn(i, result_so_far) {
sum = reduce(arr, 0, (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.
// 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.
fullDecagon = reduce([1..10], startOfDecagonSketch, fn(i, partialDecagon) {
fullDecagon = reduce([1..10], startOfDecagonSketch, (i, partialDecagon) {
// Draw one edge of the decagon.
x = cos(stepAngle * i) * radius
y = sin(stepAngle * i) * radius

View File

@ -100436,7 +100436,7 @@
"deprecated": false,
"examples": [
"r = 10 // radius\nfn drawCircle(id) {\n return startSketchOn(\"XY\")\n |> circle({ center = [id * 2 * r, 0], radius = r }, %)\n}\n\n// Call `drawCircle`, passing in each element of the array.\n// The outputs from each `drawCircle` form a new array,\n// which is the return value from `map`.\ncircles = map([1..3], drawCircle)",
"r = 10 // radius\n// Call `map`, using an anonymous function instead of a named one.\ncircles = map([1..3], fn(id) {\n return startSketchOn(\"XY\")\n |> circle({ center = [id * 2 * r, 0], radius = r }, %)\n})"
"r = 10 // radius\n// Call `map`, using an anonymous function instead of a named one.\ncircles = map([1..3], (id) {\n return startSketchOn(\"XY\")\n |> circle({ center = [id * 2 * r, 0], radius = r }, %)\n})"
]
},
{
@ -146129,8 +146129,8 @@
"deprecated": false,
"examples": [
"// This function adds two numbers.\nfn add(a, b) {\n return a + b\n}\n\n// This function adds an array of numbers.\n// It uses the `reduce` function, to call the `add` function on every\n// element of the `arr` parameter. The starting value is 0.\nfn sum(arr) {\n return reduce(arr, 0, add)\n}\n\n/* The above is basically like this pseudo-code:\nfn sum(arr):\n let sumSoFar = 0\n for i in arr:\n sumSoFar = add(sumSoFar, i)\n return sumSoFar */\n\n\n// We use `assertEqual` to check that our `sum` function gives the\n// expected result. It's good to check your work!\nassertEqual(sum([1, 2, 3]), 6, 0.00001, \"1 + 2 + 3 summed is 6\")",
"// This example works just like the previous example above, but it uses\n// an anonymous `add` function as its parameter, instead of declaring a\n// named function outside.\narr = [1, 2, 3]\nsum = reduce(arr, 0, fn(i, result_so_far) {\n return i + result_so_far\n})\n\n// We use `assertEqual` to check that our `sum` function gives the\n// expected result. It's good to check your work!\nassertEqual(sum, 6, 0.00001, \"1 + 2 + 3 summed is 6\")",
"// Declare a function that sketches a decagon.\nfn decagon(radius) {\n // Each side of the decagon is turned this many degrees from the previous angle.\n stepAngle = 1 / 10 * tau()\n\n // Start the decagon sketch at this point.\n startOfDecagonSketch = startSketchAt([cos(0) * radius, sin(0) * radius])\n\n // Use a `reduce` to draw the remaining decagon sides.\n // For each number in the array 1..10, run the given function,\n // which takes a partially-sketched decagon and adds one more edge to it.\n fullDecagon = reduce([1..10], startOfDecagonSketch, fn(i, partialDecagon) {\n // Draw one edge of the decagon.\n x = cos(stepAngle * i) * radius\n y = sin(stepAngle * i) * radius\n return lineTo([x, y], partialDecagon)\n })\n\n return fullDecagon\n}\n\n/* The `decagon` above is basically like this pseudo-code:\nfn decagon(radius):\n let stepAngle = (1/10) * tau()\n let startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)])\n\n // Here's the reduce part.\n let partialDecagon = startOfDecagonSketch\n for i in [1..10]:\n let x = cos(stepAngle * i) * radius\n let y = sin(stepAngle * i) * radius\n partialDecagon = lineTo([x, y], partialDecagon)\n fullDecagon = partialDecagon // it's now full\n return fullDecagon */\n\n\n// Use the `decagon` function declared above, to sketch a decagon with radius 5.\ndecagon(5.0)\n |> close(%)"
"// This example works just like the previous example above, but it uses\n// an anonymous `add` function as its parameter, instead of declaring a\n// named function outside.\narr = [1, 2, 3]\nsum = reduce(arr, 0, (i, result_so_far) {\n return i + result_so_far\n})\n\n// We use `assertEqual` to check that our `sum` function gives the\n// expected result. It's good to check your work!\nassertEqual(sum, 6, 0.00001, \"1 + 2 + 3 summed is 6\")",
"// Declare a function that sketches a decagon.\nfn decagon(radius) {\n // Each side of the decagon is turned this many degrees from the previous angle.\n stepAngle = 1 / 10 * tau()\n\n // Start the decagon sketch at this point.\n startOfDecagonSketch = startSketchAt([cos(0) * radius, sin(0) * radius])\n\n // Use a `reduce` to draw the remaining decagon sides.\n // For each number in the array 1..10, run the given function,\n // which takes a partially-sketched decagon and adds one more edge to it.\n fullDecagon = reduce([1..10], startOfDecagonSketch, (i, partialDecagon) {\n // Draw one edge of the decagon.\n x = cos(stepAngle * i) * radius\n y = sin(stepAngle * i) * radius\n return lineTo([x, y], partialDecagon)\n })\n\n return fullDecagon\n}\n\n/* The `decagon` above is basically like this pseudo-code:\nfn decagon(radius):\n let stepAngle = (1/10) * tau()\n let startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)])\n\n // Here's the reduce part.\n let partialDecagon = startOfDecagonSketch\n for i in [1..10]:\n let x = cos(stepAngle * i) * radius\n let y = sin(stepAngle * i) * radius\n partialDecagon = lineTo([x, y], partialDecagon)\n fullDecagon = partialDecagon // it's now full\n return fullDecagon */\n\n\n// Use the `decagon` function declared above, to sketch a decagon with radius 5.\ndecagon(5.0)\n |> close(%)"
]
},
{

View File

@ -812,7 +812,6 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
commandName: 'Shell',
})
await clickOnCap()
await app.page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
@ -828,7 +827,6 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
} 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 () => {

View File

@ -950,75 +950,7 @@ test(
test.describe('Grid visibility', { tag: '@snapshot' }, () => {
// FIXME: Skip on macos its being weird.
// 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.skip(process.platform === 'darwin', 'Skip on macos')
test('Grid turned off', async ({ page }) => {
const u = await getUtils(page)

View File

@ -26,17 +26,7 @@ test.describe('Testing constraints', () => {
})
const u = await getUtils(page)
// 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',
})
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
@ -46,26 +36,26 @@ test.describe('Testing constraints', () => {
await u.closeDebugPanel()
// Click the line of code for line.
// TODO remove this and reinstate `await topHorzSegmentClick()`
await page.getByText(`line([0, ${lengthValue.old}], %)`).click()
await page.getByText(`line([0, 20], %)`).click() // TODO remove this and reinstate // await topHorzSegmentClick()
await page.waitForTimeout(100)
// enter sketch again
await page.getByRole('button', { name: 'Edit Sketch' }).click()
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
.getByRole('button', { name: 'dimension Length', exact: true })
.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 page.getByText('Add constraining value').click()
await expect(page.locator('.cm-content')).toHaveText(
`length001 = ${lengthValue.new}sketch001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> angledLine([90, length001], %) |> xLine(-20, %)`
`length001 = 20sketch001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> angledLine([90, length001], %) |> xLine(-20, %)`
)
// Make sure we didn't pop out of sketch mode.
@ -76,6 +66,7 @@ test.describe('Testing constraints', () => {
await page.waitForTimeout(500) // wait for animation
// Exit sketch
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
await page.keyboard.press('Escape')
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
@ -533,7 +524,7 @@ part002 = startSketchOn('XZ')
})
}
})
test.describe('Test Angle constraint single selection', () => {
test.describe('Test Angle/Length constraint single selection', () => {
const cases = [
{
testName: 'Angle - Add variable',
@ -547,6 +538,18 @@ part002 = startSketchOn('XZ')
constraint: 'angle',
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
for (const { testName, addVariable, value, constraint } of cases) {
test(`${testName}`, async ({ page }) => {
@ -605,90 +608,6 @@ 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', () => {
const cases = [
{
@ -949,15 +868,6 @@ part002 = startSketchOn('XZ')
|> 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)
await page.setViewportSize({ width: 1200, height: 500 })
@ -1018,8 +928,8 @@ part002 = startSketchOn('XZ')
// await page.getByRole('button', { name: 'length', exact: true }).click()
await page.getByTestId('dropdown-constraint-length').click()
await cmdBarKclInput.fill('10')
await cmdBarSubmitButton.click()
await page.getByLabel('length Value').fill('10')
await page.getByRole('button', { name: 'Add constraining value' }).click()
activeLinesContent = await page.locator('.cm-activeLine').all()
await expect(activeLinesContent[0]).toHaveText(`|> xLine(length001, %)`)

View File

@ -91,14 +91,7 @@ test.describe('Testing segment overlays', () => {
await page.getByTestId('constraint-symbol-popover').count()
).toBeGreaterThan(0)
await unconstrainedLocator.click()
await expect(
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page
.getByRole('button', {
name: 'arrow right Continue',
})
.click()
await page.getByText('Add variable').click()
await expect(page.locator('.cm-content')).toContainText(expectFinal)
}
@ -158,14 +151,7 @@ test.describe('Testing segment overlays', () => {
await page.getByTestId('constraint-symbol-popover').count()
).toBeGreaterThan(0)
await unconstrainedLocator.click()
await expect(
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page
.getByRole('button', {
name: 'arrow right Continue',
})
.click()
await page.getByText('Add variable').click()
await expect(page.locator('.cm-content')).toContainText(
expectAfterUnconstrained
)

View File

@ -505,8 +505,7 @@ const ConstraintSymbol = ({
constrainInfo: ConstrainInfo
verticalPosition: 'top' | 'bottom'
}) => {
const { commandBarSend } = useCommandsContext()
const { context } = useModelingContext()
const { context, send } = useModelingContext()
const varNameMap: {
[key in ConstrainInfo['type']]: {
varName: string
@ -625,18 +624,11 @@ const ConstraintSymbol = ({
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
onClick={toSync(async () => {
if (!isConstrained) {
commandBarSend({
type: 'Find and select command',
send({
type: 'Convert to variable',
data: {
name: 'Constrain with named value',
groupId: 'modeling',
argDefaultValues: {
currentValue: {
pathToNode,
variableName: varName,
valueText: value,
},
},
pathToNode,
variableName: varName,
},
})
} else if (isConstrained) {

View File

@ -8,16 +8,11 @@ import { getSystemTheme } from 'lib/theme'
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
import { roundOff } from 'lib/utils'
import { varMentions } from 'lib/varCompletionExtension'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import styles from './CommandBarKclInput.module.css'
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
import { useSelector } from '@xstate/react'
const machineContextSelector = (snapshot?: {
context: Record<string, unknown>
}) => snapshot?.context
function CommandBarKclInput({
arg,
@ -36,44 +31,12 @@ function CommandBarKclInput({
arg.name
] as KclCommandValue | undefined
const { settings } = useSettingsAuthContext()
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 defaultValue = (arg.defaultValue as string) || ''
const [value, setValue] = useState(
previouslySetValue?.valueText || defaultValue || ''
)
const [createNewVariable, setCreateNewVariable] = useState(
(previouslySetValue && 'variableName' in previouslySetValue) ||
arg.createVariableByDefault ||
false
previouslySetValue && 'variableName' in previouslySetValue
)
const [canSubmit, setCanSubmit] = useState(true)
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
@ -89,7 +52,10 @@ function CommandBarKclInput({
isNewVariableNameUnique,
} = useCalculateKclExpression({
value,
initialVariableName,
initialVariableName:
previouslySetValue && 'variableName' in previouslySetValue
? previouslySetValue.variableName
: arg.name,
})
const varMentionData: Completion[] = prevVariables.map((v) => ({
label: v.key,

View File

@ -1,4 +1,4 @@
import { APP_VERSION, RELEASE_URL } from 'routes/Settings'
import { APP_VERSION } from 'routes/Settings'
import { CustomIcon } from 'components/CustomIcon'
import Tooltip from 'components/Tooltip'
import { PATHS } from 'lib/paths'
@ -72,8 +72,10 @@ export function LowerRightControls({
<menu className="flex items-center justify-end gap-3 pointer-events-auto">
{!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />}
<a
onClick={openExternalBrowserIfDesktop(RELEASE_URL)}
href={RELEASE_URL}
onClick={openExternalBrowserIfDesktop(
`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`
)}
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`}
target="_blank"
rel="noopener noreferrer"
className={'!no-underline font-mono text-xs ' + linkOverrideClassName}

View File

@ -69,7 +69,14 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const [isKclLspReady, setIsKclLspReady] = useState(false)
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
const { auth } = useSettingsAuthContext()
const {
auth,
settings: {
context: {
modeling: { defaultUnit },
},
},
} = useSettingsAuthContext()
const token = auth?.context.token
const navigate = useNavigate()
@ -85,6 +92,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const initEvent: KclWorkerOptions = {
wasmUrl: wasmUrl(),
token: token,
baseUnit: defaultUnit.current,
apiBaseUrl: VITE_KC_API_BASE_URL,
}
lspWorker.postMessage({

View File

@ -41,10 +41,7 @@ import {
angleBetweenInfo,
applyConstraintAngleBetween,
} from './Toolbar/SetAngleBetween'
import {
applyConstraintAngleLength,
applyConstraintLength,
} from './Toolbar/setAngleLength'
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
import {
canSweepSelection,
handleSelectionBatch,
@ -66,13 +63,12 @@ import {
getSketchOrientationDetails,
} from 'clientSideScene/sceneEntities'
import {
insertNamedConstant,
replaceValueAtNodePath,
moveValueIntoNewVariablePath,
sketchOnExtrudedFace,
sketchOnOffsetPlane,
startSketchOnDefault,
} from 'lang/modifyAst'
import { PathToNode, Program, parse, recast, resultIsOk } from 'lang/wasm'
import { Program, parse, recast, resultIsOk } from 'lang/wasm'
import {
doesSceneHaveExtrudedSketch,
doesSceneHaveSweepableSketch,
@ -85,6 +81,7 @@ import toast from 'react-hot-toast'
import { EditorSelection, Transaction } from '@codemirror/state'
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
import { getVarNameModal } from 'hooks/useToolbarGuards'
import { err, reportRejection, trap } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { modelingMachineEvent } from 'editor/manager'
@ -892,18 +889,12 @@ export const ModelingMachineProvider = ({
}
}
),
astConstrainLength: fromPromise(
async ({
input: { selectionRanges, sketchDetails, lengthValue },
}) => {
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
'Get length info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => {
const { modifiedAst, pathToNodeMap } =
await applyConstraintAngleLength({
selectionRanges,
})
const pResult = parse(recast(modifiedAst))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error'))
@ -1072,88 +1063,38 @@ export const ModelingMachineProvider = ({
}
}
),
'Apply named value constraint': fromPromise(
'Get convert to variable info': fromPromise(
async ({ input: { selectionRanges, sketchDetails, data } }) => {
if (!sketchDetails) {
if (!sketchDetails)
return Promise.reject(new Error('No sketch details'))
}
if (!data) {
return Promise.reject(new Error('No data from command flow'))
}
const { variableName } = await getVarNameModal({
valueName: data?.variableName || 'var',
})
let pResult = parse(recast(kclManager.ast))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error'))
let parsed = pResult.program
let result: {
modifiedAst: Node<Program>
pathToReplaced: PathToNode | null
} = {
modifiedAst: parsed,
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,
})
)
const { modifiedAst: _modifiedAst, pathToReplacedNode } =
moveValueIntoNewVariablePath(
parsed,
kclManager.programMemory,
data?.pathToNode || [],
variableName
)
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))
pResult = parse(recast(_modifiedAst))
if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error'))
parsed = pResult.program
if (trap(parsed)) return Promise.reject(parsed)
parsed = parsed as Node<Program>
if (!result.pathToReplaced)
if (!pathToReplacedNode)
return Promise.reject(new Error('No path to replaced node'))
const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch(
result.pathToReplaced || [],
pathToReplacedNode || [],
parsed,
sketchDetails.zAxis,
sketchDetails.yAxis,
@ -1166,7 +1107,7 @@ export const ModelingMachineProvider = ({
)
const selection = updateSelections(
{ 0: result.pathToReplaced },
{ 0: pathToReplacedNode },
selectionRanges,
updatedAst.newAst
)
@ -1174,7 +1115,7 @@ export const ModelingMachineProvider = ({
return {
selectionType: 'completeSelection',
selection,
updatedPathToNode: result.pathToReplaced,
updatedPathToNode: pathToReplacedNode,
}
}
),

View File

@ -10,7 +10,7 @@ interface AllKeybindingsFieldsProps {}
export const AllKeybindingsFields = forwardRef(
(
_props: AllKeybindingsFieldsProps,
props: AllKeybindingsFieldsProps,
scrollRef: ForwardedRef<HTMLDivElement>
) => {
// This is how we will get the interaction map from the context
@ -25,7 +25,7 @@ export const AllKeybindingsFields = forwardRef(
.map(([category, categoryItems]) => (
<div className="flex flex-col gap-4 px-2 pr-4">
<h2
id={`category-${category.replaceAll(/\s/g, '-')}`}
id={`category-${category}`}
className="text-xl mt-6 first-of-type:mt-0 capitalize font-bold"
>
{category}

View File

@ -13,7 +13,7 @@ import { isDesktop } from 'lib/isDesktop'
import { ActionButton } from 'components/ActionButton'
import { SettingsFieldInput } from './SettingsFieldInput'
import toast from 'react-hot-toast'
import { APP_VERSION, IS_NIGHTLY, RELEASE_URL } from 'routes/Settings'
import { APP_VERSION, PACKAGE_NAME } from 'routes/Settings'
import { PATHS } from 'lib/paths'
import {
createAndOpenNewTutorialProject,
@ -246,8 +246,10 @@ export const AllSettingsFields = forwardRef(
to inject the version from package.json */}
App version {APP_VERSION}.{' '}
<a
onClick={openExternalBrowserIfDesktop(RELEASE_URL)}
href={RELEASE_URL}
onClick={openExternalBrowserIfDesktop(
`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`
)}
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`}
target="_blank"
rel="noopener noreferrer"
>
@ -269,7 +271,7 @@ export const AllSettingsFields = forwardRef(
, and start a discussion if you don't see it! Your feedback will
help us prioritize what to build next.
</p>
{!IS_NIGHTLY && (
{PACKAGE_NAME.indexOf('-nightly') === -1 && (
<p className="max-w-2xl mt-6">
Want to experience the latest and (hopefully) greatest from our
main development branch?{' '}

View File

@ -19,7 +19,7 @@ export function KeybindingsSectionsList({
key={category}
onClick={() =>
scrollRef.current
?.querySelector(`#category-${category.replaceAll(/\s/g, '-')}`)
?.querySelector(`#category-${category}`)
?.scrollIntoView({
block: 'center',
behavior: 'smooth',

View File

@ -23,6 +23,7 @@ import {
engineCommandManager,
sceneEntitiesManager,
} from 'lib/singletons'
import { uuidv4 } from 'lib/utils'
import { IndexLoaderData } from 'lib/types'
import { settings } from 'lib/settings/initialSettings'
import {
@ -128,11 +129,27 @@ export const SettingsAuthProviderBase = ({
.setTheme(context.app.theme.current)
.catch(reportRejection)
},
setEngineScaleGridVisibility: ({ context }) => {
engineCommandManager.setScaleGridVisibility(
context.modeling.showScaleGrid.current
)
},
setClientTheme: ({ context }) => {
const opposingTheme = getOppositeTheme(context.app.theme.current)
sceneInfra.theme = 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 }) => {
if (!('data' in event)) return
const eventParts = event.type.replace(/^set./, '').split('.') as [
@ -158,27 +175,17 @@ export const SettingsAuthProviderBase = ({
},
'Execute AST': ({ context, event }) => {
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 =
event.type === 'Set all settings' &&
relevantSetting(event.settings)
event.settings?.modeling?.defaultUnit?.current !==
context.modeling.defaultUnit.current
const resetSettingsIncludesUnitChange =
event.type === 'Reset settings' && relevantSetting(settings)
event.type === 'Reset settings' &&
context.modeling.defaultUnit.current !==
settings?.modeling?.defaultUnit?.default
if (
event.type === 'set.modeling.defaultUnit' ||
event.type === 'set.modeling.showScaleGrid' ||
event.type === 'set.modeling.highlightEdges' ||
allSettingsIncludesUnitChange ||
resetSettingsIncludesUnitChange
) {

View File

@ -22,7 +22,6 @@ import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { normaliseAngle } from '../../lib/utils'
import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap'
import { KclCommandValue } from 'lib/commandTypes'
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
@ -64,57 +63,6 @@ export function angleLengthInfo({
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({
selectionRanges,
angleOrLength = 'setLength',

View File

@ -1,5 +1,7 @@
import { LspWorkerEventType } from '@kittycad/codemirror-lsp-client'
import { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
export enum LspWorker {
Kcl = 'kcl',
Copilot = 'copilot',
@ -7,6 +9,7 @@ export enum LspWorker {
export interface KclWorkerOptions {
wasmUrl: string
token: string
baseUnit: UnitLength
apiBaseUrl: string
}

View File

@ -17,6 +17,7 @@ import {
KclWorkerOptions,
CopilotWorkerOptions,
} from 'editor/plugins/lsp/types'
import { EngineCommandManager } from 'lang/std/engineConnection'
import { err, reportRejection } from 'lib/trap'
const intoServer: IntoServer = new IntoServer()
@ -45,12 +46,14 @@ export async function copilotLspRun(
export async function kclLspRun(
config: ServerConfig,
engineCommandManager: EngineCommandManager | null,
token: string,
baseUnit: string,
baseUrl: string
) {
try {
console.log('start kcl lsp')
await kcl_lsp_run(config, null, undefined, token, baseUrl)
await kcl_lsp_run(config, engineCommandManager, baseUnit, token, baseUrl)
} catch (e: any) {
console.log('kcl lsp failed', e)
// We can't restart here because a moved value, we should do this another way.
@ -79,7 +82,13 @@ onmessage = function (event: MessageEvent) {
switch (worker) {
case LspWorker.Kcl:
const kclData = eventData as KclWorkerOptions
await kclLspRun(config, kclData.token, kclData.apiBaseUrl)
await kclLspRun(
config,
null,
kclData.token,
kclData.baseUnit,
kclData.apiBaseUrl
)
break
case LspWorker.Copilot:
let copilotData = eventData as CopilotWorkerOptions

View File

@ -2,7 +2,7 @@ import { useLayoutEffect, useEffect, useRef } from 'react'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { deferExecution } from 'lib/utils'
import { Themes } from 'lib/theme'
import { makeDefaultPlanes } from 'lang/wasm'
import { makeDefaultPlanes, modifyGrid } from 'lang/wasm'
import { useModelingContext } from './useModelingContext'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { useAppState, useAppStream } from 'AppState'
@ -56,6 +56,9 @@ export function useSetupEngineManager(
makeDefaultPlanes: () => {
return makeDefaultPlanes(kclManager.engineCommandManager)
},
modifyGrid: (hidden: boolean) => {
return modifyGrid(kclManager.engineCommandManager, hidden)
},
})
hasSetNonZeroDimensions.current = true
}

View File

@ -24,8 +24,6 @@ export function useConvertToVariable(range?: SourceRange) {
}, [enable])
useEffect(() => {
// Return early if there are no selection ranges for whatever reason
if (!context.selectionRanges) return
const parsed = ast
const meta = isNodeSafeToReplace(

View File

@ -45,7 +45,6 @@ import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
import { Models } from '@kittycad/lib'
import { ExtrudeFacePlane } from 'machines/modelingMachine'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { KclExpressionWithVariable } from 'lib/commandTypes'
export function startSketchOnDefault(
node: Node<Program>,
@ -591,25 +590,6 @@ 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
* of an offset plane. The new sketch just has to come after the offset
@ -953,31 +933,6 @@ 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(
ast: Node<Program>,
programMemory: ProgramMemory,

View File

@ -40,6 +40,7 @@ beforeAll(async () => {
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
setMediaStream: () => {},
setIsStreamReady: () => {},
modifyGrid: async () => {},
callbackOnEngineLiteConnect: () => {
resolve(true)
},

View File

@ -111,7 +111,8 @@ export function addShell({
const pathToNode: PathToNode = [
['body', ''],
[modifiedAst.body.length - 1, 'index'],
['declaration', 'VariableDeclaration'],
['declarations', 'VariableDeclaration'],
['0', 'index'],
['init', 'VariableDeclarator'],
['arguments', 'CallExpression'],
[0, 'index'],

View File

@ -661,7 +661,7 @@ describe('Testing doesSceneHaveExtrudedSketch', () => {
|> circle({ center = [0, 0], radius = 1 }, %)
extrude001 = extrude(1, sketch001)
`
const ast = assertParse(exampleCode)
const ast = parse(exampleCode)
if (err(ast)) throw ast
const extrudable = doesSceneHaveExtrudedSketch(ast)
expect(extrudable).toBeTruthy()
@ -671,7 +671,7 @@ extrude001 = extrude(1, sketch001)
|> circle({ center = [0, 0], radius = 1 }, %)
|> extrude(1, %)
`
const ast = assertParse(exampleCode)
const ast = parse(exampleCode)
if (err(ast)) throw ast
const extrudable = doesSceneHaveExtrudedSketch(ast)
expect(extrudable).toBeTruthy()
@ -680,7 +680,7 @@ extrude001 = extrude(1, sketch001)
const exampleCode = `extrude001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 1 }, %)
`
const ast = assertParse(exampleCode)
const ast = parse(exampleCode)
if (err(ast)) throw ast
const extrudable = doesSceneHaveExtrudedSketch(ast)
expect(extrudable).toBeFalsy()

View File

@ -139,6 +139,7 @@ beforeAll(async () => {
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
setMediaStream: () => {},
setIsStreamReady: () => {},
modifyGrid: async () => {},
// eslint-disable-next-line @typescript-eslint/no-misused-promises
callbackOnEngineLiteConnect: async () => {
const cacheEntries = Object.entries(codeToWriteCacheFor) as [

View File

@ -1399,6 +1399,7 @@ export class EngineCommandManager extends EventTarget {
}
private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null
private modifyGrid: (hidden: boolean) => Promise<void> | null = () => null
private onEngineConnectionOpened = () => {}
private onEngineConnectionClosed = () => {}
@ -1431,6 +1432,7 @@ export class EngineCommandManager extends EventTarget {
height,
token,
makeDefaultPlanes,
modifyGrid,
settings = {
pool: null,
theme: Themes.Dark,
@ -1450,12 +1452,14 @@ export class EngineCommandManager extends EventTarget {
height: number
token?: string
makeDefaultPlanes: () => Promise<DefaultPlanes>
modifyGrid: (hidden: boolean) => Promise<void>
settings?: SettingsViaQueryString
}) {
if (settings) {
this.settings = settings
}
this.makeDefaultPlanes = makeDefaultPlanes
this.modifyGrid = modifyGrid
if (width === 0 || height === 0) {
return
}
@ -1535,15 +1539,21 @@ export class EngineCommandManager extends EventTarget {
type: 'default_camera_get_settings',
},
})
await this.initPlanes()
setIsStreamReady(true)
// We want modify the grid first because we don't want it to flash.
// Ideally these would already be default hidden in engine (TODO do
// 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.
this.dispatchEvent(
new CustomEvent(EngineCommandManagerEvents.SceneReady, {
detail: this.engineConnection,
})
)
// Other parts of the application should use this to react on scene ready.
this.dispatchEvent(
new CustomEvent(EngineCommandManagerEvents.SceneReady, {
detail: this.engineConnection,
})
)
})
}
this.engineConnection.addEventListener(
@ -2202,6 +2212,15 @@ export class EngineCommandManager extends EventTarget {
}).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.
// So when passing a range, we need to also specify the command type
mapRangeToObjectId(

View File

@ -1,13 +1,14 @@
import init, {
parse_wasm,
recast_wasm,
execute,
execute_wasm,
kcl_lint,
modify_ast_for_sketch_wasm,
is_points_ccw,
get_tangential_arc_to_info,
program_memory_init,
make_default_planes,
modify_grid,
coredump,
toml_stringify,
default_app_settings,
@ -42,9 +43,7 @@ import { Environment } from '../wasm-lib/kcl/bindings/Environment'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError'
import { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
import { getChangedSettingsAtLevel } from 'lib/settings/settingsUtils'
export type { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
export type { ObjectExpression } from '../wasm-lib/kcl/bindings/ObjectExpression'
@ -494,20 +493,18 @@ export const _executor = async (
return Promise.reject(programMemoryOverride)
try {
let jsAppSettings = default_app_settings()
let baseUnit = 'mm'
if (!TEST) {
const getSettingsState = import('components/SettingsAuthProvider').then(
(module) => module.getSettingsState
)
const settings = (await getSettingsState)()
if (settings) {
jsAppSettings = getChangedSettingsAtLevel(settings, 'user')
}
baseUnit =
(await getSettingsState)()?.modeling.defaultUnit.current || 'mm'
}
const execState: RawExecState = await execute(
const execState: RawExecState = await execute_wasm(
JSON.stringify(node),
JSON.stringify(programMemoryOverride?.toRaw() || null),
JSON.stringify({ settings: jsAppSettings }),
baseUnit,
engineCommandManager,
fileSystemManager
)
@ -555,6 +552,20 @@ 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 (
engineCommandManager: EngineCommandManager,
ast: Node<Program>,

View File

@ -32,14 +32,8 @@ export function mouseControlsToCameraSystem(
mouseControl: MouseControlType | undefined
): CameraSystem | undefined {
switch (mouseControl) {
// TODO: understand why the values come back without underscores and fix the root cause
// @ts-ignore: TS2678
case 'kittycad':
case 'kitty_cad':
return 'KittyCAD'
// TODO: understand why the values come back without underscores and fix the root cause
// @ts-ignore: TS2678
case 'onshape':
case 'on_shape':
return 'OnShape'
case 'trackpad_friendly':
@ -50,9 +44,6 @@ export function mouseControlsToCameraSystem(
return 'NX'
case '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':
return 'AutoCAD'
default:

View File

@ -1,13 +1,8 @@
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 { KCL_DEFAULT_LENGTH, KCL_DEFAULT_DEGREE } from 'lib/constants'
import { components } from 'lib/machine-api'
import { Selections } from 'lib/selections'
import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap'
import { modelingMachine, SketchTool } from 'machines/modelingMachine'
type OutputFormat = Models['OutputFormat_type']
@ -59,18 +54,6 @@ export type ModelingCommandSchema = {
'change tool': {
tool: SketchTool
}
'Constrain length': {
selection: Selections
length: KclCommandValue
}
'Constrain with named value': {
currentValue: {
valueText: string
pathToNode: PathToNode
variableName: string
}
namedValue: KclCommandValue
}
'Text-to-CAD': {
prompt: string
}
@ -377,88 +360,6 @@ 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': {
description: 'Use the Zoo Text-to-CAD API to generate part starters.',
icon: 'chat',

View File

@ -148,22 +148,7 @@ export type CommandArgumentConfig<
selectionTypes: Artifact['type'][]
multiple: boolean
}
| {
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'
defaultValue?:
@ -237,22 +222,7 @@ export type CommandArgument<
selectionTypes: Artifact['type'][]
multiple: boolean
}
| {
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'
defaultValue?:

View File

@ -185,8 +185,6 @@ export function buildCommandArgument<
} else if (arg.inputType === 'kcl') {
return {
inputType: arg.inputType,
createVariableByDefault: arg.createVariableByDefault,
variableName: arg.variableName,
defaultValue: arg.defaultValue,
...baseCommandArgument,
} satisfies CommandArgument<O, T> & { inputType: 'kcl' }

View File

@ -630,29 +630,12 @@ export function getSelectionCountByType(
}
})
selection.graphSelections.forEach((graphSelection) => {
if (!graphSelection.artifact) {
/**
* 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
}
selection.graphSelections.forEach((selection) => {
if (!selection.artifact) {
incrementOrInitializeSelectionType('other')
return
}
incrementOrInitializeSelectionType(graphSelection.artifact.type)
incrementOrInitializeSelectionType(selection.artifact.type)
})
return selectionsByType

View File

@ -12,7 +12,7 @@ export type InteractionMapItem = {
* Controls both the available names for interaction map categories
* and the order in which they are displayed.
*/
const interactionMapCategories = [
export const interactionMapCategories = [
'Sketching',
'Modeling',
'Command Palette',

View File

@ -112,6 +112,9 @@ export async function executor(
makeDefaultPlanes: () => {
return new Promise((resolve) => resolve(defaultPlanes))
},
modifyGrid: (hidden: boolean) => {
return new Promise((resolve) => resolve())
},
})
return new Promise((resolve) => {

View File

@ -540,15 +540,13 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
[
{
id: 'constraint-length',
disabled: (state) => !state.matches({ Sketch: 'SketchIdle' }),
onClick: ({ commandBarSend }) =>
commandBarSend({
type: 'Find and select command',
data: {
name: 'Constrain length',
groupId: 'modeling',
},
}),
disabled: (state) =>
!(
state.matches({ Sketch: 'SketchIdle' }) &&
state.can({ type: 'Constrain length' })
),
onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain length' }),
icon: 'dimension',
status: 'available',
title: 'Length',

File diff suppressed because one or more lines are too long

View File

@ -42,6 +42,8 @@ export const settingsMachine = setup({
setClientTheme: () => {},
'Execute AST': () => {},
toastSuccess: () => {},
setEngineEdges: () => {},
setEngineScaleGridVisibility: () => {},
setClientSideSceneUnits: () => {},
persistSettings: () => {},
resetSettings: assign(({ context, event }) => {
@ -170,7 +172,7 @@ export const settingsMachine = setup({
'set.modeling.highlightEdges': {
target: 'persisting settings',
actions: ['setSettingAtLevel', 'toastSuccess', 'Execute AST'],
actions: ['setSettingAtLevel', 'toastSuccess', 'setEngineEdges'],
},
'Reset settings': {
@ -199,7 +201,11 @@ export const settingsMachine = setup({
'set.modeling.showScaleGrid': {
target: 'persisting settings',
actions: ['setSettingAtLevel', 'toastSuccess', 'Execute AST'],
actions: [
'setSettingAtLevel',
'toastSuccess',
'setEngineScaleGridVisibility',
],
},
},
},

View File

@ -30,12 +30,6 @@ export const PACKAGE_NAME = isDesktop()
? window.electron.packageJson.name
: 'zoo-modeling-app'
export const IS_NIGHTLY = PACKAGE_NAME.indexOf('-nightly') > -1
export const RELEASE_URL = `https://github.com/KittyCAD/modeling-app/releases/tag/${
IS_NIGHTLY ? 'nightly-' : ''
}v${APP_VERSION}`
export const Settings = () => {
const navigate = useNavigate()
const [searchParams, setSearchParams] = useSearchParams()

View File

@ -12,8 +12,8 @@ redo-kcl-stdlib-docs-no-imgs:
# Generate the stdlib image artifacts
# Then run the stdlib docs generation
redo-kcl-stdlib-docs:
TWENTY_TWENTY=overwrite {{cnr}} -p kcl-lib --no-fail-fast -- kcl_test_example
EXPECTORATE=overwrite {{cnr}} -p kcl-lib --no-fail-fast -- docs::gen_std_tests::test_generate_stdlib
TWENTY_TWENTY=overwrite {{cnr}} -p kcl-lib kcl_test_example
EXPECTORATE=overwrite {{cnr}} -p kcl-lib docs::gen_std_tests::test_generate_stdlib
# Copy a test KCL file from executor tests into a new simulation test.
copy-exec-test-into-sim-test test_name:

View File

@ -120,61 +120,6 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
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.
async fn batch_modeling_cmd(
&self,
@ -559,11 +504,11 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
}))
}
async fn modify_grid(&self, hidden: bool, source_range: SourceRange) -> Result<(), KclError> {
async fn modify_grid(&self, hidden: bool) -> Result<(), KclError> {
// Hide/show the grid.
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
source_range,
Default::default(),
&ModelingCmd::from(mcmd::ObjectVisible {
hidden,
object_id: *GRID_OBJECT_ID,
@ -574,7 +519,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
// Hide/show the grid scale text.
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
source_range,
Default::default(),
&ModelingCmd::from(mcmd::ObjectVisible {
hidden,
object_id: *GRID_SCALE_TEXT_OBJECT_ID,
@ -582,6 +527,8 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
)
.await?;
self.flush_batch(false, Default::default()).await?;
Ok(())
}

View File

@ -1,50 +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>,
}

View File

@ -369,7 +369,6 @@ impl Node<CallExpressionKw> {
// Build a hashmap from argument labels to the final evaluated values.
let mut fn_args = HashMap::with_capacity(self.arguments.len());
let mut tag_declarator_args = Vec::new();
for arg_expr in &self.arguments {
let source_range = SourceRange::from(arg_expr.arg.clone());
let metadata = Metadata { source_range };
@ -377,12 +376,8 @@ impl Node<CallExpressionKw> {
.execute_expr(&arg_expr.arg, exec_state, &metadata, StatementKind::Expression)
.await?;
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 tag_declarator_args = tag_declarator_args; // remove mutability
// Evaluate the unlabeled first param, if any exists.
let unlabeled = if let Some(ref arg_expr) = self.unlabeled {
@ -408,7 +403,7 @@ impl Node<CallExpressionKw> {
FunctionKind::Core(func) => {
// Attempt to call the function.
let mut result = func.std_lib_fn()(exec_state, args).await?;
update_memory_for_tags_of_geometry(&mut result, &tag_declarator_args, exec_state)?;
update_memory_for_tags_of_geometry(&mut result, exec_state)?;
Ok(result)
}
FunctionKind::UserDefined => {
@ -424,7 +419,6 @@ impl Node<CallExpression> {
let fn_name = &self.callee.name;
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 {
let metadata = Metadata {
@ -434,19 +428,15 @@ impl Node<CallExpression> {
.execute_expr(arg_expr, exec_state, &metadata, StatementKind::Expression)
.await?;
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);
}
let tag_declarator_args = tag_declarator_args; // remove mutability
match ctx.stdlib.get_either(fn_name) {
FunctionKind::Core(func) => {
// Attempt to call the function.
let args = crate::std::Args::new(fn_args, self.into(), ctx.clone());
let mut result = func.std_lib_fn()(exec_state, args).await?;
update_memory_for_tags_of_geometry(&mut result, &tag_declarator_args, exec_state)?;
update_memory_for_tags_of_geometry(&mut result, exec_state)?;
Ok(result)
}
FunctionKind::UserDefined => {
@ -485,24 +475,7 @@ impl Node<CallExpression> {
}
}
/// `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)?;
}
fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut ExecState) -> Result<(), KclError> {
// If the return result is a sketch or solid, we want to update the
// 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
@ -510,7 +483,7 @@ fn update_memory_for_tags_of_geometry(
match result {
KclValue::Sketch { value: ref mut sketch } => {
for (_, tag) in sketch.tags.iter() {
exec_state.memory.update_tag_if_defined(&tag.value, tag.clone());
exec_state.memory.update_tag(&tag.value, tag.clone())?;
}
}
KclValue::Solid(ref mut solid) => {
@ -548,7 +521,7 @@ fn update_memory_for_tags_of_geometry(
info.sketch = solid.id;
t.info = Some(info);
exec_state.memory.update_tag_if_defined(&tag.name, t.clone());
exec_state.memory.update_tag(&tag.name, t.clone())?;
// update the sketch tags.
solid.sketch.tags.insert(tag.name.clone(), t);
@ -569,6 +542,22 @@ fn update_memory_for_tags_of_geometry(
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> {
#[async_recursion]
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {

View File

@ -23,18 +23,15 @@ type Point3D = kcmc::shared::Point3d<f64>;
pub use function_param::FunctionParam;
pub use kcl_value::{KclObjectFields, KclValue};
pub(crate) mod cache;
mod exec_ast;
mod function_param;
mod kcl_value;
use crate::{
engine::{EngineManager, ExecutionKind},
errors::{KclError, KclErrorDetails},
execution::cache::{CacheInformation, CacheResult},
fs::{FileManager, FileSystem},
parsing::ast::types::{
BodyItem, Expr, FunctionExpression, ImportSelector, ItemVisibility, Node, NodeRef, TagDeclarator, TagNode,
parsing::ast::{
cache::{get_changed_program, CacheInformation},
types::{
BodyItem, Expr, FunctionExpression, ImportSelector, ItemVisibility, Node, NodeRef, TagDeclarator, TagNode,
},
},
settings::types::UnitLength,
source_range::{ModuleId, SourceRange},
@ -42,6 +39,10 @@ use crate::{
ExecError, Program,
};
mod exec_ast;
mod function_param;
mod kcl_value;
/// State for executing a program.
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
@ -124,16 +125,10 @@ impl ProgramMemory {
Ok(())
}
pub fn add_tag(&mut self, tag: &str, value: TagIdentifier, source_range: SourceRange) -> Result<(), KclError> {
self.add(tag, KclValue::TagIdentifier(Box::new(value)), source_range)
}
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;
}
pub fn update_tag(&mut self, tag: &str, value: TagIdentifier) -> Result<(), KclError> {
self.environments[self.current_env.index()].insert(tag.to_string(), KclValue::TagIdentifier(Box::new(value)));
Ok(())
}
/// Get a value from the program memory.
@ -850,7 +845,7 @@ impl GetTangentialInfoFromPathsResult {
impl Sketch {
pub(crate) fn add_tag(&mut self, tag: NodeRef<'_, TagDeclarator>, current_path: &Path) {
let mut tag_identifier = TagIdentifier::from(tag);
let mut tag_identifier: TagIdentifier = tag.into();
let base = current_path.get_base();
tag_identifier.info = Some(TagEngineInfo {
id: base.geo_meta.id,
@ -1659,6 +1654,17 @@ impl ExecutorContext {
let engine: Arc<Box<dyn EngineManager>> =
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 {
engine,
fs: Arc::new(FileManager::new()),
@ -1685,7 +1691,7 @@ impl ExecutorContext {
pub async fn new(
engine_manager: crate::engine::conn_wasm::EngineCommandManager,
fs_manager: crate::fs::wasm::FileSystemManager,
settings: ExecutorSettings,
units: UnitLength,
) -> Result<Self, String> {
Ok(ExecutorContext {
engine: Arc::new(Box::new(
@ -1695,16 +1701,16 @@ impl ExecutorContext {
)),
fs: Arc::new(FileManager::new(fs_manager)),
stdlib: Arc::new(StdLib::new()),
settings,
settings: ExecutorSettings {
units,
..Default::default()
},
context_type: ContextType::Live,
})
}
#[cfg(target_arch = "wasm32")]
pub async fn new_mock(
fs_manager: crate::fs::wasm::FileSystemManager,
settings: ExecutorSettings,
) -> Result<Self, String> {
pub async fn new_mock(fs_manager: crate::fs::wasm::FileSystemManager, units: UnitLength) -> Result<Self, String> {
Ok(ExecutorContext {
engine: Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -1713,7 +1719,10 @@ impl ExecutorContext {
)),
fs: Arc::new(FileManager::new(fs_manager)),
stdlib: Arc::new(StdLib::new()),
settings,
settings: ExecutorSettings {
units,
..Default::default()
},
context_type: ContextType::Mock,
})
}
@ -1802,71 +1811,6 @@ impl ExecutorContext {
// AND if we aren't in wasm it doesn't really matter.
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.
/// You can optionally pass in some initialization memory.
@ -1887,7 +1831,7 @@ impl ExecutorContext {
let _stats = crate::log::LogPerfStats::new("Interpretation");
// Get the program that actually changed from the old and new information.
let cache_result = self.get_changed_program(cache_info.clone()).await;
let cache_result = get_changed_program(cache_info.clone(), &self.settings);
// Check if we don't need to re-execute.
let Some(cache_result) = cache_result else {
@ -1904,9 +1848,23 @@ impl ExecutorContext {
// TODO: Use the top-level file's path.
exec_state.add_module(std::path::PathBuf::from(""));
// Re-apply the settings, in case the cache was busted.
self.engine.reapply_settings(&self.settings, Default::default()).await?;
// Before we even start executing the program, set the units.
self.engine
.batch_modeling_cmd(
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)
.await?;
@ -2127,7 +2085,7 @@ impl ExecutorContext {
let item = match init {
Expr::None(none) => KclValue::from(none),
Expr::Literal(literal) => KclValue::from(literal),
Expr::TagDeclarator(tag) => KclValue::from(tag),
Expr::TagDeclarator(tag) => tag.execute(exec_state).await?,
Expr::Identifier(identifier) => {
let value = exec_state.memory.get(&identifier.name, identifier.into())?;
value.clone()
@ -2183,8 +2141,23 @@ impl ExecutorContext {
self.settings.units = units;
}
/// Get a snapshot of the current scene.
pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
/// 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.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.
self.engine
.send_modeling_cmd(
@ -2220,17 +2193,6 @@ impl ExecutorContext {
};
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,
@ -2327,12 +2289,9 @@ mod tests {
use pretty_assertions::assert_eq;
use super::*;
use crate::{
parsing::ast::types::{DefaultParamVal, Identifier, Node, Parameter},
OldAstState,
};
use crate::parsing::ast::types::{DefaultParamVal, Identifier, Node, Parameter};
pub async fn parse_execute(code: &str) -> Result<(Program, ExecutorContext, ExecState)> {
pub async fn parse_execute(code: &str) -> Result<ProgramMemory> {
let program = Program::parse_no_errs(code)?;
let ctx = ExecutorContext {
@ -2343,9 +2302,9 @@ mod tests {
context_type: ContextType::Mock,
};
let mut exec_state = ExecState::default();
ctx.run(program.clone().into(), &mut exec_state).await?;
ctx.run(program.into(), &mut exec_state).await?;
Ok((program, ctx, exec_state))
Ok(exec_state.memory)
}
/// Convenience function to get a JSON value from memory and unwrap.
@ -2756,39 +2715,36 @@ let shape = layer() |> patternTransform(10, transform, %)
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_with_functions() {
let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#;
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
assert_eq!(5.0, mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap());
let memory = parse_execute(ast).await.unwrap();
assert_eq!(5.0, mem_get_json(&memory, "myVar").as_f64().unwrap());
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute() {
let ast = r#"const myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
assert_eq!(7.4, mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap());
let memory = parse_execute(ast).await.unwrap();
assert_eq!(7.4, mem_get_json(&memory, "myVar").as_f64().unwrap());
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_start_negative() {
let ast = r#"const myVar = -5 + 6"#;
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
assert_eq!(1.0, mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap());
let memory = parse_execute(ast).await.unwrap();
assert_eq!(1.0, mem_get_json(&memory, "myVar").as_f64().unwrap());
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_with_pi() {
let ast = r#"const myVar = pi() * 2"#;
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
assert_eq!(
std::f64::consts::TAU,
mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap()
);
let memory = parse_execute(ast).await.unwrap();
assert_eq!(std::f64::consts::TAU, mem_get_json(&memory, "myVar").as_f64().unwrap());
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_define_decimal_without_leading_zero() {
let ast = r#"let thing = .4 + 7"#;
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
assert_eq!(7.4, mem_get_json(&exec_state.memory, "thing").as_f64().unwrap());
let memory = parse_execute(ast).await.unwrap();
assert_eq!(7.4, mem_get_json(&memory, "thing").as_f64().unwrap());
}
#[tokio::test(flavor = "multi_thread")]
@ -2827,11 +2783,11 @@ fn check = (x) => {
}
check(false)
"#;
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
assert_eq!(false, mem_get_json(&exec_state.memory, "notTrue").as_bool().unwrap());
assert_eq!(true, mem_get_json(&exec_state.memory, "notFalse").as_bool().unwrap());
assert_eq!(true, mem_get_json(&exec_state.memory, "c").as_bool().unwrap());
assert_eq!(false, mem_get_json(&exec_state.memory, "d").as_bool().unwrap());
let mem = parse_execute(ast).await.unwrap();
assert_eq!(false, mem_get_json(&mem, "notTrue").as_bool().unwrap());
assert_eq!(true, mem_get_json(&mem, "notFalse").as_bool().unwrap());
assert_eq!(true, mem_get_json(&mem, "c").as_bool().unwrap());
assert_eq!(false, mem_get_json(&mem, "d").as_bool().unwrap());
}
#[tokio::test(flavor = "multi_thread")]
@ -2932,10 +2888,8 @@ let notTagDeclarator = !myTagDeclarator";
);
let code9 = "
sk = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> line([5, 0], %, $myTag)
notTagIdentifier = !myTag";
let myTagDeclarator = $myTag
let notTagIdentifier = !myTag";
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
// check the full error.
@ -3213,310 +3167,4 @@ let w = f() + f()
let json = serde_json::to_string(&mem).unwrap();
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);
}
}

View File

@ -82,15 +82,16 @@ mod wasm;
pub use coredump::CoreDump;
pub use engine::{EngineManager, ExecutionKind};
pub use errors::{CompilationError, ConnectionError, ExecError, KclError};
pub use execution::{
cache::{CacheInformation, OldAstState},
ExecState, ExecutorContext, ExecutorSettings,
};
pub use execution::{ExecState, ExecutorContext, ExecutorSettings};
pub use lsp::{
copilot::Backend as CopilotLspBackend,
kcl::{Backend as KclLspBackend, Server as KclLspServerSubCommand},
};
pub use parsing::ast::{modify::modify_ast_for_sketch, types::FormatOptions};
pub use parsing::ast::{
cache::{CacheInformation, OldAstState},
modify::modify_ast_for_sketch,
types::FormatOptions,
};
pub use settings::types::{project::ProjectConfiguration, Configuration, UnitLength};
pub use source_range::{ModuleId, SourceRange};
@ -137,7 +138,7 @@ pub use lsp::test_util::kcl_lsp_server;
impl Program {
pub fn parse(input: &str) -> Result<(Option<Program>, Vec<CompilationError>), KclError> {
let module_id = ModuleId::default();
let tokens = parsing::token::lex(input, module_id)?;
let tokens = parsing::token::lexer(input, module_id)?;
let (ast, errs) = parsing::parse_tokens(tokens).0?;
Ok((ast.map(|ast| Program { ast }), errs))
@ -145,7 +146,7 @@ impl Program {
pub fn parse_no_errs(input: &str) -> Result<Program, KclError> {
let module_id = ModuleId::default();
let tokens = parsing::token::lex(input, module_id)?;
let tokens = parsing::token::lexer(input, module_id)?;
let ast = parsing::parse_tokens(tokens).parse_errs_as_err()?;
Ok(Program { ast })

View File

@ -45,32 +45,38 @@ use crate::{
errors::Suggestion,
lsp::{backend::Backend as _, util::IntoDiagnostic},
parsing::{
ast::types::{Expr, Node, VariableKind},
token::TokenStream,
ast::{
cache::{CacheInformation, OldAstState},
types::{Expr, Node, VariableKind},
},
token::TokenType,
PIPE_OPERATOR,
},
CacheInformation, ModuleId, OldAstState, Program, SourceRange,
ModuleId, 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,
];
const SEMANTIC_TOKEN_MODIFIERS: [SemanticTokenModifier; 5] = [
SemanticTokenModifier::DECLARATION,
SemanticTokenModifier::DEFINITION,
SemanticTokenModifier::DEFAULT_LIBRARY,
SemanticTokenModifier::READONLY,
SemanticTokenModifier::STATIC,
];
lazy_static::lazy_static! {
pub static ref SEMANTIC_TOKEN_TYPES: Vec<SemanticTokenType> = {
// This is safe to unwrap because we know all the token types are valid.
// And the test would fail if they were not.
let mut gen = TokenType::all_semantic_token_types().unwrap();
gen.extend(vec![
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.
#[derive(Clone, Debug)]
@ -99,7 +105,7 @@ pub struct Backend {
/// The stdlib signatures for the language.
pub stdlib_signatures: HashMap<String, SignatureHelp>,
/// Token maps.
pub(super) token_map: DashMap<String, TokenStream>,
pub token_map: DashMap<String, Vec<crate::parsing::token::Token>>,
/// AST maps.
pub ast_map: DashMap<String, Node<crate::parsing::ast::types::Program>>,
/// Last successful execution.
@ -278,7 +284,7 @@ impl crate::lsp::backend::Backend for Backend {
// Lets update the tokens.
let module_id = ModuleId::default();
let tokens = match crate::parsing::token::lex(&params.text, module_id) {
let tokens = match crate::parsing::token::lexer(&params.text, module_id) {
Ok(tokens) => tokens,
Err(err) => {
self.add_to_diagnostics(&params, &[err], true).await;
@ -404,11 +410,11 @@ impl Backend {
self.executor_ctx.read().await
}
async fn update_semantic_tokens(&self, tokens: &TokenStream, params: &TextDocumentItem) {
async fn update_semantic_tokens(&self, tokens: &[crate::parsing::token::Token], params: &TextDocumentItem) {
// Update the semantic tokens map.
let mut semantic_tokens = vec![];
let mut last_position = Position::new(0, 0);
for token in tokens.as_slice() {
for token in tokens {
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
// the rest from the ast.
@ -438,11 +444,8 @@ impl Backend {
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 modifier_index: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));
crate::walk::walk(&ast, |node: crate::walk::Node| {
let Ok(node_range): Result<SourceRange, _> = (&node).try_into() else {
return Ok(true);
};
crate::walk::walk(&ast, &|node: crate::walk::Node| {
let node_range: SourceRange = (&node).into();
if !node_range.contains(source_range.start()) {
return Ok(true);
}
@ -560,7 +563,7 @@ impl Backend {
let semantic_token = SemanticToken {
delta_line: position.line - last_position.line + 1,
delta_start: 0,
length: (token.end - token.start) as u32,
length: token.value.len() as u32,
token_type: token_type_index,
token_modifiers_bitset,
};
@ -579,7 +582,7 @@ impl Backend {
} else {
position.character - last_position.character
},
length: (token.end - token.start) as u32,
length: token.value.len() as u32,
token_type: token_type_index,
token_modifiers_bitset,
};
@ -960,8 +963,8 @@ impl LanguageServer for Backend {
semantic_tokens_options: SemanticTokensOptions {
work_done_progress_options: WorkDoneProgressOptions::default(),
legend: SemanticTokensLegend {
token_types: SEMANTIC_TOKEN_TYPES.to_vec(),
token_modifiers: SEMANTIC_TOKEN_MODIFIERS.to_vec(),
token_types: SEMANTIC_TOKEN_TYPES.clone(),
token_modifiers: SEMANTIC_TOKEN_MODIFIERS.clone(),
},
range: Some(false),
full: Some(SemanticTokensFullOptions::Bool(true)),

View File

@ -1082,7 +1082,7 @@ fn myFn = (param1) => {
// Get the token map.
let token_map = server.token_map.get("file:///test.kcl").unwrap().clone();
assert!(!token_map.is_empty());
assert!(token_map != vec![]);
// Get the ast.
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
@ -2206,7 +2206,7 @@ part001 = cube([0,0], 20)
// Get the tokens.
let tokens = server.token_map.get("file:///test.kcl").unwrap().clone();
assert_eq!(tokens.as_slice().len(), 120);
assert_eq!(tokens.len(), 120);
// Get the ast.
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
@ -3379,11 +3379,11 @@ part001 = startSketchOn('XY')
// Get the symbols map.
let symbols_map = server.symbols_map.get("file:///test.kcl").unwrap().clone();
assert!(!symbols_map.is_empty());
assert!(symbols_map != vec![]);
// Get the semantic tokens map.
let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone();
assert!(!semantic_tokens_map.is_empty());
assert!(semantic_tokens_map != vec![]);
// Get the memory.
let memory = server.memory_map.get("file:///test.kcl").unwrap().clone();
@ -3422,7 +3422,7 @@ NEW_LINT = 1"#
// Get the semantic tokens map.
let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone();
assert!(!semantic_tokens_map.is_empty());
assert!(semantic_tokens_map != vec![]);
// Get the memory.
let memory = server.memory_map.get("file:///test.kcl");
@ -3466,7 +3466,7 @@ part001 = startSketchOn('XY')
// Get the token map.
let token_map = server.token_map.get("file:///test.kcl").unwrap().clone();
assert!(!token_map.is_empty());
assert!(token_map != vec![]);
// Get the ast.
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
@ -3474,11 +3474,11 @@ part001 = startSketchOn('XY')
// Get the symbols map.
let symbols_map = server.symbols_map.get("file:///test.kcl").unwrap().clone();
assert!(!symbols_map.is_empty());
assert!(symbols_map != vec![]);
// Get the semantic tokens map.
let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone();
assert!(!semantic_tokens_map.is_empty());
assert!(semantic_tokens_map != vec![]);
// Get the memory.
let memory = server.memory_map.get("file:///test.kcl").unwrap().clone();
@ -3509,7 +3509,7 @@ part001 = startSketchOn('XY')
// Get the token map.
let token_map = server.token_map.get("file:///test.kcl").unwrap().clone();
assert!(!token_map.is_empty());
assert!(token_map != vec![]);
// Get the ast.
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
@ -3517,11 +3517,11 @@ part001 = startSketchOn('XY')
// Get the symbols map.
let symbols_map = server.symbols_map.get("file:///test.kcl").unwrap().clone();
assert!(!symbols_map.is_empty());
assert!(symbols_map != vec![]);
// Get the semantic tokens map.
let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone();
assert!(!semantic_tokens_map.is_empty());
assert!(semantic_tokens_map != vec![]);
// Get the memory.
let memory = server.memory_map.get("file:///test.kcl");

View File

@ -0,0 +1,373 @@
//! 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);
}
}

View File

@ -1,3 +1,4 @@
pub(crate) mod cache;
pub(crate) mod digest;
pub mod modify;
pub mod types;

View File

@ -184,7 +184,7 @@ impl Node<Program> {
/// Walk the ast and get all the variables and tags as completion items.
pub fn completion_items<'a>(&'a self) -> Result<Vec<CompletionItem>> {
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"))?;
match node {
crate::walk::Node::TagDeclarator(tag) => {
@ -195,7 +195,7 @@ impl Node<Program> {
}
_ => {}
}
Ok::<bool, anyhow::Error>(true)
Ok(true)
})?;
let x = completions.lock().unwrap();
Ok(x.clone())
@ -204,7 +204,7 @@ impl Node<Program> {
/// Returns all the lsp symbols in the program.
pub fn get_lsp_symbols<'a>(&'a self, code: &str) -> Result<Vec<DocumentSymbol>> {
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"))?;
match node {
crate::walk::Node::TagDeclarator(tag) => {
@ -215,7 +215,7 @@ impl Node<Program> {
}
_ => {}
}
Ok::<bool, anyhow::Error>(true)
Ok(true)
})?;
let x = symbols.lock().unwrap();
Ok(x.clone())
@ -227,10 +227,10 @@ impl Node<Program> {
RuleT: crate::lint::Rule<'a>,
{
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"))?;
findings.append(&mut rule.check(node)?);
Ok::<bool, anyhow::Error>(true)
Ok(true)
})?;
let x = v.lock().unwrap();
Ok(x.clone())

View File

@ -2,7 +2,7 @@ use crate::{
errors::{CompilationError, KclError, KclErrorDetails},
parsing::{
ast::types::{Node, Program},
token::TokenStream,
token::{Token, TokenType},
},
source_range::{ModuleId, SourceRange},
};
@ -34,13 +34,15 @@ pub fn top_level_parse(code: &str) -> ParseResult {
/// Parse the given KCL code into an AST.
pub fn parse_str(code: &str, module_id: ModuleId) -> ParseResult {
let tokens = pr_try!(crate::parsing::token::lex(code, module_id));
let tokens = pr_try!(crate::parsing::token::lexer(code, module_id));
parse_tokens(tokens)
}
/// Parse the supplied tokens into an AST.
pub fn parse_tokens(mut tokens: TokenStream) -> ParseResult {
let unknown_tokens = tokens.remove_unknown();
pub fn parse_tokens(tokens: Vec<Token>) -> ParseResult {
let (tokens, unknown_tokens): (Vec<Token>, Vec<Token>) = tokens
.into_iter()
.partition(|token| token.token_type != TokenType::Unknown);
if !unknown_tokens.is_empty() {
let source_ranges = unknown_tokens.iter().map(SourceRange::from).collect();
@ -67,7 +69,7 @@ pub fn parse_tokens(mut tokens: TokenStream) -> ParseResult {
return Node::<Program>::default().into();
}
parser::run_parser(tokens.as_slice())
parser::run_parser(&mut tokens.as_slice())
}
/// Result of parsing.

File diff suppressed because it is too large Load Diff

View File

@ -1,221 +1,28 @@
// Clippy does not agree with rustc here for some reason.
#![allow(clippy::needless_lifetimes)]
use std::{fmt, iter::Enumerate, num::NonZeroUsize};
use std::str::FromStr;
use anyhow::Result;
use parse_display::Display;
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tower_lsp::lsp_types::SemanticTokenType;
use winnow::{
self,
error::ParseError,
stream::{ContainsToken, Stream},
};
use winnow::{error::ParseError, stream::ContainsToken};
use crate::{
errors::KclError,
parsing::ast::types::{ItemVisibility, VariableKind},
source_range::{ModuleId, SourceRange},
};
use tokeniser::Input;
mod tokeniser;
// Re-export
pub use tokeniser::Input;
#[cfg(test)]
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.
#[derive(Debug, PartialEq, Eq, Copy, Clone, Display)]
#[derive(Debug, PartialEq, Eq, Copy, Clone, Deserialize, Serialize, JsonSchema, FromStr, Display)]
#[serde(rename_all = "camelCase")]
#[display(style = "camelCase")]
pub enum TokenType {
/// A number.
@ -266,8 +73,6 @@ pub enum TokenType {
impl TryFrom<TokenType> for SemanticTokenType {
type Error = anyhow::Error;
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 {
TokenType::Number => Self::NUMBER,
TokenType::Word => Self::VARIABLE,
@ -297,6 +102,52 @@ impl TryFrom<TokenType> for SemanticTokenType {
}
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 {
matches!(self, Self::Whitespace)
}
@ -306,15 +157,17 @@ impl TokenType {
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)]
pub struct Token {
#[serde(rename = "type")]
pub token_type: TokenType,
/// Offset in the source code where this token begins.
pub start: usize,
/// Offset in the source code where this token ends.
pub end: usize,
pub(super) module_id: ModuleId,
pub(super) value: String,
#[serde(default, skip_serializing_if = "ModuleId::is_top_level")]
pub module_id: ModuleId,
pub value: String,
}
impl ContainsToken<Token> for (TokenType, &str) {
@ -396,7 +249,7 @@ impl From<&Token> for SourceRange {
}
}
pub fn lex(s: &str, module_id: ModuleId) -> Result<TokenStream, KclError> {
pub fn lexer(s: &str, module_id: ModuleId) -> Result<Vec<Token>, KclError> {
tokeniser::lex(s, module_id).map_err(From::from)
}
@ -428,3 +281,15 @@ 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

View File

@ -47,7 +47,7 @@ fn read(filename: &'static str, test_name: &str) -> String {
fn parse(test_name: &str) {
let input = read("input.kcl", test_name);
let tokens = crate::parsing::token::lex(&input, ModuleId::default()).unwrap();
let tokens = crate::parsing::token::lexer(&input, ModuleId::default()).unwrap();
// Parse the tokens into an AST.
let parse_res = Result::<_, KclError>::Ok(crate::parsing::parse_tokens(tokens).unwrap());
@ -1502,45 +1502,3 @@ mod kw_fn {
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
}
}

View File

@ -55,11 +55,7 @@ async fn do_execute_and_snapshot(
program: Program,
) -> Result<(crate::execution::ExecState, image::DynamicImage), ExecError> {
let mut exec_state = Default::default();
let snapshot_png_bytes = ctx
.execute_and_prepare_snapshot(&program, &mut exec_state)
.await?
.contents
.0;
let snapshot_png_bytes = ctx.execute_and_prepare(&program, &mut exec_state).await?.contents.0;
// Decode the snapshot, return it.
let img = image::ImageReader::new(std::io::Cursor::new(snapshot_png_bytes))

View File

@ -166,14 +166,7 @@ pub(crate) enum ExprContext {
}
impl Expr {
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;
}
pub(crate) fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
match &self {
Expr::BinaryExpression(bin_exp) => bin_exp.recast(options),
Expr::ArrayExpression(array_exp) => array_exp.recast(options, indentation_level, ctxt),
@ -182,7 +175,11 @@ impl Expr {
Expr::MemberExpression(mem_exp) => mem_exp.recast(),
Expr::Literal(literal) => literal.recast(),
Expr::FunctionExpression(func_exp) => {
let mut result = if is_decl { String::new() } else { "fn".to_owned() };
let mut result = if ctxt == ExprContext::Decl {
String::new()
} else {
"fn".to_owned()
};
result += &func_exp.recast(options, indentation_level);
result
}
@ -2137,10 +2134,8 @@ fn f() {
.into_iter()
.enumerate()
{
let tokens = crate::parsing::token::lex(raw, ModuleId::default()).unwrap();
let literal = crate::parsing::parser::unsigned_number_literal
.parse(tokens.as_slice())
.unwrap();
let tokens = crate::parsing::token::lexer(raw, ModuleId::default()).unwrap();
let literal = crate::parsing::parser::unsigned_number_literal.parse(&tokens).unwrap();
assert_eq!(
literal.recast(),
expected,
@ -2175,28 +2170,6 @@ sketch002 = startSketchOn({
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]
fn recast_objects_with_comments() {
use winnow::Parser;
@ -2218,9 +2191,9 @@ sketch002 = startSketchOn({
.into_iter()
.enumerate()
{
let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
crate::parsing::parser::print_tokens(tokens.as_slice());
let expr = crate::parsing::parser::object.parse(tokens.as_slice()).unwrap();
let tokens = crate::parsing::token::lexer(input, ModuleId::default()).unwrap();
crate::parsing::parser::print_tokens(&tokens);
let expr = crate::parsing::parser::object.parse(&tokens).unwrap();
assert_eq!(
expr.recast(&FormatOptions::new(), 0, ExprContext::Other),
expected,
@ -2316,10 +2289,8 @@ sketch002 = startSketchOn({
.into_iter()
.enumerate()
{
let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
let expr = crate::parsing::parser::array_elem_by_elem
.parse(tokens.as_slice())
.unwrap();
let tokens = crate::parsing::token::lexer(input, ModuleId::default()).unwrap();
let expr = crate::parsing::parser::array_elem_by_elem.parse(&tokens).unwrap();
assert_eq!(
expr.recast(&FormatOptions::new(), 0, ExprContext::Other),
expected,

View File

@ -5,7 +5,7 @@ use crate::{
/// 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.
#[derive(Copy, Clone, Debug)]
#[derive(Clone, Debug)]
pub enum Node<'a> {
Program(NodeRef<'a, types::Program>),
@ -31,7 +31,6 @@ pub enum Node<'a> {
MemberExpression(NodeRef<'a, types::MemberExpression>),
UnaryExpression(NodeRef<'a, types::UnaryExpression>),
IfExpression(NodeRef<'a, types::IfExpression>),
ElseIf(&'a types::ElseIf),
Parameter(&'a types::Parameter),
@ -39,22 +38,11 @@ pub enum Node<'a> {
MemberObject(&'a types::MemberObject),
LiteralIdentifier(&'a types::LiteralIdentifier),
KclNone(&'a types::KclNone),
}
/// Returned during source_range conversion.
#[derive(Debug)]
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 {
impl From<&Node<'_>> for SourceRange {
fn from(node: &Node) -> Self {
match node {
Node::Program(n) => SourceRange::from(*n),
Node::ImportStatement(n) => SourceRange::from(*n),
Node::ExpressionStatement(n) => SourceRange::from(*n),
@ -80,62 +68,6 @@ impl TryFrom<&Node<'_>> for SourceRange {
Node::MemberObject(m) => SourceRange::new(m.start(), m.end(), m.module_id()),
Node::IfExpression(n) => SourceRange::from(*n),
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::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(),
}
}
}
@ -184,6 +116,4 @@ impl_from!(Node, ObjectProperty);
impl_from_ref!(Node, Parameter);
impl_from_ref!(Node, MemberObject);
impl_from!(Node, IfExpression);
impl_from!(Node, ElseIf);
impl_from_ref!(Node, LiteralIdentifier);
impl_from!(Node, KclNone);

View File

@ -1,197 +0,0 @@
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::PipeSubstitution(_)
| Node::TagDeclarator(_)
| Node::Identifier(_)
| Node::ImportStatement(_)
| Node::MemberObject(_)
| Node::LiteralIdentifier(_)
| Node::KclNone(_)
| Node::Literal(_) => vec![],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
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);
}
}

View File

@ -1,55 +1,329 @@
use anyhow::Result;
use super::ast_visitor::{Visitable, Visitor};
use crate::{
parsing::ast::types::{NodeRef, Program},
parsing::ast::types::{
BinaryPart, BodyItem, Expr, IfExpression, LiteralIdentifier, MemberExpression, MemberObject, NodeRef,
ObjectExpression, ObjectProperty, Parameter, Program, UnaryExpression, VariableDeclarator,
},
walk::Node,
};
/// *DEPRECATED* Walk trait.
///
/// 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.
/// Walker is implemented by things that are able to walk an AST tree to
/// produce lints. This trait is implemented automatically for a few of the
/// common types, but can be manually implemented too.
pub trait Walker<'a> {
/// Walk will visit every element of the AST, recursing through the
/// whole tree.
/// Walk will visit every element of the AST.
fn walk(&self, n: Node<'a>) -> Result<bool>;
}
impl<'tree, VisitorT> Walker<'tree> for VisitorT
impl<'a, FnT> Walker<'a> for FnT
where
VisitorT: Visitor<'tree>,
VisitorT: Clone,
anyhow::Error: From<VisitorT::Error>,
VisitorT::Error: Send,
VisitorT::Error: Sync,
FnT: Fn(Node<'a>) -> Result<bool>,
{
fn walk(&self, n: Node<'tree>) -> Result<bool> {
if !n.visit(self.clone())? {
return Ok(false);
}
for child in n.children() {
if !Self::walk(self, child)? {
return Ok(false);
}
}
Ok(true)
fn walk(&self, n: Node<'a>) -> Result<bool> {
self(n)
}
}
/// 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
WalkT: Walker<'a>,
{
let prog: Node = prog.into();
f.walk(prog)
if !f.walk(prog.into())? {
return Ok(false);
}
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)]
@ -71,10 +345,10 @@ const bar = 2
"
);
walk(&program, |node| {
walk(&program, &|node| {
if let Node::VariableDeclarator(vd) = node {
if vd.id.name == "foo" {
return Ok::<bool, anyhow::Error>(false);
return Ok(false);
}
panic!("walk didn't stop");
}

View File

@ -1,5 +1,4 @@
mod ast_node;
mod ast_visitor;
mod ast_walk;
pub use ast_node::Node;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 127 KiB

View File

@ -87,6 +87,72 @@ snapshot_kind: text
}
]
},
"arc_tag": {
"type": "TagIdentifier",
"type": "TagIdentifier",
"value": "arc_tag",
"info": {
"type": "TagEngineInfo",
"id": "[uuid]",
"sketch": "[uuid]",
"path": {
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
527,
549,
0
]
},
"ccw": true,
"center": [
200.0,
100.0
],
"from": [
280.0,
100.0
],
"radius": 80.0,
"tag": {
"end": 548,
"start": 540,
"type": "TagDeclarator",
"value": "arc_tag"
},
"to": [
280.0,
99.99999999999999
],
"type": "Arc"
},
"surface": {
"faceId": "[uuid]",
"id": "[uuid]",
"sourceRange": [
527,
549,
0
],
"tag": {
"end": 548,
"start": 540,
"type": "TagDeclarator",
"value": "arc_tag"
},
"type": "extrudeArc"
}
},
"__meta": [
{
"sourceRange": [
540,
548,
0
]
}
]
},
"b": {
"type": "TagIdentifier",
"type": "TagIdentifier",

View File

@ -1,586 +0,0 @@
---
source: kcl/src/simulation_tests.rs
description: Result of parsing tag_can_be_proxied_through_parameter.kcl
snapshot_kind: text
---
{
"Ok": {
"body": [
{
"declaration": {
"end": 118,
"id": {
"end": 47,
"name": "myCircle",
"start": 39,
"type": "Identifier"
},
"init": {
"body": {
"body": [
{
"argument": {
"arguments": [
{
"end": 106,
"properties": [
{
"end": 87,
"key": {
"end": 83,
"name": "radius",
"start": 77,
"type": "Identifier"
},
"start": 77,
"type": "ObjectProperty",
"value": {
"end": 87,
"raw": "4",
"start": 86,
"type": "Literal",
"type": "Literal",
"value": 4.0
}
},
{
"end": 104,
"key": {
"end": 95,
"name": "center",
"start": 89,
"type": "Identifier"
},
"start": 89,
"type": "ObjectProperty",
"value": {
"elements": [
{
"end": 100,
"raw": "0",
"start": 99,
"type": "Literal",
"type": "Literal",
"value": 0.0
},
{
"end": 103,
"raw": "0",
"start": 102,
"type": "Literal",
"type": "Literal",
"value": 0.0
}
],
"end": 104,
"start": 98,
"type": "ArrayExpression",
"type": "ArrayExpression"
}
}
],
"start": 75,
"type": "ObjectExpression",
"type": "ObjectExpression"
},
{
"end": 110,
"name": "sk",
"start": 108,
"type": "Identifier",
"type": "Identifier"
},
{
"end": 115,
"name": "tag",
"start": 112,
"type": "Identifier",
"type": "Identifier"
}
],
"callee": {
"end": 74,
"name": "circle",
"start": 68,
"type": "Identifier"
},
"end": 116,
"start": 68,
"type": "CallExpression",
"type": "CallExpression"
},
"end": 116,
"start": 61,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 118,
"start": 57
},
"end": 118,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 50,
"name": "sk",
"start": 48,
"type": "Identifier"
}
},
{
"type": "Parameter",
"identifier": {
"end": 55,
"name": "tag",
"start": 52,
"type": "Identifier"
}
}
],
"start": 47,
"type": "FunctionExpression",
"type": "FunctionExpression"
},
"start": 39,
"type": "VariableDeclarator"
},
"end": 118,
"kind": "fn",
"start": 36,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 223,
"id": {
"end": 122,
"name": "c1",
"start": 120,
"type": "Identifier"
},
"init": {
"body": [
{
"arguments": [
{
"end": 143,
"raw": "'XY'",
"start": 139,
"type": "Literal",
"type": "Literal",
"value": "XY"
}
],
"callee": {
"end": 138,
"name": "startSketchOn",
"start": 125,
"type": "Identifier"
},
"end": 144,
"start": 125,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"elements": [
{
"end": 167,
"raw": "0",
"start": 166,
"type": "Literal",
"type": "Literal",
"value": 0.0
},
{
"end": 170,
"raw": "0",
"start": 169,
"type": "Literal",
"type": "Literal",
"value": 0.0
}
],
"end": 171,
"start": 165,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
{
"end": 174,
"start": 173,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 164,
"name": "startProfileAt",
"start": 150,
"type": "Identifier"
},
"end": 175,
"start": 150,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"end": 191,
"start": 190,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
},
{
"end": 198,
"start": 193,
"type": "TagDeclarator",
"type": "TagDeclarator",
"value": "mine"
}
],
"callee": {
"end": 189,
"name": "myCircle",
"start": 181,
"type": "Identifier"
},
"end": 199,
"start": 181,
"type": "CallExpression",
"type": "CallExpression"
}
],
"end": 223,
"nonCodeMeta": {
"nonCodeNodes": {
"2": [
{
"end": 223,
"start": 199,
"type": "NonCodeNode",
"value": {
"type": "blockComment",
"value": "The tag can be used.",
"style": "line"
}
}
]
},
"startNodes": []
},
"start": 125,
"type": "PipeExpression",
"type": "PipeExpression"
},
"start": 120,
"type": "VariableDeclarator"
},
"end": 223,
"kind": "const",
"start": 120,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 257,
"id": {
"end": 228,
"name": "ang1",
"start": 224,
"type": "Identifier"
},
"init": {
"arguments": [
{
"computed": false,
"end": 256,
"object": {
"computed": false,
"end": 251,
"object": {
"end": 246,
"name": "c1",
"start": 244,
"type": "Identifier",
"type": "Identifier"
},
"property": {
"end": 251,
"name": "tags",
"start": 247,
"type": "Identifier",
"type": "Identifier"
},
"start": 244,
"type": "MemberExpression",
"type": "MemberExpression"
},
"property": {
"end": 256,
"name": "mine",
"start": 252,
"type": "Identifier",
"type": "Identifier"
},
"start": 244,
"type": "MemberExpression",
"type": "MemberExpression"
}
],
"callee": {
"end": 243,
"name": "tangentToEnd",
"start": 231,
"type": "Identifier"
},
"end": 257,
"start": 231,
"type": "CallExpression",
"type": "CallExpression"
},
"start": 224,
"type": "VariableDeclarator"
},
"end": 257,
"kind": "const",
"start": 224,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 384,
"id": {
"end": 307,
"name": "c2",
"start": 305,
"type": "Identifier"
},
"init": {
"body": [
{
"arguments": [
{
"end": 328,
"raw": "'XY'",
"start": 324,
"type": "Literal",
"type": "Literal",
"value": "XY"
}
],
"callee": {
"end": 323,
"name": "startSketchOn",
"start": 310,
"type": "Identifier"
},
"end": 329,
"start": 310,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"elements": [
{
"end": 352,
"raw": "0",
"start": 351,
"type": "Literal",
"type": "Literal",
"value": 0.0
},
{
"end": 355,
"raw": "0",
"start": 354,
"type": "Literal",
"type": "Literal",
"value": 0.0
}
],
"end": 356,
"start": 350,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
{
"end": 359,
"start": 358,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 349,
"name": "startProfileAt",
"start": 335,
"type": "Identifier"
},
"end": 360,
"start": 335,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"end": 376,
"start": 375,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
},
{
"end": 383,
"start": 378,
"type": "TagDeclarator",
"type": "TagDeclarator",
"value": "mine"
}
],
"callee": {
"end": 374,
"name": "myCircle",
"start": 366,
"type": "Identifier"
},
"end": 384,
"start": 366,
"type": "CallExpression",
"type": "CallExpression"
}
],
"end": 384,
"start": 310,
"type": "PipeExpression",
"type": "PipeExpression"
},
"start": 305,
"type": "VariableDeclarator"
},
"end": 384,
"kind": "const",
"start": 305,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 418,
"id": {
"end": 389,
"name": "ang2",
"start": 385,
"type": "Identifier"
},
"init": {
"arguments": [
{
"computed": false,
"end": 417,
"object": {
"computed": false,
"end": 412,
"object": {
"end": 407,
"name": "c2",
"start": 405,
"type": "Identifier",
"type": "Identifier"
},
"property": {
"end": 412,
"name": "tags",
"start": 408,
"type": "Identifier",
"type": "Identifier"
},
"start": 405,
"type": "MemberExpression",
"type": "MemberExpression"
},
"property": {
"end": 417,
"name": "mine",
"start": 413,
"type": "Identifier",
"type": "Identifier"
},
"start": 405,
"type": "MemberExpression",
"type": "MemberExpression"
}
],
"callee": {
"end": 404,
"name": "tangentToEnd",
"start": 392,
"type": "Identifier"
},
"end": 418,
"start": 392,
"type": "CallExpression",
"type": "CallExpression"
},
"start": 385,
"type": "VariableDeclarator"
},
"end": 418,
"kind": "const",
"start": 385,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 419,
"nonCodeMeta": {
"nonCodeNodes": {
"0": [
{
"end": 120,
"start": 118,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
],
"2": [
{
"end": 304,
"start": 257,
"type": "NonCodeNode",
"value": {
"type": "newLineBlockComment",
"value": "The same tag declarator can be used again.",
"style": "line"
}
}
]
},
"startNodes": [
{
"end": 35,
"start": 0,
"type": "NonCodeNode",
"value": {
"type": "blockComment",
"value": "A function with a tag parameter.",
"style": "line"
}
}
]
},
"start": 0
}
}

View File

@ -1,16 +0,0 @@
// A function with a tag parameter.
fn myCircle(sk, tag) {
return circle({ radius = 4, center = [0, 0] }, sk, tag)
}
c1 = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> myCircle(%, $mine)
// The tag can be used.
ang1 = tangentToEnd(c1.tags.mine)
// The same tag declarator can be used again.
c2 = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> myCircle(%, $mine)
ang2 = tangentToEnd(c2.tags.mine)

View File

@ -1,524 +0,0 @@
---
source: kcl/src/simulation_tests.rs
description: Program memory after executing tag_can_be_proxied_through_parameter.kcl
snapshot_kind: text
---
{
"environments": [
{
"bindings": {
"HALF_TURN": {
"type": "Number",
"value": 180.0,
"__meta": []
},
"QUARTER_TURN": {
"type": "Number",
"value": 90.0,
"__meta": []
},
"THREE_QUARTER_TURN": {
"type": "Number",
"value": 270.0,
"__meta": []
},
"ZERO": {
"type": "Number",
"value": 0.0,
"__meta": []
},
"ang1": {
"type": "Number",
"value": 90.0,
"__meta": [
{
"sourceRange": [
231,
257,
0
]
}
]
},
"ang2": {
"type": "Number",
"value": 90.0,
"__meta": [
{
"sourceRange": [
392,
418,
0
]
}
]
},
"c1": {
"type": "Sketch",
"value": {
"type": "Sketch",
"id": "[uuid]",
"paths": [
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
68,
116,
0
]
},
"ccw": true,
"center": [
0.0,
0.0
],
"from": [
4.0,
0.0
],
"radius": 4.0,
"tag": {
"end": 198,
"start": 193,
"type": "TagDeclarator",
"value": "mine"
},
"to": [
4.0,
0.0
],
"type": "Circle"
}
],
"on": {
"type": "plane",
"id": "[uuid]",
"value": "XY",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"xAxis": {
"x": 1.0,
"y": 0.0,
"z": 0.0
},
"yAxis": {
"x": 0.0,
"y": 1.0,
"z": 0.0
},
"zAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0
},
"__meta": []
},
"start": {
"from": [
4.0,
0.0
],
"to": [
4.0,
0.0
],
"tag": null,
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
68,
116,
0
]
}
},
"tags": {
"mine": {
"type": "TagIdentifier",
"value": "mine",
"info": {
"type": "TagEngineInfo",
"id": "[uuid]",
"sketch": "[uuid]",
"path": {
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
68,
116,
0
]
},
"ccw": true,
"center": [
0.0,
0.0
],
"from": [
4.0,
0.0
],
"radius": 4.0,
"tag": {
"end": 198,
"start": 193,
"type": "TagDeclarator",
"value": "mine"
},
"to": [
4.0,
0.0
],
"type": "Circle"
},
"surface": null
},
"__meta": [
{
"sourceRange": [
193,
198,
0
]
}
]
}
},
"__meta": [
{
"sourceRange": [
68,
116,
0
]
}
]
}
},
"c2": {
"type": "Sketch",
"value": {
"type": "Sketch",
"id": "[uuid]",
"paths": [
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
68,
116,
0
]
},
"ccw": true,
"center": [
0.0,
0.0
],
"from": [
4.0,
0.0
],
"radius": 4.0,
"tag": {
"end": 383,
"start": 378,
"type": "TagDeclarator",
"value": "mine"
},
"to": [
4.0,
0.0
],
"type": "Circle"
}
],
"on": {
"type": "plane",
"id": "[uuid]",
"value": "XY",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"xAxis": {
"x": 1.0,
"y": 0.0,
"z": 0.0
},
"yAxis": {
"x": 0.0,
"y": 1.0,
"z": 0.0
},
"zAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0
},
"__meta": []
},
"start": {
"from": [
4.0,
0.0
],
"to": [
4.0,
0.0
],
"tag": null,
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
68,
116,
0
]
}
},
"tags": {
"mine": {
"type": "TagIdentifier",
"value": "mine",
"info": {
"type": "TagEngineInfo",
"id": "[uuid]",
"sketch": "[uuid]",
"path": {
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
68,
116,
0
]
},
"ccw": true,
"center": [
0.0,
0.0
],
"from": [
4.0,
0.0
],
"radius": 4.0,
"tag": {
"end": 383,
"start": 378,
"type": "TagDeclarator",
"value": "mine"
},
"to": [
4.0,
0.0
],
"type": "Circle"
},
"surface": null
},
"__meta": [
{
"sourceRange": [
378,
383,
0
]
}
]
}
},
"__meta": [
{
"sourceRange": [
68,
116,
0
]
}
]
}
},
"myCircle": {
"type": "Function",
"expression": {
"body": {
"body": [
{
"argument": {
"arguments": [
{
"end": 106,
"properties": [
{
"end": 87,
"key": {
"end": 83,
"name": "radius",
"start": 77,
"type": "Identifier"
},
"start": 77,
"type": "ObjectProperty",
"value": {
"end": 87,
"raw": "4",
"start": 86,
"type": "Literal",
"type": "Literal",
"value": 4.0
}
},
{
"end": 104,
"key": {
"end": 95,
"name": "center",
"start": 89,
"type": "Identifier"
},
"start": 89,
"type": "ObjectProperty",
"value": {
"elements": [
{
"end": 100,
"raw": "0",
"start": 99,
"type": "Literal",
"type": "Literal",
"value": 0.0
},
{
"end": 103,
"raw": "0",
"start": 102,
"type": "Literal",
"type": "Literal",
"value": 0.0
}
],
"end": 104,
"start": 98,
"type": "ArrayExpression",
"type": "ArrayExpression"
}
}
],
"start": 75,
"type": "ObjectExpression",
"type": "ObjectExpression"
},
{
"end": 110,
"name": "sk",
"start": 108,
"type": "Identifier",
"type": "Identifier"
},
{
"end": 115,
"name": "tag",
"start": 112,
"type": "Identifier",
"type": "Identifier"
}
],
"callee": {
"end": 74,
"name": "circle",
"start": 68,
"type": "Identifier"
},
"end": 116,
"start": 68,
"type": "CallExpression",
"type": "CallExpression"
},
"end": 116,
"start": 61,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 118,
"start": 57
},
"end": 118,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 50,
"name": "sk",
"start": 48,
"type": "Identifier"
}
},
{
"type": "Parameter",
"identifier": {
"end": 55,
"name": "tag",
"start": 52,
"type": "Identifier"
}
}
],
"start": 47,
"type": "FunctionExpression"
},
"memory": {
"environments": [
{
"bindings": {
"HALF_TURN": {
"type": "Number",
"value": 180.0,
"__meta": []
},
"QUARTER_TURN": {
"type": "Number",
"value": 90.0,
"__meta": []
},
"THREE_QUARTER_TURN": {
"type": "Number",
"value": 270.0,
"__meta": []
},
"ZERO": {
"type": "Number",
"value": 0.0,
"__meta": []
}
},
"parent": null
}
],
"currentEnv": 0,
"return": null
},
"__meta": [
{
"sourceRange": [
47,
118,
0
]
}
]
}
},
"parent": null
}
],
"currentEnv": 0,
"return": null
}

View File

@ -1,400 +0,0 @@
---
source: kcl/src/simulation_tests.rs
description: Result of parsing tag_proxied_through_function_does_not_define_var.kcl
snapshot_kind: text
---
{
"Ok": {
"body": [
{
"declaration": {
"end": 193,
"id": {
"end": 47,
"name": "myCircle",
"start": 39,
"type": "Identifier"
},
"init": {
"body": {
"body": [
{
"declaration": {
"end": 113,
"id": {
"end": 62,
"name": "c",
"start": 61,
"type": "Identifier"
},
"init": {
"arguments": [
{
"end": 103,
"properties": [
{
"end": 84,
"key": {
"end": 80,
"name": "radius",
"start": 74,
"type": "Identifier"
},
"start": 74,
"type": "ObjectProperty",
"value": {
"end": 84,
"raw": "4",
"start": 83,
"type": "Literal",
"type": "Literal",
"value": 4.0
}
},
{
"end": 101,
"key": {
"end": 92,
"name": "center",
"start": 86,
"type": "Identifier"
},
"start": 86,
"type": "ObjectProperty",
"value": {
"elements": [
{
"end": 97,
"raw": "0",
"start": 96,
"type": "Literal",
"type": "Literal",
"value": 0.0
},
{
"end": 100,
"raw": "0",
"start": 99,
"type": "Literal",
"type": "Literal",
"value": 0.0
}
],
"end": 101,
"start": 95,
"type": "ArrayExpression",
"type": "ArrayExpression"
}
}
],
"start": 72,
"type": "ObjectExpression",
"type": "ObjectExpression"
},
{
"end": 107,
"name": "sk",
"start": 105,
"type": "Identifier",
"type": "Identifier"
},
{
"end": 112,
"name": "tag",
"start": 109,
"type": "Identifier",
"type": "Identifier"
}
],
"callee": {
"end": 71,
"name": "circle",
"start": 65,
"type": "Identifier"
},
"end": 113,
"start": 65,
"type": "CallExpression",
"type": "CallExpression"
},
"start": 61,
"type": "VariableDeclarator"
},
"end": 113,
"kind": "const",
"start": 61,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 173,
"id": {
"end": 152,
"name": "ang",
"start": 149,
"type": "Identifier"
},
"init": {
"arguments": [
{
"end": 172,
"name": "mine",
"start": 168,
"type": "Identifier",
"type": "Identifier"
}
],
"callee": {
"end": 167,
"name": "tangentToEnd",
"start": 155,
"type": "Identifier"
},
"end": 173,
"start": 155,
"type": "CallExpression",
"type": "CallExpression"
},
"start": 149,
"type": "VariableDeclarator"
},
"end": 173,
"kind": "const",
"start": 149,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"argument": {
"elements": [
{
"end": 185,
"name": "c",
"start": 184,
"type": "Identifier",
"type": "Identifier"
},
{
"end": 190,
"name": "ang",
"start": 187,
"type": "Identifier",
"type": "Identifier"
}
],
"end": 191,
"start": 183,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
"end": 191,
"start": 176,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 193,
"nonCodeMeta": {
"nonCodeNodes": {
"0": [
{
"end": 146,
"start": 115,
"type": "NonCodeNode",
"value": {
"type": "blockComment",
"value": "This should not be allowed.",
"style": "line"
}
}
]
},
"startNodes": []
},
"start": 57
},
"end": 193,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 50,
"name": "sk",
"start": 48,
"type": "Identifier"
}
},
{
"type": "Parameter",
"identifier": {
"end": 55,
"name": "tag",
"start": 52,
"type": "Identifier"
}
}
],
"start": 47,
"type": "FunctionExpression",
"type": "FunctionExpression"
},
"start": 39,
"type": "VariableDeclarator"
},
"end": 193,
"kind": "fn",
"start": 36,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 274,
"id": {
"end": 197,
"name": "c1",
"start": 195,
"type": "Identifier"
},
"init": {
"body": [
{
"arguments": [
{
"end": 218,
"raw": "'XY'",
"start": 214,
"type": "Literal",
"type": "Literal",
"value": "XY"
}
],
"callee": {
"end": 213,
"name": "startSketchOn",
"start": 200,
"type": "Identifier"
},
"end": 219,
"start": 200,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"elements": [
{
"end": 242,
"raw": "0",
"start": 241,
"type": "Literal",
"type": "Literal",
"value": 0.0
},
{
"end": 245,
"raw": "0",
"start": 244,
"type": "Literal",
"type": "Literal",
"value": 0.0
}
],
"end": 246,
"start": 240,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
{
"end": 249,
"start": 248,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 239,
"name": "startProfileAt",
"start": 225,
"type": "Identifier"
},
"end": 250,
"start": 225,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"end": 266,
"start": 265,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
},
{
"end": 273,
"start": 268,
"type": "TagDeclarator",
"type": "TagDeclarator",
"value": "mine"
}
],
"callee": {
"end": 264,
"name": "myCircle",
"start": 256,
"type": "Identifier"
},
"end": 274,
"start": 256,
"type": "CallExpression",
"type": "CallExpression"
}
],
"end": 274,
"start": 200,
"type": "PipeExpression",
"type": "PipeExpression"
},
"start": 195,
"type": "VariableDeclarator"
},
"end": 274,
"kind": "const",
"start": 195,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 275,
"nonCodeMeta": {
"nonCodeNodes": {
"0": [
{
"end": 195,
"start": 193,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
]
},
"startNodes": [
{
"end": 35,
"start": 0,
"type": "NonCodeNode",
"value": {
"type": "blockComment",
"value": "A function with a tag parameter.",
"style": "line"
}
}
]
},
"start": 0
}
}

View File

@ -1,20 +0,0 @@
---
source: kcl/src/simulation_tests.rs
description: Error from executing tag_proxied_through_function_does_not_define_var.kcl
snapshot_kind: text
---
KCL UndefinedValue error
× undefined value: memory item key `mine` is not defined
╭─[5:22]
4 │ // This should not be allowed.
5 │ ang = tangentToEnd(mine)
· ────
6 │ return [c, ang]
7 │ }
8 │
9 │ c1 = startSketchOn('XY')
10 │ |> startProfileAt([0, 0], %)
11 │ |> myCircle(%, $mine)
· ──────────────────
╰────

View File

@ -1,11 +0,0 @@
// A function with a tag parameter.
fn myCircle(sk, tag) {
c = circle({ radius = 4, center = [0, 0] }, sk, tag)
// This should not be allowed.
ang = tangentToEnd(mine)
return [c, ang]
}
c1 = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> myCircle(%, $mine)

View File

@ -1,6 +1,6 @@
//! Wasm bindings for `kcl`.
use std::sync::Arc;
use std::{str::FromStr, sync::Arc};
use futures::stream::TryStreamExt;
use gloo_utils::format::JsValueSerdeExt;
@ -56,10 +56,10 @@ pub async fn clear_scene_and_bust_cache(
// wasm_bindgen wrapper for execute
#[wasm_bindgen]
pub async fn execute(
pub async fn execute_wasm(
program_ast_json: &str,
program_memory_override_str: &str,
settings: &str,
units: &str,
engine_manager: kcl_lib::wasm_engine::EngineCommandManager,
fs_manager: kcl_lib::wasm_engine::FileSystemManager,
) -> Result<JsValue, String> {
@ -73,11 +73,11 @@ pub async fn execute(
// You cannot override the memory in non-mock mode.
let is_mock = program_memory_override.is_some();
let settings: kcl_lib::Configuration = serde_json::from_str(settings).map_err(|e| e.to_string())?;
let units = kcl_lib::UnitLength::from_str(units).map_err(|e| e.to_string())?;
let ctx = if is_mock {
kcl_lib::ExecutorContext::new_mock(fs_manager, settings.into()).await?
kcl_lib::ExecutorContext::new_mock(fs_manager, units).await?
} else {
kcl_lib::ExecutorContext::new(engine_manager, fs_manager, settings.into()).await?
kcl_lib::ExecutorContext::new(engine_manager, fs_manager, units).await?
};
let mut exec_state = ExecState::default();
@ -168,6 +168,23 @@ pub async fn make_default_planes(
JsValue::from_serde(&default_planes).map_err(|e| e.to_string())
}
// wasm_bindgen wrapper for modifying the grid
#[wasm_bindgen]
pub async fn modify_grid(
engine_manager: kcl_lib::wasm_engine::EngineCommandManager,
hidden: bool,
) -> Result<(), String> {
console_error_panic_hook::set_once();
// deserialize the ast from a stringified json
let engine = kcl_lib::wasm_engine::EngineConnection::new(engine_manager)
.await
.map_err(|e| format!("{:?}", e))?;
engine.modify_grid(hidden).await.map_err(String::from)?;
Ok(())
}
// wasm_bindgen wrapper for execute
#[wasm_bindgen]
pub async fn modify_ast_for_sketch_wasm(
@ -279,7 +296,7 @@ impl ServerConfig {
pub async fn kcl_lsp_run(
config: ServerConfig,
engine_manager: Option<kcl_lib::wasm_engine::EngineCommandManager>,
settings: Option<String>,
units: &str,
token: String,
baseurl: String,
) -> Result<(), JsValue> {
@ -292,12 +309,8 @@ pub async fn kcl_lsp_run(
} = config;
let executor_ctx = if let Some(engine_manager) = engine_manager {
let settings: kcl_lib::Configuration = if let Some(settings) = settings {
serde_json::from_str(&settings).map_err(|e| e.to_string())?
} else {
Default::default()
};
Some(kcl_lib::ExecutorContext::new(engine_manager, fs.clone(), settings.into()).await?)
let units = kcl_lib::UnitLength::from_str(units).map_err(|e| e.to_string())?;
Some(kcl_lib::ExecutorContext::new(engine_manager, fs.clone(), units).await?)
} else {
None
};

View File

@ -1,216 +0,0 @@
//! Cache testing framework.
use anyhow::Result;
use kcl_lib::ExecError;
struct Variation<'a> {
code: &'a str,
settings: &'a kcl_lib::ExecutorSettings,
}
async fn cache_test(test_name: &str, variations: Vec<Variation<'_>>) -> Result<Vec<(String, image::DynamicImage)>> {
let first = variations
.first()
.ok_or_else(|| anyhow::anyhow!("No variations provided for test '{}'", test_name))?;
let mut ctx = kcl_lib::ExecutorContext::new_with_client(first.settings.clone(), None, None).await?;
let mut exec_state = kcl_lib::ExecState::default();
let mut old_ast_state = None;
let mut img_results = Vec::new();
for (index, variation) in variations.iter().enumerate() {
let program = kcl_lib::Program::parse_no_errs(variation.code)?;
// set the new settings.
ctx.settings = variation.settings.clone();
ctx.run(
kcl_lib::CacheInformation {
old: old_ast_state,
new_ast: program.ast.clone(),
},
&mut exec_state,
)
.await?;
let snapshot_png_bytes = ctx.prepare_snapshot().await?.contents.0;
// Decode the snapshot, return it.
let img = image::ImageReader::new(std::io::Cursor::new(snapshot_png_bytes))
.with_guessed_format()
.map_err(|e| ExecError::BadPng(e.to_string()))
.and_then(|x| x.decode().map_err(|e| ExecError::BadPng(e.to_string())))?;
// Save the snapshot.
let path = crate::assert_out(&format!("cache_{}_{}", test_name, index), &img);
img_results.push((path, img));
// Prepare the last state.
old_ast_state = Some(kcl_lib::OldAstState {
ast: program.ast,
exec_state: exec_state.clone(),
settings: variation.settings.clone(),
});
}
Ok(img_results)
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_cache_change_units_changes_output() {
let code = r#"part001 = startSketchOn('XY')
|> startProfileAt([5.5229, 5.25217], %)
|> line([10.50433, -1.19122], %)
|> line([8.01362, -5.48731], %)
|> line([-1.02877, -6.76825], %)
|> line([-11.53311, 2.81559], %)
|> close(%)
|> extrude(4, %)
"#;
let result = cache_test(
"change_units_changes_output",
vec![
Variation {
code,
settings: &kcl_lib::ExecutorSettings {
units: kcl_lib::UnitLength::In,
..Default::default()
},
},
Variation {
code,
settings: &kcl_lib::ExecutorSettings {
units: kcl_lib::UnitLength::Mm,
..Default::default()
},
},
],
)
.await
.unwrap();
let first = result.first().unwrap();
let second = result.last().unwrap();
assert!(first.1 != second.1);
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_cache_change_grid_visualizes_grid_off_to_on() {
let code = r#"part001 = startSketchOn('XY')
|> startProfileAt([5.5229, 5.25217], %)
|> line([10.50433, -1.19122], %)
|> line([8.01362, -5.48731], %)
|> line([-1.02877, -6.76825], %)
|> line([-11.53311, 2.81559], %)
|> close(%)
|> extrude(4, %)
"#;
let result = cache_test(
"change_grid_visualizes_grid_off_to_on",
vec![
Variation {
code,
settings: &kcl_lib::ExecutorSettings {
show_grid: false,
..Default::default()
},
},
Variation {
code,
settings: &kcl_lib::ExecutorSettings {
show_grid: true,
..Default::default()
},
},
],
)
.await
.unwrap();
let first = result.first().unwrap();
let second = result.last().unwrap();
assert!(first.1 != second.1);
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_cache_change_grid_visualizes_grid_on_to_off() {
let code = r#"part001 = startSketchOn('XY')
|> startProfileAt([5.5229, 5.25217], %)
|> line([10.50433, -1.19122], %)
|> line([8.01362, -5.48731], %)
|> line([-1.02877, -6.76825], %)
|> line([-11.53311, 2.81559], %)
|> close(%)
|> extrude(4, %)
"#;
let result = cache_test(
"change_grid_visualizes_grid_on_to_off",
vec![
Variation {
code,
settings: &kcl_lib::ExecutorSettings {
show_grid: true,
..Default::default()
},
},
Variation {
code,
settings: &kcl_lib::ExecutorSettings {
show_grid: false,
..Default::default()
},
},
],
)
.await
.unwrap();
let first = result.first().unwrap();
let second = result.last().unwrap();
assert!(first.1 != second.1);
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_cache_change_highlight_edges_changes_visual() {
let code = r#"part001 = startSketchOn('XY')
|> startProfileAt([5.5229, 5.25217], %)
|> line([10.50433, -1.19122], %)
|> line([8.01362, -5.48731], %)
|> line([-1.02877, -6.76825], %)
|> line([-11.53311, 2.81559], %)
|> close(%)
|> extrude(4, %)
"#;
let result = cache_test(
"change_highlight_edges_changes_visual",
vec![
Variation {
code,
settings: &kcl_lib::ExecutorSettings {
highlight_edges: true,
..Default::default()
},
},
Variation {
code,
settings: &kcl_lib::ExecutorSettings {
highlight_edges: false,
..Default::default()
},
},
],
)
.await
.unwrap();
let first = result.first().unwrap();
let second = result.last().unwrap();
assert!(first.1 != second.1);
}

View File

@ -1,5 +1,3 @@
mod cache;
use kcl_lib::{
test_server::{execute_and_snapshot, execute_and_snapshot_no_auth},
UnitLength,
@ -7,7 +5,7 @@ use kcl_lib::{
/// The minimum permissible difference between asserted twenty-twenty images.
/// i.e. how different the current model snapshot can be from the previous saved one.
pub(crate) const MIN_DIFF: f64 = 0.99;
const MIN_DIFF: f64 = 0.99;
macro_rules! kcl_input {
($file:literal) => {
@ -15,11 +13,8 @@ macro_rules! kcl_input {
};
}
pub(crate) fn assert_out(test_name: &str, result: &image::DynamicImage) -> String {
let path = format!("tests/executor/outputs/{test_name}.png");
twenty_twenty::assert_image(&path, result, MIN_DIFF);
path
fn assert_out(test_name: &str, result: &image::DynamicImage) {
twenty_twenty::assert_image(format!("tests/executor/outputs/{test_name}.png"), result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 43 KiB