Compare commits

...

14 Commits

Author SHA1 Message Date
30afa65ccf Cut release v0.27.0 (#4516)
* Cut release v0.26.6

* Cut release v0.27.0
2024-11-20 10:09:53 -06:00
a2f9e70d18 breaking change: Change "type" to be a keyword, import() "type:" parameter to "format:" (#4517) 2024-11-20 14:53:37 +00:00
986675fe89 Fix formatting for nested function returns (#4518)
Previously, this was the output of the formatter:

```
fn f = () => {
  return () => {
  return 1
}
}
```

Now the above will be reformatted as

```
fn f = () => {
  return () => {
    return 1
  }
}
```

Much better!
2024-11-20 09:23:30 -05:00
d8ce5ad8bd Make most top-level modules in KCL private (#4478)
* Make ast module private

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Make most other modules private

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Expand API to support CLI, Python bindings, and LSP crate

Signed-off-by: Nick Cameron <nrc@ncameron.org>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-11-20 15:19:25 +13:00
1a9926be8a Fix to not have unneeded console errors (#4482)
* Fix to not have unneeded console errors

* Change to use a type that isn't string
2024-11-19 17:34:54 -05:00
max
54b5774f9e Add Support for Fillet with Extrude in the Sketch Pipe (#4168)
* update code mod

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

* fmt

* lint

* make yarn-tsc happy

* fmt

* typo

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-19 21:30:26 +01:00
66bbbf81e2 Pass current file name through to export command (#4503)
* Pass current file name through to export command

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

* Oops I needed a couple other things, not just that one line change

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

* Undo overriding of internal zipped file names
That was liable to cause conflicts and whatnot per @jessfraz feedback

* Update E2E test that was still looking for `output.gltf`

* Missed one other test my bad

* Should've just grepped for output.gltf to begin with

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Paul Tagliamonte <paul@zoo.dev>
2024-11-19 11:30:23 -05:00
652519aeae fix: center rectangle code writing bug (#4512)
fix: bug that did not write the code to the editor when the workflow finished
2024-11-19 11:10:08 -05:00
f826afb32d Clear code mirror history on file change (#4510)
* clear history when loading a new file

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-11-19 02:08:22 +00:00
f71fafdece breaking change: Add more KCL reserved words, part 1 (#4502) 2024-11-19 00:54:25 +00:00
16b7544d69 fix missing docs files (#4509)
* fix missing docs files

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix justfile

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix justfile

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-11-18 22:58:33 +00:00
34f019305b Move tests into simulation (#4487)
Replace wasm-lib integration tests with KCL simulation tests.
2024-11-18 22:20:32 +00:00
79e06b3a00 Nadro/adhoc/stdlib arcTo (three point arc) (#4485)
* feat: implementing arcTo in standard library, first pass

* feat: computing center and radius for arcto

* fix: updating comment for arcTo

* fix: cargo fmt fix

* fix: bug, the x was used twice!

* fix: Cleaning up some code and adding more comments

* fix: this has to be removed

* fix: resolved merge conflicts with main and updated the codebase to remove the JSON stuff

* fix: addressing cargo clippy issues

* fix: typos

* fix: adding generated docs

* Update doc test snapshots

---------

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2024-11-18 22:17:16 +00:00
24bc4fcd8c Show offset planes in the scene, let user select them (#4481)
* Update offset_plane to actually create and show the plane in-engine

* Fix broken ability to use offsetPlanes in startSketchOn

* Make the newly-visible offset planes usable for sketching via UI

* Add a playwright test for sketching on an offset plane via point-and-click

* cargo clippy & cargo fmt

* Make `PlaneData` the first item of `SketchData` so autocomplete continues to work well for `startSketchOn`

* @nadr0 feedback re: `offsetIndex`

* From @jtran: "Need to call the ID generator so that IDs are stable."

* More feedback from @jtran and fix incomplete use of `id_generator` in last commit

* Oops I missed saving `isPathToNodeNumber` earlier 🤦🏻

* Make the distinction between `Plane` and `PlaneOrientationData` more clear per @nadr0 and @lf94's feedback

* Make `newPathToNode` less hardcoded, per @lf94's feedback

* Don't need to unbox and rebox `plane`

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

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

* Rearranging of enums and structs, but the offsetPlanes are still not used by their sketches

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

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

* Revert all my little newtype fiddling it's a waste of time.

* Update docs

* cargo fmt

* Remove log

* Print the unexpected diagnostics

* Undo renaming of `PlaneData`

* Remove generated PlaneRientationData docs page

* Redo doc generation after undoing `PlaneData` rename

* Impl FromKclValue for the new plane datatypes

* Clippy lint

* When starting a sketch, only hide the plane if it's a custom plane

* Fix FromKclValue and macro use since merge

* Fix to not convert Plane to PlaneData

* Make sure offset planes are `Custom` type

* SketchData actually doesn't need to be in a certain order
This avoids the autocompletion issue I was having.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Adam Chalmers <adam.chalmers@zoo.dev>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2024-11-18 16:25:25 -05:00
323 changed files with 369291 additions and 3365 deletions

File diff suppressed because one or more lines are too long

41
docs/kcl/arcTo.md Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -20,6 +20,7 @@ layout: manual
* [`angledLineToX`](kcl/angledLineToX)
* [`angledLineToY`](kcl/angledLineToY)
* [`arc`](kcl/arc)
* [`arcTo`](kcl/arcTo)
* [`asin`](kcl/asin)
* [`assert`](kcl/assert)
* [`assertEqual`](kcl/assertEqual)

File diff suppressed because one or more lines are too long

View File

@ -31,7 +31,7 @@ map(array: [KclValue], map_fn: FunctionParam) -> [KclValue]
r = 10 // radius
fn drawCircle = (id) => {
return startSketchOn("XY")
|> circle({ center: [id * 2 * r, 0], radius: r }, %)
|> circle({ center: [id * 2 * r, 0], radius: r }, %)
}
// Call `drawCircle`, passing in each element of the array.
@ -47,7 +47,7 @@ r = 10 // radius
// Call `map`, using an anonymous function instead of a named one.
circles = map([1..3], (id) => {
return startSketchOn("XY")
|> circle({ center: [id * 2 * r, 0], radius: r }, %)
|> circle({ center: [id * 2 * r, 0], radius: r }, %)
})
```

View File

@ -9,7 +9,7 @@ Offset a plane by a distance along its normal.
For example, if you offset the 'XZ' plane by 10, the new plane will be parallel to the 'XZ' plane and 10 units away from it.
```js
offsetPlane(std_plane: StandardPlane, offset: number) -> PlaneData
offsetPlane(std_plane: StandardPlane, offset: number) -> Plane
```
@ -22,7 +22,7 @@ offsetPlane(std_plane: StandardPlane, offset: number) -> PlaneData
### Returns
[`PlaneData`](/docs/kcl/types/PlaneData) - Data for a plane.
[`Plane`](/docs/kcl/types/Plane) - A plane.
### Examples

View File

@ -96,24 +96,24 @@ fn cube = (length, center) => {
p3 = [l + x, -l + y]
return startSketchAt(p0)
|> lineTo(p1, %)
|> lineTo(p2, %)
|> lineTo(p3, %)
|> lineTo(p0, %)
|> close(%)
|> extrude(length, %)
|> lineTo(p1, %)
|> lineTo(p2, %)
|> lineTo(p3, %)
|> lineTo(p0, %)
|> close(%)
|> extrude(length, %)
}
width = 20
fn transform = (i) => {
return {
// Move down each time.
translate: [0, 0, -i * width],
// Make the cube longer, wider and flatter each time.
scale: [pow(1.1, i), pow(1.1, i), pow(0.9, i)],
// Turn by 15 degrees each time.
rotation: { angle: 15 * i, origin: "local" }
}
// Move down each time.
translate: [0, 0, -i * width],
// Make the cube longer, wider and flatter each time.
scale: [pow(1.1, i), pow(1.1, i), pow(0.9, i)],
// Turn by 15 degrees each time.
rotation: { angle: 15 * i, origin: "local" }
}
}
myCubes = cube(width, [100, 0])
@ -133,25 +133,25 @@ fn cube = (length, center) => {
p3 = [l + x, -l + y]
return startSketchAt(p0)
|> lineTo(p1, %)
|> lineTo(p2, %)
|> lineTo(p3, %)
|> lineTo(p0, %)
|> close(%)
|> extrude(length, %)
|> lineTo(p1, %)
|> lineTo(p2, %)
|> lineTo(p3, %)
|> lineTo(p0, %)
|> close(%)
|> extrude(length, %)
}
width = 20
fn transform = (i) => {
return {
translate: [0, 0, -i * width],
rotation: {
angle: 90 * i,
// Rotate around the overall scene's origin.
origin: "global"
translate: [0, 0, -i * width],
rotation: {
angle: 90 * i,
// Rotate around the overall scene's origin.
origin: "global"
}
}
}
}
myCubes = cube(width, [100, 100])
|> patternTransform(4, transform, %)
```
@ -168,16 +168,16 @@ t = 0.005 // taper factor [0-1)
fn transform = (replicaId) => {
scale = r * abs(1 - (t * replicaId)) * (5 + cos(replicaId / 8))
return {
translate: [0, 0, replicaId * 10],
scale: [scale, scale, 0]
}
translate: [0, 0, replicaId * 10],
scale: [scale, scale, 0]
}
}
// Each layer is just a pretty thin cylinder.
fn layer = () => {
return startSketchOn("XY")
// or some other plane idk
|> circle({ center: [0, 0], radius: 1 }, %, $tag1)
|> extrude(h, %)
// or some other plane idk
|> circle({ center: [0, 0], radius: 1 }, %, $tag1)
|> extrude(h, %)
}
// The vase is 100 layers tall.
// The 100 layers are replica of each other, with a slight transformation applied to each.

View File

@ -36,15 +36,15 @@ fn add = (a, b) => {
// This function adds an array of numbers.
// It uses the `reduce` function, to call the `add` function on every
// element of the `array` parameter. The starting value is 0.
fn sum = (array) => {
return reduce(array, 0, add)
// element of the `arr` parameter. The starting value is 0.
fn sum = (arr) => {
return reduce(arr, 0, add)
}
/* The above is basically like this pseudo-code:
fn sum(array):
fn sum(arr):
let sumSoFar = 0
for i in array:
for i in arr:
sumSoFar = add(sumSoFar, i)
return sumSoFar */
@ -60,8 +60,8 @@ assertEqual(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
// an anonymous `add` function as its parameter, instead of declaring a
// named function outside.
array = [1, 2, 3]
sum = reduce(array, 0, (i, result_so_far) => {
arr = [1, 2, 3]
sum = reduce(arr, 0, (i, result_so_far) => {
return i + result_so_far
})
@ -89,7 +89,7 @@ fn decagon = (radius) => {
x = cos(stepAngle * i) * radius
y = sin(stepAngle * i) * radius
return lineTo([x, y], partialDecagon)
})
})
return fullDecagon
}

View File

@ -38,8 +38,8 @@ cube = startSketchAt([0, 0])
fn cylinder = (radius, tag) => {
return startSketchAt([0, 0])
|> circle({ radius: radius, center: segEnd(tag) }, %)
|> extrude(radius, %)
|> circle({ radius: radius, center: segEnd(tag) }, %)
|> extrude(radius, %)
}
cylinder(1, line1)

View File

@ -38,11 +38,11 @@ cube = startSketchAt([0, 0])
fn cylinder = (radius, tag) => {
return startSketchAt([0, 0])
|> circle({
radius: radius,
center: segStart(tag)
}, %)
|> extrude(radius, %)
|> circle({
radius: radius,
center: segStart(tag)
}, %)
|> extrude(radius, %)
}
cylinder(1, line1)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
---
title: "ArcToData"
excerpt: "Data to draw a three point arc (arcTo)."
layout: manual
---
Data to draw a three point arc (arcTo).
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `end` |`[number, number]`| End point of the arc. A point in 3D space | No |
| `interior` |`[number, number]`| Interior point of the arc. A point in 3D space | No |

View File

@ -24,7 +24,7 @@ Autodesk Filmbox (FBX) format
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `fbx`| | No |
| `format` |enum: `fbx`| | No |
----
@ -40,7 +40,7 @@ Binary glTF 2.0. We refer to this as glTF since that is how our customers refer
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `gltf`| | No |
| `format` |enum: `gltf`| | No |
----
@ -56,7 +56,7 @@ Wavefront OBJ format.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `obj`| | No |
| `format` |enum: `obj`| | No |
| `coords` |[`System`](/docs/kcl/types/System)| Co-ordinate system of input data. Defaults to the [KittyCAD co-ordinate system. | No |
| `units` |[`UnitLength`](/docs/kcl/types/UnitLength)| The units of the input data. This is very important for correct scaling and when calculating physics properties like mass, etc. Defaults to millimeters. | No |
@ -74,7 +74,7 @@ The PLY Polygon File Format.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ply`| | No |
| `format` |enum: `ply`| | No |
| `coords` |[`System`](/docs/kcl/types/System)| Co-ordinate system of input data. Defaults to the [KittyCAD co-ordinate system. | No |
| `units` |[`UnitLength`](/docs/kcl/types/UnitLength)| The units of the input data. This is very important for correct scaling and when calculating physics properties like mass, etc. Defaults to millimeters. | No |
@ -92,7 +92,7 @@ SolidWorks part (SLDPRT) format.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `sldprt`| | No |
| `format` |enum: `sldprt`| | No |
----
@ -108,7 +108,7 @@ ISO 10303-21 (STEP) format.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `step`| | No |
| `format` |enum: `step`| | No |
----
@ -124,7 +124,7 @@ ST**ereo**L**ithography format.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `stl`| | No |
| `format` |enum: `stl`| | No |
| `coords` |[`System`](/docs/kcl/types/System)| Co-ordinate system of input data. Defaults to the [KittyCAD co-ordinate system. | No |
| `units` |[`UnitLength`](/docs/kcl/types/UnitLength)| The units of the input data. This is very important for correct scaling and when calculating physics properties like mass, etc. Defaults to millimeters. | No |

View File

@ -180,7 +180,7 @@ A plane.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Plane`| | No |
| `type` |enum: [`Plane`](/docs/kcl/types/Plane)| | No |
| `id` |`string`| The id of the plane. | No |
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| Any KCL value. | No |
| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No |

27
docs/kcl/types/Plane.md Normal file
View File

@ -0,0 +1,27 @@
---
title: "Plane"
excerpt: "A plane."
layout: manual
---
A plane.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `id` |`string`| The id of the plane. | No |
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| A plane. | No |
| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No |
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -1,10 +1,10 @@
---
title: "PlaneData"
excerpt: "Data for a plane."
excerpt: "Orientation data that can be used to construct a plane, not a plane in itself."
layout: manual
---
Data for a plane.
Orientation data that can be used to construct a plane, not a plane in itself.

View File

@ -22,6 +22,18 @@ Data for start sketch on. You can start a sketch on a plane or an solid.
----
Data for start sketch on. You can start a sketch on a plane or an solid.
[`Plane`](/docs/kcl/types/Plane)
----
Data for start sketch on. You can start a sketch on a plane or an solid.

View File

@ -62,6 +62,8 @@ test(
const errorToastMessage = page.getByText(`Error while exporting`)
const engineErrorToastMessage = page.getByText(`Nothing to export`)
const alreadyExportingToastMessage = page.getByText(`Already exporting`)
// The open file's name is `main.kcl`, so the export file name should be `main.gltf`
const exportFileName = `main.gltf`
// Click the export button
await exportButton.click()
@ -96,7 +98,7 @@ test(
.poll(
async () => {
try {
const outputGltf = await fsp.readFile('output.gltf')
const outputGltf = await fsp.readFile(exportFileName)
return outputGltf.byteLength
} catch (e) {
return 0
@ -106,8 +108,8 @@ test(
)
.toBeGreaterThan(300_000)
// clean up output.gltf
await fsp.rm('output.gltf')
// clean up exported file
await fsp.rm(exportFileName)
})
})
@ -138,6 +140,8 @@ test(
const errorToastMessage = page.getByText(`Error while exporting`)
const engineErrorToastMessage = page.getByText(`Nothing to export`)
const alreadyExportingToastMessage = page.getByText(`Already exporting`)
// The open file's name is `other.kcl`, so the export file name should be `other.gltf`
const exportFileName = `other.gltf`
// Click the export button
await exportButton.click()
@ -171,7 +175,7 @@ test(
.poll(
async () => {
try {
const outputGltf = await fsp.readFile('output.gltf')
const outputGltf = await fsp.readFile(exportFileName)
return outputGltf.byteLength
} catch (e) {
return 0
@ -181,8 +185,8 @@ test(
)
.toBeGreaterThan(100_000)
// clean up output.gltf
await fsp.rm('output.gltf')
// clean up exported file
await fsp.rm(exportFileName)
})
await electronApp.close()
})

View File

@ -1135,3 +1135,189 @@ _test.describe('Deleting items from the file pane', () => {
}
)
})
_test.describe(
'Undo and redo do not keep history when navigating between files',
() => {
_test(
`open a file, change something, open a different file, hitting undo should do nothing`,
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const testDir = join(dir, 'testProject')
await fsp.mkdir(testDir, { recursive: true })
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(testDir, 'main.kcl')
)
await fsp.copyFile(
executorInputPath('basic_fillet_cube_end.kcl'),
join(testDir, 'other.kcl')
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
// Constants and locators
const projectCard = page.getByText('testProject')
const otherFile = page
.getByRole('listitem')
.filter({ has: page.getByRole('button', { name: 'other.kcl' }) })
await _test.step(
'Open project and make a change to the file',
async () => {
await projectCard.click()
await u.waitForPageLoad()
// Get the text in the code locator.
const originalText = await u.codeLocator.innerText()
// Click in the editor and add some new lines.
await u.codeLocator.click()
await page.keyboard.type(`sketch001 = startSketchOn('XY')
some other shit`)
// Ensure the content in the editor changed.
const newContent = await u.codeLocator.innerText()
expect(originalText !== newContent)
}
)
await _test.step('navigate to other.kcl', async () => {
await u.openFilePanel()
await otherFile.click()
await u.waitForPageLoad()
await u.openKclCodePanel()
await _expect(u.codeLocator).toContainText('getOppositeEdge(thing)')
})
await _test.step('hit undo', async () => {
// Get the original content of the file.
const originalText = await u.codeLocator.innerText()
// Now hit undo
await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('KeyZ')
await page.keyboard.up('ControlOrMeta')
await page.waitForTimeout(100)
await expect(u.codeLocator).toContainText(originalText)
})
}
)
_test(
`open a file, change something, undo it, open a different file, hitting redo should do nothing`,
{ tag: '@electron' },
// Skip on windows i think the keybindings are different for redo.
async ({ browserName }, testInfo) => {
test.skip(process.platform === 'win32', 'Skip on windows')
const { page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const testDir = join(dir, 'testProject')
await fsp.mkdir(testDir, { recursive: true })
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(testDir, 'main.kcl')
)
await fsp.copyFile(
executorInputPath('basic_fillet_cube_end.kcl'),
join(testDir, 'other.kcl')
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
// Constants and locators
const projectCard = page.getByText('testProject')
const otherFile = page
.getByRole('listitem')
.filter({ has: page.getByRole('button', { name: 'other.kcl' }) })
const badContent = 'this shit'
await _test.step(
'Open project and make a change to the file',
async () => {
await projectCard.click()
await u.waitForPageLoad()
// Get the text in the code locator.
const originalText = await u.codeLocator.innerText()
// Click in the editor and add some new lines.
await u.codeLocator.click()
await page.keyboard.type(badContent)
// Ensure the content in the editor changed.
const newContent = await u.codeLocator.innerText()
expect(originalText !== newContent)
// Now hit undo
await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('KeyZ')
await page.keyboard.up('ControlOrMeta')
await page.waitForTimeout(100)
await expect(u.codeLocator).toContainText(originalText)
await expect(u.codeLocator).not.toContainText(badContent)
// Hit redo.
await page.keyboard.down('Shift')
await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('KeyZ')
await page.keyboard.up('ControlOrMeta')
await page.keyboard.up('Shift')
await page.waitForTimeout(100)
await expect(u.codeLocator).toContainText(originalText)
await expect(u.codeLocator).toContainText(badContent)
// Now hit undo
await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('KeyZ')
await page.keyboard.up('ControlOrMeta')
await page.waitForTimeout(100)
await expect(u.codeLocator).toContainText(originalText)
await expect(u.codeLocator).not.toContainText(badContent)
}
)
await _test.step('navigate to other.kcl', async () => {
await u.openFilePanel()
await otherFile.click()
await u.waitForPageLoad()
await u.openKclCodePanel()
await _expect(u.codeLocator).toContainText('getOppositeEdge(thing)')
await expect(u.codeLocator).not.toContainText(badContent)
})
await _test.step('hit redo', async () => {
// Get the original content of the file.
const originalText = await u.codeLocator.innerText()
// Now hit redo
await page.keyboard.down('Shift')
await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('KeyZ')
await page.keyboard.up('ControlOrMeta')
await page.keyboard.up('Shift')
await page.waitForTimeout(100)
await expect(u.codeLocator).toContainText(originalText)
await expect(u.codeLocator).not.toContainText(badContent)
})
}
)
}
)

View File

@ -247,7 +247,7 @@ test.describe('Can export from electron app', () => {
.poll(
async () => {
try {
const outputGltf = await fsp.readFile('output.gltf')
const outputGltf = await fsp.readFile('main.gltf')
return outputGltf.byteLength
} catch (e) {
return 0
@ -257,8 +257,8 @@ test.describe('Can export from electron app', () => {
)
.toBeGreaterThan(300_000)
// clean up output.gltf
await fsp.rm('output.gltf')
// clean up exported file
await fsp.rm('main.gltf')
})
await electronApp.close()

View File

@ -1274,3 +1274,44 @@ test2.describe('Sketch mode should be toleratant to syntax errors', () => {
}
)
})
test2.describe(`Sketching with offset planes`, () => {
test2(
`Can select an offset plane to sketch on`,
async ({ app, scene, toolbar, editor }) => {
// We seed the scene with a single offset plane
await app.initialise(`offsetPlane001 = offsetPlane("XY", 10)`)
const [planeClick, planeHover] = scene.makeMouseHelpers(650, 200)
await test2.step(`Start sketching on the offset plane`, async () => {
await toolbar.startSketchPlaneSelection()
await test2.step(`Hovering should highlight code`, async () => {
await planeHover()
await editor.expectState({
activeLines: [`offsetPlane001=offsetPlane("XY",10)`],
diagnostics: [],
highlightedCode: 'offsetPlane("XY", 10)',
})
})
await test2.step(
`Clicking should select the plane and enter sketch mode`,
async () => {
await planeClick()
// Have to wait for engine-side animation to finish
await app.page.waitForTimeout(600)
await expect2(toolbar.lineBtn).toBeEnabled()
await editor.expectEditor.toContain('startSketchOn(offsetPlane001)')
await editor.expectState({
activeLines: [`offsetPlane001=offsetPlane("XY",10)`],
diagnostics: [],
highlightedCode: '',
})
}
)
})
}
)
})

View File

@ -283,7 +283,7 @@ part001 = startSketchOn('-XZ')
const gltfFilename = filenames.filter((t: string) =>
t.includes('.gltf')
)[0]
if (!gltfFilename) throw new Error('No output.gltf in this archive')
if (!gltfFilename) throw new Error('No gLTF in this archive')
cliCommand = `export ZOO_TOKEN=${secrets.snapshottoken} && zoo file snapshot --output-format=png --src-format=${outputType} ${parentPath}/${gltfFilename} ${imagePath}`
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -1,6 +1,6 @@
{
"name": "zoo-modeling-app",
"version": "0.26.5",
"version": "0.27.0",
"private": true,
"productName": "Zoo Modeling App",
"author": {

View File

@ -47,6 +47,7 @@ import {
VariableDeclaration,
VariableDeclarator,
sketchFromKclValue,
sketchFromKclValueOptional,
} from 'lang/wasm'
import {
engineCommandManager,
@ -92,7 +93,7 @@ import {
updateCenterRectangleSketch,
} from 'lib/rectangleTool'
import { getThemeColorForThreeJs, Themes } from 'lib/theme'
import { err, reportRejection, trap } from 'lib/trap'
import { err, Reason, reportRejection, trap } from 'lib/trap'
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
import { SegmentInputs } from 'lang/std/stdTypes'
@ -1178,6 +1179,11 @@ export class SceneEntities {
await kclManager.executeAstMock(_ast)
sceneInfra.modelingSend({ type: 'Finish center rectangle' })
// lee: I had this at the bottom of the function, but it's
// possible sketchFromKclValue "fails" when sketching on a face,
// and this couldn't wouldn't run.
await codeManager.updateEditorWithAstAndWriteToFile(_ast)
const { execState } = await executeAst({
ast: _ast,
useFakeExecutor: true,
@ -1692,10 +1698,13 @@ export class SceneEntities {
this.sceneProgramMemory = programMemory
const maybeSketch = programMemory.get(variableDeclarationName)
let sketch = undefined
const sg = sketchFromKclValue(maybeSketch, variableDeclarationName)
if (!err(sg)) {
sketch = sg
let sketch: Sketch | undefined
const sk = sketchFromKclValueOptional(
maybeSketch,
variableDeclarationName
)
if (!(sk instanceof Reason)) {
sketch = sk
} else if ((maybeSketch as Solid).sketch) {
sketch = (maybeSketch as Solid).sketch
}

View File

@ -63,6 +63,7 @@ import {
import {
moveValueIntoNewVariablePath,
sketchOnExtrudedFace,
sketchOnOffsetPlane,
startSketchOnDefault,
} from 'lang/modifyAst'
import { Program, parse, recast } from 'lang/wasm'
@ -483,7 +484,7 @@ export const ModelingMachineProvider = ({
engineCommandManager.exportInfo = {
intent: ExportIntent.Save,
// This never gets used its only for make.
name: '',
name: file?.name?.replace('.kcl', `.${event.data.type}`) || '',
}
const format = {
@ -636,13 +637,16 @@ export const ModelingMachineProvider = ({
),
'animate-to-face': fromPromise(async ({ input }) => {
if (!input) return undefined
if (input.type === 'extrudeFace') {
const sketched = sketchOnExtrudedFace(
kclManager.ast,
input.sketchPathToNode,
input.extrudePathToNode,
input.faceInfo
)
if (input.type === 'extrudeFace' || input.type === 'offsetPlane') {
const sketched =
input.type === 'extrudeFace'
? sketchOnExtrudedFace(
kclManager.ast,
input.sketchPathToNode,
input.extrudePathToNode,
input.faceInfo
)
: sketchOnOffsetPlane(kclManager.ast, input.pathToNode)
if (err(sketched)) {
const sketchedError = new Error(
'Incompatible face, please try another'
@ -654,10 +658,9 @@ export const ModelingMachineProvider = ({
await kclManager.executeAstMock(modifiedAst)
await letEngineAnimateAndSyncCamAfter(
engineCommandManager,
input.faceId
)
const id =
input.type === 'extrudeFace' ? input.faceId : input.planeId
await letEngineAnimateAndSyncCamAfter(engineCommandManager, id)
sceneInfra.camControls.syncDirection = 'clientToEngine'
return {
sketchPathToNode: pathToNewSketchNode,

View File

@ -43,6 +43,7 @@ import {
completionKeymap,
} from '@codemirror/autocomplete'
import CodeEditor from './CodeEditor'
import { codeManagerHistoryCompartment } from 'lang/codeManager'
export const editorShortcutMeta = {
formatCode: {
@ -89,7 +90,7 @@ export const KclEditorPane = () => {
cursorBlinkRate: cursorBlinking.current ? 1200 : 0,
}),
lineHighlightField,
history(),
codeManagerHistoryCompartment.of(history()),
closeBrackets(),
codeFolding(),
keymap.of([
@ -121,7 +122,6 @@ export const KclEditorPane = () => {
lineNumbers(),
highlightActiveLineGutter(),
highlightSpecialChars(),
history(),
foldGutter(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),

View File

@ -5,12 +5,12 @@ import {
ProgramMemory,
Path,
ExtrudeSurface,
sketchFromKclValue,
sketchFromKclValueOptional,
} from 'lang/wasm'
import { useKclContext } from 'lang/KclProvider'
import { useResolvedTheme } from 'hooks/useResolvedTheme'
import { ActionButton } from 'components/ActionButton'
import { err, trap } from 'lib/trap'
import { Reason, trap } from 'lib/trap'
import Tooltip from 'components/Tooltip'
import { useModelingContext } from 'hooks/useModelingContext'
@ -93,13 +93,13 @@ export const processMemory = (programMemory: ProgramMemory) => {
// @ts-ignore
val.type !== 'Function'
) {
const sg = sketchFromKclValue(val, key)
const sk = sketchFromKclValueOptional(val, key)
if (val.type === 'Solid') {
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
return rest
})
} else if (!err(sg)) {
processedMemory[key] = sg.paths.map(({ __geoMeta, ...rest }: Path) => {
} else if (!(sk instanceof Reason)) {
processedMemory[key] = sk.paths.map(({ __geoMeta, ...rest }: Path) => {
return rest
})
} else {

View File

@ -88,6 +88,10 @@ export function useEngineConnectionSubscriptions() {
? [codeRef.range]
: [codeRef.range, consumedCodeRef.range]
)
} else if (artifact?.type === 'plane') {
const codeRef = artifact.codeRef
if (err(codeRef)) return
editorManager.setHighlightRange([codeRef.range])
} else {
editorManager.setHighlightRange([[0, 0]])
}
@ -186,8 +190,42 @@ export function useEngineConnectionSubscriptions() {
})
return
}
const artifact =
engineCommandManager.artifactGraph.get(planeOrFaceId)
if (artifact?.type === 'plane') {
const planeInfo = await getFaceDetails(planeOrFaceId)
sceneInfra.modelingSend({
type: 'Select default plane',
data: {
type: 'offsetPlane',
zAxis: [
planeInfo.z_axis.x,
planeInfo.z_axis.y,
planeInfo.z_axis.z,
],
yAxis: [
planeInfo.y_axis.x,
planeInfo.y_axis.y,
planeInfo.y_axis.z,
],
position: [
planeInfo.origin.x,
planeInfo.origin.y,
planeInfo.origin.z,
].map((num) => num / sceneInfra._baseUnitMultiplier) as [
number,
number,
number
],
planeId: planeOrFaceId,
pathToNode: artifact.codeRef.pathToNode,
},
})
}
// Artifact is likely an extrusion face
const faceId = planeOrFaceId
const artifact = engineCommandManager.artifactGraph.get(faceId)
const extrusion = getSweepFromSuspectedSweepSurface(
faceId,
engineCommandManager.artifactGraph

View File

@ -6,14 +6,17 @@ import { isDesktop } from 'lib/isDesktop'
import toast from 'react-hot-toast'
import { editorManager } from 'lib/singletons'
import { Annotation, Transaction } from '@codemirror/state'
import { KeyBinding } from '@codemirror/view'
import { EditorView, KeyBinding } from '@codemirror/view'
import { recast, Program } from 'lang/wasm'
import { err } from 'lib/trap'
import { Compartment } from '@codemirror/state'
import { history } from '@codemirror/commands'
const PERSIST_CODE_KEY = 'persistCode'
const codeManagerUpdateAnnotation = Annotation.define<boolean>()
export const codeManagerUpdateEvent = codeManagerUpdateAnnotation.of(true)
export const codeManagerHistoryCompartment = new Compartment()
export default class CodeManager {
private _code: string = bracket
@ -90,9 +93,12 @@ export default class CodeManager {
/**
* Update the code in the editor.
*/
updateCodeEditor(code: string): void {
updateCodeEditor(code: string, clearHistory?: boolean): void {
this.code = code
if (editorManager.editorView) {
if (clearHistory) {
clearCodeMirrorHistory(editorManager.editorView)
}
editorManager.editorView.dispatch({
changes: {
from: 0,
@ -101,7 +107,7 @@ export default class CodeManager {
},
annotations: [
codeManagerUpdateEvent,
Transaction.addToHistory.of(true),
Transaction.addToHistory.of(!clearHistory),
],
})
}
@ -110,11 +116,11 @@ export default class CodeManager {
/**
* Update the code, state, and the code the code mirror editor sees.
*/
updateCodeStateEditor(code: string): void {
updateCodeStateEditor(code: string, clearHistory?: boolean): void {
if (this._code !== code) {
this.code = code
this.#updateState(code)
this.updateCodeEditor(code)
this.updateCodeEditor(code, clearHistory)
}
}
@ -167,3 +173,17 @@ function safeLSSetItem(key: string, value: string) {
if (typeof window === 'undefined') return
localStorage?.setItem(key, value)
}
function clearCodeMirrorHistory(view: EditorView) {
// Clear history
view.dispatch({
effects: [codeManagerHistoryCompartment.reconfigure([])],
annotations: [codeManagerUpdateEvent],
})
// Add history back
view.dispatch({
effects: [codeManagerHistoryCompartment.reconfigure([history()])],
annotations: [codeManagerUpdateEvent],
})
}

View File

@ -19,6 +19,7 @@ import {
ProgramMemory,
SourceRange,
sketchFromKclValue,
isPathToNodeNumber,
} from './wasm'
import {
isNodeSafeToReplacePath,
@ -526,6 +527,60 @@ export function sketchOnExtrudedFace(
}
}
/**
* 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
* plane declaration.
*/
export function sketchOnOffsetPlane(
node: Node<Program>,
offsetPathToNode: PathToNode
) {
let _node = { ...node }
// Find the offset plane declaration
const offsetPlaneDeclarator = getNodeFromPath<VariableDeclarator>(
_node,
offsetPathToNode,
'VariableDeclarator',
true
)
if (err(offsetPlaneDeclarator)) return offsetPlaneDeclarator
const { node: offsetPlaneNode } = offsetPlaneDeclarator
const offsetPlaneName = offsetPlaneNode.id.name
// Create a new sketch declaration
const newSketchName = findUniqueName(
node,
KCL_DEFAULT_CONSTANT_PREFIXES.SKETCH
)
const newSketch = createVariableDeclaration(
newSketchName,
createCallExpressionStdLib('startSketchOn', [
createIdentifier(offsetPlaneName),
]),
undefined,
'const'
)
// Decide where to insert the new sketch declaration
const offsetIndex = offsetPathToNode[1][0]
if (!isPathToNodeNumber(offsetIndex)) {
return new Error('Expected offsetIndex to be a number')
}
// and insert it
_node.body.splice(offsetIndex + 1, 0, newSketch)
const newPathToNode = structuredClone(offsetPathToNode)
newPathToNode[1][0] = offsetIndex + 1
// Return the modified AST and the path to the new sketch declaration
return {
modifiedAst: _node,
pathToNode: newPathToNode,
}
}
export const getLastIndex = (pathToNode: PathToNode): number =>
splitPathAtLastIndex(pathToNode).index

View File

@ -77,22 +77,30 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
code.indexOf(expectedExtrudeSnippet),
code.indexOf(expectedExtrudeSnippet) + expectedExtrudeSnippet.length,
]
const expedtedExtrudePath = getNodePathFromSourceRange(ast, extrudeRange)
const expedtedExtrudeNodeResult = getNodeFromPath<VariableDeclarator>(
ast,
expedtedExtrudePath
)
if (err(expedtedExtrudeNodeResult)) {
return expedtedExtrudeNodeResult
const expectedExtrudePath = getNodePathFromSourceRange(ast, extrudeRange)
const expectedExtrudeNodeResult = getNodeFromPath<
VariableDeclarator | CallExpression
>(ast, expectedExtrudePath)
if (err(expectedExtrudeNodeResult)) {
return expectedExtrudeNodeResult
}
const expectedExtrudeNode = expedtedExtrudeNodeResult.node
const init = expectedExtrudeNode.init
if (init.type !== 'CallExpression' && init.type !== 'PipeExpression') {
return new Error(
'Expected extrude expression is not a CallExpression or PipeExpression'
)
const expectedExtrudeNode = expectedExtrudeNodeResult.node
// check whether extrude is in the sketch pipe
const extrudeInSketchPipe = expectedExtrudeNode.type === 'CallExpression'
if (extrudeInSketchPipe) {
return expectedExtrudeNode
}
return init
if (!extrudeInSketchPipe) {
const init = expectedExtrudeNode.init
if (init.type !== 'CallExpression' && init.type !== 'PipeExpression') {
return new Error(
'Expected extrude expression is not a CallExpression or PipeExpression'
)
}
return init
}
return new Error('Expected extrude expression not found')
}
// ast
@ -160,6 +168,23 @@ extrude001 = extrude(-15, sketch001)`
expectedExtrudeSnippet
)
}, 5_000)
it('should return the correct paths when extrusion occurs within the sketch pipe', async () => {
const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line([20, 0], %)
|> line([0, -20], %)
|> line([-20, 0], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
|> extrude(15, %)`
const selectedSegmentSnippet = `line([20, 0], %)`
const expectedExtrudeSnippet = `extrude(15, %)`
await runGetPathToExtrudeForSegmentSelectionTest(
code,
selectedSegmentSnippet,
expectedExtrudeSnippet
)
}, 5_000)
it('should return the correct paths for a valid selection and extrusion in case of several extrusions and sketches', async () => {
const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-30, 30], %)
@ -296,6 +321,34 @@ extrude001 = extrude(-15, sketch001)`
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(-15, sketch001)
|> fillet({ radius: 3, tags: [seg01] }, %)`
await runModifyAstCloneWithFilletAndTag(
code,
segmentSnippets,
radiusValue,
expectedCode
)
})
it('should add a fillet to the sketch pipe', async () => {
const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line([20, 0], %)
|> line([0, -20], %)
|> line([-20, 0], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
|> extrude(-15, %)`
const segmentSnippets = ['line([0, -20], %)']
const radiusValue = 3
const expectedCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, 10], %)
|> line([20, 0], %)
|> line([0, -20], %, $seg01)
|> line([-20, 0], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
|> extrude(-15, %)
|> fillet({ radius: 3, tags: [seg01] }, %)`
await runModifyAstCloneWithFilletAndTag(

View File

@ -146,7 +146,7 @@ export function modifyAstCloneWithFilletAndTag(
// Modify the extrude expression to include this fillet expression
// CallExpression - no fillet
// PipeExpression - fillet exists
// PipeExpression - fillet exists or extrude in sketch pipe
let pathToFilletNode: PathToNode = []
@ -167,15 +167,7 @@ export function modifyAstCloneWithFilletAndTag(
)
pathToFilletNodes.push(pathToFilletNode)
} else if (extrudeDeclarator.init.type === 'PipeExpression') {
// 2. case when fillet exists
const existingFilletCall = extrudeDeclarator.init.body.find((node) => {
return node.type === 'CallExpression' && node.callee.name === 'fillet'
})
if (!existingFilletCall || existingFilletCall.type !== 'CallExpression') {
return new Error('Fillet CallExpression not found.')
}
// 2. case when fillet exists or extrude in sketch pipe
// mutate the extrude node with the new fillet call
extrudeDeclarator.init.body.push(filletCall)
@ -317,14 +309,14 @@ function locateExtrudeDeclarator(
node: Program,
pathToExtrudeNode: PathToNode
): { extrudeDeclarator: VariableDeclarator } | Error {
const extrudeChunk = getNodeFromPath<VariableDeclaration>(
const nodeOfExtrudeCall = getNodeFromPath<VariableDeclaration>(
node,
pathToExtrudeNode,
'VariableDeclaration'
)
if (err(extrudeChunk)) return extrudeChunk
if (err(nodeOfExtrudeCall)) return nodeOfExtrudeCall
const { node: extrudeVarDecl } = extrudeChunk
const { node: extrudeVarDecl } = nodeOfExtrudeCall
const extrudeDeclarator = extrudeVarDecl.declarations[0]
if (!extrudeDeclarator) {
return new Error('Extrude Declarator not found.')

View File

@ -530,14 +530,25 @@ describe('Testing hasSketchPipeBeenExtruded', () => {
|> line([-17.67, 0.85], %)
|> close(%)
extrude001 = extrude(10, sketch001)
sketch002 = startSketchOn(extrude001, $seg01)
sketch002 = startSketchOn(extrude001, seg01)
|> startProfileAt([-12.94, 6.6], %)
|> line([2.45, -0.2], %)
|> line([-2, -1.25], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
sketch003 = startSketchOn(extrude001, 'END')
|> startProfileAt([8.14, 2.8], %)
|> line([-1.24, 4.39], %)
|> line([3.79, 1.91], %)
|> line([1.77, -2.95], %)
|> line([3.12, 1.74], %)
|> line([1.91, -4.09], %)
|> line([-5.6, -2.75], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
|> extrude(3.14, %)
`
it('finds sketch001 pipe to be extruded', async () => {
it('identifies sketch001 pipe as extruded (extrusion after pipe)', async () => {
const ast = parse(exampleCode)
if (err(ast)) throw ast
const lineOfInterest = `line([4.99, -0.46], %, $seg01)`
@ -552,7 +563,7 @@ sketch002 = startSketchOn(extrude001, $seg01)
)
expect(extruded).toBeTruthy()
})
it('find sketch002 NOT pipe to be extruded', async () => {
it('identifies sketch002 pipe as not extruded', async () => {
const ast = parse(exampleCode)
if (err(ast)) throw ast
const lineOfInterest = `line([2.45, -0.2], %)`
@ -567,6 +578,21 @@ sketch002 = startSketchOn(extrude001, $seg01)
)
expect(extruded).toBeFalsy()
})
it('identifies sketch003 pipe as extruded (extrusion within pipe)', async () => {
const ast = parse(exampleCode)
if (err(ast)) throw ast
const lineOfInterest = `|> line([3.12, 1.74], %)`
const characterIndex =
exampleCode.indexOf(lineOfInterest) + lineOfInterest.length
const extruded = hasSketchPipeBeenExtruded(
{
range: [characterIndex, characterIndex],
type: 'default',
},
ast
)
expect(extruded).toBeTruthy()
})
})
describe('Testing doesSceneHaveSweepableSketch', () => {

View File

@ -14,6 +14,7 @@ import {
ProgramMemory,
ReturnStatement,
sketchFromKclValue,
sketchFromKclValueOptional,
SourceRange,
SyntaxType,
VariableDeclaration,
@ -27,7 +28,7 @@ import {
getConstraintLevelFromSourceRange,
getConstraintType,
} from './std/sketchcombos'
import { err } from 'lib/trap'
import { err, Reason } from 'lib/trap'
import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement'
import { Node } from 'wasm-lib/kcl/bindings/Node'
@ -846,7 +847,8 @@ export function hasExtrudeSketch({
const varName = varDec.declarations[0].id.name
const varValue = programMemory?.get(varName)
return (
varValue?.type === 'Solid' || !err(sketchFromKclValue(varValue, varName))
varValue?.type === 'Solid' ||
!(sketchFromKclValueOptional(varValue, varName) instanceof Reason)
)
}
@ -927,7 +929,11 @@ export function findUsesOfTagInPipe(
export function hasSketchPipeBeenExtruded(selection: Selection, ast: Program) {
const path = getNodePathFromSourceRange(ast, selection.range)
const _node = getNodeFromPath<PipeExpression>(ast, path, 'PipeExpression')
const _node = getNodeFromPath<Node<PipeExpression>>(
ast,
path,
'PipeExpression'
)
if (err(_node)) return false
const { node: pipeExpression } = _node
if (pipeExpression.type !== 'PipeExpression') return false
@ -940,19 +946,33 @@ export function hasSketchPipeBeenExtruded(selection: Selection, ast: Program) {
const varDec = _varDec.node
if (varDec.type !== 'VariableDeclarator') return false
let extruded = false
traverse(ast as any, {
// option 1: extrude or revolve is called in the sketch pipe
traverse(pipeExpression, {
enter(node) {
if (
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
(node.callee.name === 'extrude' || node.callee.name === 'revolve') &&
node.arguments?.[1]?.type === 'Identifier' &&
node.arguments[1].name === varDec.id.name
(node.callee.name === 'extrude' || node.callee.name === 'revolve')
) {
extruded = true
}
},
})
// option 2: extrude or revolve is called in the separate pipe
if (!extruded) {
traverse(ast as any, {
enter(node) {
if (
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
(node.callee.name === 'extrude' || node.callee.name === 'revolve') &&
node.arguments?.[1]?.type === 'Identifier' &&
node.arguments[1].name === varDec.id.name
) {
extruded = true
}
},
})
}
return extruded
}

View File

@ -98,12 +98,22 @@ sketch004 = startSketchOn(extrude003, seg02)
|> close(%)
extrude004 = extrude(3, sketch004)
`
const exampleCodeOffsetPlanes = `
offsetPlane001 = offsetPlane("XY", 20)
offsetPlane002 = offsetPlane("XZ", -50)
offsetPlane003 = offsetPlane("YZ", 10)
sketch002 = startSketchOn(offsetPlane001)
|> startProfileAt([0, 0], %)
|> line([6.78, 15.01], %)
`
// add more code snippets here and use `getCommands` to get the orderedCommands and responseMap for more tests
const codeToWriteCacheFor = {
exampleCode1,
sketchOnFaceOnFaceEtc,
exampleCodeNo3D,
exampleCodeOffsetPlanes,
} as const
type CodeKey = keyof typeof codeToWriteCacheFor
@ -165,6 +175,52 @@ afterAll(() => {
})
describe('testing createArtifactGraph', () => {
describe('code with offset planes and a sketch:', () => {
let ast: Program
let theMap: ReturnType<typeof createArtifactGraph>
it('setup', () => {
// putting this logic in here because describe blocks runs before beforeAll has finished
const {
orderedCommands,
responseMap,
ast: _ast,
} = getCommands('exampleCodeOffsetPlanes')
ast = _ast
theMap = createArtifactGraph({ orderedCommands, responseMap, ast })
})
it(`there should be one sketch`, () => {
const sketches = [...filterArtifacts({ types: ['path'] }, theMap)].map(
(path) => expandPath(path[1], theMap)
)
expect(sketches).toHaveLength(1)
sketches.forEach((path) => {
if (err(path)) throw path
expect(path.type).toBe('path')
})
})
it(`there should be three offsetPlanes`, () => {
const offsetPlanes = [
...filterArtifacts({ types: ['plane'] }, theMap),
].map((plane) => expandPlane(plane[1], theMap))
expect(offsetPlanes).toHaveLength(3)
offsetPlanes.forEach((path) => {
expect(path.type).toBe('plane')
})
})
it(`Only one offset plane should have a path`, () => {
const offsetPlanes = [
...filterArtifacts({ types: ['plane'] }, theMap),
].map((plane) => expandPlane(plane[1], theMap))
const offsetPlaneWithPaths = offsetPlanes.filter(
(plane) => plane.paths.length
)
expect(offsetPlaneWithPaths).toHaveLength(1)
})
})
describe('code with an extrusion, fillet and sketch of face:', () => {
let ast: Program
let theMap: ReturnType<typeof createArtifactGraph>

View File

@ -249,7 +249,20 @@ export function getArtifactsToUpdate({
const cmd = command.cmd
const returnArr: ReturnType<typeof getArtifactsToUpdate> = []
if (!response) return returnArr
if (cmd.type === 'enable_sketch_mode') {
if (cmd.type === 'make_plane' && range[1] !== 0) {
// If we're calling `make_plane` and the code range doesn't end at `0`
// it's not a default plane, but a custom one from the offsetPlane standard library function
return [
{
id,
artifact: {
type: 'plane',
pathIds: [],
codeRef: { range, pathToNode },
},
},
]
} else if (cmd.type === 'enable_sketch_mode') {
const plane = getArtifact(currentPlaneId)
const pathIds = plane?.type === 'plane' ? plane?.pathIds : []
const codeRef =

View File

@ -1631,7 +1631,11 @@ export class EngineCommandManager extends EventTarget {
switch (this.exportInfo.intent) {
case ExportIntent.Save: {
exportSave(event.data, this.pendingExport.toastId).then(() => {
exportSave({
data: event.data,
fileName: this.exportInfo.name,
toastId: this.pendingExport.toastId,
}).then(() => {
this.pendingExport?.resolve(null)
}, this.pendingExport?.reject)
break

View File

@ -32,7 +32,7 @@ import { CoreDumpManager } from 'lib/coredump'
import openWindow from 'lib/openWindow'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import { TEST } from 'env'
import { err } from 'lib/trap'
import { err, Reason } from 'lib/trap'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { DeepPartial } from 'lib/types'
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
@ -144,6 +144,12 @@ export const parse = (code: string | Error): Node<Program> | Error => {
export type PathToNode = [string | number, string][]
export const isPathToNodeNumber = (
pathToNode: string | number
): pathToNode is number => {
return typeof pathToNode === 'number'
}
export interface ExecState {
memory: ProgramMemory
idGenerator: IdGenerator
@ -359,10 +365,10 @@ export class ProgramMemory {
}
// TODO: In the future, make the parameter be a KclValue.
export function sketchFromKclValue(
export function sketchFromKclValueOptional(
obj: any,
varName: string | null
): Sketch | Error {
): Sketch | Reason {
if (obj?.value?.type === 'Sketch') return obj.value
if (obj?.value?.type === 'Solid') return obj.value.sketch
if (obj?.type === 'Solid') return obj.sketch
@ -371,15 +377,26 @@ export function sketchFromKclValue(
}
const actualType = obj?.value?.type ?? obj?.type
if (actualType) {
console.log(obj)
return new Error(
return new Reason(
`Expected ${varName} to be a sketch or solid, but it was ${actualType} instead.`
)
} else {
return new Error(`Expected ${varName} to be a sketch, but it wasn't.`)
return new Reason(`Expected ${varName} to be a sketch, but it wasn't.`)
}
}
// TODO: In the future, make the parameter be a KclValue.
export function sketchFromKclValue(
obj: any,
varName: string | null
): Sketch | Error {
const result = sketchFromKclValueOptional(obj, varName)
if (result instanceof Reason) {
return result.toError()
}
return result
}
export const executor = async (
node: Node<Program>,
programMemory: ProgramMemory | Error = ProgramMemory.empty(),

View File

@ -68,7 +68,16 @@ const save_ = async (file: ModelingAppFile, toastId: string) => {
}
// Saves files locally from an export call.
export async function exportSave(data: ArrayBuffer, toastId: string) {
// We override the file's name with one passed in from the client side.
export async function exportSave({
data,
fileName,
toastId,
}: {
data: ArrayBuffer
fileName: string
toastId: string
}) {
// This converts the ArrayBuffer to a Rust equivalent Vec<u8>.
let uintArray = new Uint8Array(data)
@ -80,9 +89,10 @@ export async function exportSave(data: ArrayBuffer, toastId: string) {
zip.file(file.name, new Uint8Array(file.contents), { binary: true })
}
return zip.generateAsync({ type: 'array' }).then((contents) => {
return save_({ name: 'output.zip', contents }, toastId)
return save_({ name: `${fileName || 'output'}.zip`, contents }, toastId)
})
} else {
files[0].name = fileName || files[0].name
return save_(files[0], toastId)
}
}

View File

@ -124,7 +124,9 @@ export const fileLoader: LoaderFunction = async (
// We explicitly do not write to the file here since we are loading from
// the file system and not the editor.
codeManager.updateCurrentFilePath(currentFilePath)
codeManager.updateCodeStateEditor(code)
// We pass true on the end here to clear the code editor history.
// This way undo and redo are not super weird when opening new files.
codeManager.updateCodeStateEditor(code, true)
}
// Set the file system manager to the project path

View File

@ -2,6 +2,23 @@ import toast from 'react-hot-toast'
type ExcludeErr<T> = Exclude<T, Error>
/**
* Similar to Error, but more lightweight, without the stack trace. It can also
* be used to represent a reason for not being able to provide an alternative,
* which isn't necessarily an error.
*/
export class Reason {
message: string
constructor(message: string) {
this.message = message
}
toError() {
return new Error(this.message)
}
}
/**
* This is intentionally *not* exported due to misuse. We'd like to add a lint.
*/

View File

@ -159,6 +159,15 @@ export type DefaultPlane = {
yAxis: [number, number, number]
}
export type OffsetPlane = {
type: 'offsetPlane'
position: [number, number, number]
planeId: string
pathToNode: PathToNode
zAxis: [number, number, number]
yAxis: [number, number, number]
}
export type SegmentOverlayPayload =
| {
type: 'set-one'
@ -198,7 +207,7 @@ export type ModelingMachineEvent =
| { type: 'Sketch On Face' }
| {
type: 'Select default plane'
data: DefaultPlane | ExtrudeFacePlane
data: DefaultPlane | ExtrudeFacePlane | OffsetPlane
}
| {
type: 'Set selection'
@ -1394,7 +1403,7 @@ export const modelingMachine = setup({
}
),
'animate-to-face': fromPromise(
async (_: { input?: ExtrudeFacePlane | DefaultPlane }) => {
async (_: { input?: ExtrudeFacePlane | DefaultPlane | OffsetPlane }) => {
return {} as
| undefined
| {

View File

@ -1589,6 +1589,8 @@ dependencies = [
"console",
"lazy_static",
"linked-hash-map",
"pest",
"pest_derive",
"regex",
"serde",
"similar",
@ -1689,6 +1691,7 @@ dependencies = [
"databake",
"derive-docs",
"expectorate",
"fnv",
"form_urlencoded",
"futures",
"git_rev",
@ -1734,18 +1737,6 @@ dependencies = [
"zip",
]
[[package]]
name = "kcl-macros"
version = "0.1.0"
dependencies = [
"databake",
"kcl-lib",
"pretty_assertions",
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "kcl-test-server"
version = "0.1.16"

View File

@ -68,7 +68,6 @@ debug = "line-tables-only"
members = [
"derive-docs",
"kcl",
"kcl-macros",
"kcl-test-server",
"kcl-to-core",
]

View File

@ -173,11 +173,11 @@ fn do_stdlib_inner(
quote! {
let code_blocks = vec![#(#cb),*];
code_blocks.iter().map(|cb| {
let program = crate::parser::top_level_parse(cb).unwrap();
let program = crate::Program::parse(cb).unwrap();
let mut options: crate::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.recast(&options, 0)
program.ast.recast(&options, 0)
}).collect::<Vec<String>>()
}
} else {
@ -748,8 +748,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr
quote! {
#[tokio::test(flavor = "multi_thread")]
async fn #test_name_mock() {
let program = crate::parser::top_level_parse(#code_block).unwrap();
let id_generator = crate::executor::IdGenerator::default();
let program = crate::Program::parse(#code_block).unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await.unwrap())),
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
@ -758,7 +757,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, &mut crate::ExecState::default()).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -2,8 +2,7 @@
mod test_examples_someFn {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_someFn0() {
let program = crate::parser::top_level_parse("someFn()").unwrap();
let id_generator = crate::executor::IdGenerator::default();
let program = crate::Program::parse("someFn()").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -15,7 +14,9 @@ mod test_examples_someFn {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, &mut crate::ExecState::default())
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -111,10 +112,10 @@ impl crate::docs::StdLibFn for SomeFn {
code_blocks
.iter()
.map(|cb| {
let program = crate::parser::top_level_parse(cb).unwrap();
let program = crate::Program::parse(cb).unwrap();
let mut options: crate::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.recast(&options, 0)
program.ast.recast(&options, 0)
})
.collect::<Vec<String>>()
}

View File

@ -2,8 +2,7 @@
mod test_examples_someFn {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_someFn0() {
let program = crate::parser::top_level_parse("someFn()").unwrap();
let id_generator = crate::executor::IdGenerator::default();
let program = crate::Program::parse("someFn()").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -15,7 +14,9 @@ mod test_examples_someFn {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, &mut crate::ExecState::default())
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -111,10 +112,10 @@ impl crate::docs::StdLibFn for SomeFn {
code_blocks
.iter()
.map(|cb| {
let program = crate::parser::top_level_parse(cb).unwrap();
let program = crate::Program::parse(cb).unwrap();
let mut options: crate::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.recast(&options, 0)
program.ast.recast(&options, 0)
})
.collect::<Vec<String>>()
}

View File

@ -3,9 +3,7 @@ mod test_examples_show {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_show0() {
let program =
crate::parser::top_level_parse("This is another code block.\nyes sirrr.\nshow")
.unwrap();
let id_generator = crate::executor::IdGenerator::default();
crate::Program::parse("This is another code block.\nyes sirrr.\nshow").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -17,7 +15,9 @@ mod test_examples_show {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, &mut crate::ExecState::default())
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -36,9 +36,7 @@ mod test_examples_show {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_show1() {
let program =
crate::parser::top_level_parse("This is code.\nIt does other shit.\nshow").unwrap();
let id_generator = crate::executor::IdGenerator::default();
let program = crate::Program::parse("This is code.\nIt does other shit.\nshow").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -50,7 +48,9 @@ mod test_examples_show {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, &mut crate::ExecState::default())
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -149,10 +149,10 @@ impl crate::docs::StdLibFn for Show {
code_blocks
.iter()
.map(|cb| {
let program = crate::parser::top_level_parse(cb).unwrap();
let program = crate::Program::parse(cb).unwrap();
let mut options: crate::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.recast(&options, 0)
program.ast.recast(&options, 0)
})
.collect::<Vec<String>>()
}

View File

@ -2,9 +2,7 @@
mod test_examples_show {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_show0() {
let program =
crate::parser::top_level_parse("This is code.\nIt does other shit.\nshow").unwrap();
let id_generator = crate::executor::IdGenerator::default();
let program = crate::Program::parse("This is code.\nIt does other shit.\nshow").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -16,7 +14,9 @@ mod test_examples_show {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, &mut crate::ExecState::default())
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -112,10 +112,10 @@ impl crate::docs::StdLibFn for Show {
code_blocks
.iter()
.map(|cb| {
let program = crate::parser::top_level_parse(cb).unwrap();
let program = crate::Program::parse(cb).unwrap();
let mut options: crate::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.recast(&options, 0)
program.ast.recast(&options, 0)
})
.collect::<Vec<String>>()
}

View File

@ -3,9 +3,7 @@ mod test_examples_my_func {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_my_func0() {
let program =
crate::parser::top_level_parse("This is another code block.\nyes sirrr.\nmyFunc")
.unwrap();
let id_generator = crate::executor::IdGenerator::default();
crate::Program::parse("This is another code block.\nyes sirrr.\nmyFunc").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -17,7 +15,9 @@ mod test_examples_my_func {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, &mut crate::ExecState::default())
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -36,9 +36,7 @@ mod test_examples_my_func {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_my_func1() {
let program =
crate::parser::top_level_parse("This is code.\nIt does other shit.\nmyFunc").unwrap();
let id_generator = crate::executor::IdGenerator::default();
let program = crate::Program::parse("This is code.\nIt does other shit.\nmyFunc").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -50,7 +48,9 @@ mod test_examples_my_func {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, &mut crate::ExecState::default())
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -149,10 +149,10 @@ impl crate::docs::StdLibFn for MyFunc {
code_blocks
.iter()
.map(|cb| {
let program = crate::parser::top_level_parse(cb).unwrap();
let program = crate::Program::parse(cb).unwrap();
let mut options: crate::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.recast(&options, 0)
program.ast.recast(&options, 0)
})
.collect::<Vec<String>>()
}

View File

@ -3,9 +3,7 @@ mod test_examples_line_to {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_line_to0() {
let program =
crate::parser::top_level_parse("This is another code block.\nyes sirrr.\nlineTo")
.unwrap();
let id_generator = crate::executor::IdGenerator::default();
crate::Program::parse("This is another code block.\nyes sirrr.\nlineTo").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -17,7 +15,9 @@ mod test_examples_line_to {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, &mut crate::ExecState::default())
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -36,9 +36,7 @@ mod test_examples_line_to {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_line_to1() {
let program =
crate::parser::top_level_parse("This is code.\nIt does other shit.\nlineTo").unwrap();
let id_generator = crate::executor::IdGenerator::default();
let program = crate::Program::parse("This is code.\nIt does other shit.\nlineTo").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -50,7 +48,9 @@ mod test_examples_line_to {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, &mut crate::ExecState::default())
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -157,10 +157,10 @@ impl crate::docs::StdLibFn for LineTo {
code_blocks
.iter()
.map(|cb| {
let program = crate::parser::top_level_parse(cb).unwrap();
let program = crate::Program::parse(cb).unwrap();
let mut options: crate::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.recast(&options, 0)
program.ast.recast(&options, 0)
})
.collect::<Vec<String>>()
}

View File

@ -3,8 +3,7 @@ mod test_examples_min {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_min0() {
let program =
crate::parser::top_level_parse("This is another code block.\nyes sirrr.\nmin").unwrap();
let id_generator = crate::executor::IdGenerator::default();
crate::Program::parse("This is another code block.\nyes sirrr.\nmin").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -16,7 +15,9 @@ mod test_examples_min {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, &mut crate::ExecState::default())
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -35,9 +36,7 @@ mod test_examples_min {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_min1() {
let program =
crate::parser::top_level_parse("This is code.\nIt does other shit.\nmin").unwrap();
let id_generator = crate::executor::IdGenerator::default();
let program = crate::Program::parse("This is code.\nIt does other shit.\nmin").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -49,7 +48,9 @@ mod test_examples_min {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, &mut crate::ExecState::default())
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -148,10 +149,10 @@ impl crate::docs::StdLibFn for Min {
code_blocks
.iter()
.map(|cb| {
let program = crate::parser::top_level_parse(cb).unwrap();
let program = crate::Program::parse(cb).unwrap();
let mut options: crate::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.recast(&options, 0)
program.ast.recast(&options, 0)
})
.collect::<Vec<String>>()
}

View File

@ -2,9 +2,7 @@
mod test_examples_show {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_show0() {
let program =
crate::parser::top_level_parse("This is code.\nIt does other shit.\nshow").unwrap();
let id_generator = crate::executor::IdGenerator::default();
let program = crate::Program::parse("This is code.\nIt does other shit.\nshow").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -16,7 +14,9 @@ mod test_examples_show {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, &mut crate::ExecState::default())
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -112,10 +112,10 @@ impl crate::docs::StdLibFn for Show {
code_blocks
.iter()
.map(|cb| {
let program = crate::parser::top_level_parse(cb).unwrap();
let program = crate::Program::parse(cb).unwrap();
let mut options: crate::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.recast(&options, 0)
program.ast.recast(&options, 0)
})
.collect::<Vec<String>>()
}

View File

@ -2,9 +2,7 @@
mod test_examples_import {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_import0() {
let program =
crate::parser::top_level_parse("This is code.\nIt does other shit.\nimport").unwrap();
let id_generator = crate::executor::IdGenerator::default();
let program = crate::Program::parse("This is code.\nIt does other shit.\nimport").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -16,7 +14,9 @@ mod test_examples_import {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, &mut crate::ExecState::default())
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -112,10 +112,10 @@ impl crate::docs::StdLibFn for Import {
code_blocks
.iter()
.map(|cb| {
let program = crate::parser::top_level_parse(cb).unwrap();
let program = crate::Program::parse(cb).unwrap();
let mut options: crate::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.recast(&options, 0)
program.ast.recast(&options, 0)
})
.collect::<Vec<String>>()
}

View File

@ -2,9 +2,7 @@
mod test_examples_import {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_import0() {
let program =
crate::parser::top_level_parse("This is code.\nIt does other shit.\nimport").unwrap();
let id_generator = crate::executor::IdGenerator::default();
let program = crate::Program::parse("This is code.\nIt does other shit.\nimport").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -16,7 +14,9 @@ mod test_examples_import {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, &mut crate::ExecState::default())
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -112,10 +112,10 @@ impl crate::docs::StdLibFn for Import {
code_blocks
.iter()
.map(|cb| {
let program = crate::parser::top_level_parse(cb).unwrap();
let program = crate::Program::parse(cb).unwrap();
let mut options: crate::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.recast(&options, 0)
program.ast.recast(&options, 0)
})
.collect::<Vec<String>>()
}

View File

@ -2,9 +2,7 @@
mod test_examples_import {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_import0() {
let program =
crate::parser::top_level_parse("This is code.\nIt does other shit.\nimport").unwrap();
let id_generator = crate::executor::IdGenerator::default();
let program = crate::Program::parse("This is code.\nIt does other shit.\nimport").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -16,7 +14,9 @@ mod test_examples_import {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, &mut crate::ExecState::default())
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -112,10 +112,10 @@ impl crate::docs::StdLibFn for Import {
code_blocks
.iter()
.map(|cb| {
let program = crate::parser::top_level_parse(cb).unwrap();
let program = crate::Program::parse(cb).unwrap();
let mut options: crate::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.recast(&options, 0)
program.ast.recast(&options, 0)
})
.collect::<Vec<String>>()
}

View File

@ -2,9 +2,7 @@
mod test_examples_show {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_show0() {
let program =
crate::parser::top_level_parse("This is code.\nIt does other shit.\nshow").unwrap();
let id_generator = crate::executor::IdGenerator::default();
let program = crate::Program::parse("This is code.\nIt does other shit.\nshow").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -16,7 +14,9 @@ mod test_examples_show {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, &mut crate::ExecState::default())
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -112,10 +112,10 @@ impl crate::docs::StdLibFn for Show {
code_blocks
.iter()
.map(|cb| {
let program = crate::parser::top_level_parse(cb).unwrap();
let program = crate::Program::parse(cb).unwrap();
let mut options: crate::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.recast(&options, 0)
program.ast.recast(&options, 0)
})
.collect::<Vec<String>>()
}

View File

@ -2,8 +2,7 @@
mod test_examples_some_function {
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_example_some_function0() {
let program = crate::parser::top_level_parse("someFunction()").unwrap();
let id_generator = crate::executor::IdGenerator::default();
let program = crate::Program::parse("someFunction()").unwrap();
let ctx = crate::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -15,7 +14,9 @@ mod test_examples_some_function {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator, None).await.unwrap();
ctx.run(&program, &mut crate::ExecState::default())
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -106,10 +107,10 @@ impl crate::docs::StdLibFn for SomeFunction {
code_blocks
.iter()
.map(|cb| {
let program = crate::parser::top_level_parse(cb).unwrap();
let program = crate::Program::parse(cb).unwrap();
let mut options: crate::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
program.recast(&options, 0)
program.ast.recast(&options, 0)
})
.collect::<Vec<String>>()
}

View File

@ -1,29 +1,20 @@
cnr := "cargo nextest run"
cita := "cargo insta test --accept"
# Create a new KCL snapshot test from `tests/inputs/my-test.kcl`.
new-test name:
echo "kcl_test!(\"{{name}}\", {{name}});" >> tests/executor/visuals.rs
TWENTY_TWENTY=overwrite {{cnr}} --test executor -E 'test(=visuals::{{name}})'
# Run the same lint checks we run in CI.
lint:
cargo clippy --workspace --all-targets -- -D warnings
# Generate the stdlib image artifacts
# Then run the stdlib docs generation
redo-kcl-stdlib-docs:
TWENTY_TWENTY=overwrite {{cnr}} -p kcl-lib kcl_test_example
EXPECTORATE=overwrite {{cnr}} -p kcl-lib docs::gen_std_tests::test_generate_stdlib
# Create a new KCL deterministic simulation test case.
new-sim-test test_name kcl_program render_to_png="true":
# Each test file gets its own directory. This will contain the KCL program, and its
# snapshotted artifacts (e.g. serialized tokens, serialized ASTs, program memory,
# PNG snapshots, etc).
mkdir kcl/tests/{{test_name}}
echo "{{kcl_program}}" > kcl/tests/{{test_name}}/input.kcl
# Add the various tests for this new test case.
cat kcl/tests/simtest.tmpl | sed "s/TEST_NAME_HERE/{{test_name}}/" | sed "s/RENDER_TO_PNG/{{render_to_png}}/" >> kcl/src/simulation_tests.rs
# Run all the tests for the first time, in the right order.
new-sim-test test_name render_to_png="true":
{{cita}} -p kcl-lib -- tests::{{test_name}}::tokenize
{{cita}} -p kcl-lib -- tests::{{test_name}}::parse
{{cita}} -p kcl-lib -- tests::{{test_name}}::unparse
TWENTY_TWENTY=overwrite {{cita}} -p kcl-lib -- tests::{{test_name}}::kcl_test_execute

View File

@ -1,21 +0,0 @@
[package]
name = "kcl-macros"
description = "Macro for compiling KCL to its AST during Rust compile-time"
version = "0.1.0"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
proc-macro = true
[dependencies]
databake = "0.1.8"
kcl-lib = { path = "../kcl" }
proc-macro2 = "1"
quote = "1"
syn = { version = "2.0.87", features = ["full"] }
[dev-dependencies]
pretty_assertions = "1.4.1"

View File

@ -1,22 +0,0 @@
//! This crate contains macros for parsing KCL at Rust compile-time.
use databake::*;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, LitStr};
/// Parses KCL into its AST at compile-time.
/// This macro takes exactly one argument: A string literal containing KCL.
/// # Examples
/// ```
/// extern crate alloc;
/// use kcl_compile_macro::parse_kcl;
/// let ast: kcl_lib::ast::types::Program = parse_kcl!("const y = 4");
/// ```
#[proc_macro]
pub fn parse(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as LitStr);
let kcl_src = input.value();
let ast = kcl_lib::parser::top_level_parse(&kcl_src).unwrap();
let ast_struct = ast.bake(&Default::default());
quote!(#ast_struct).into()
}

View File

@ -1,60 +0,0 @@
extern crate alloc;
use kcl_lib::ast::types::{
BodyItem, Expr, Identifier, ItemVisibility, Literal, LiteralValue, ModuleId, Node, Program, VariableDeclaration,
VariableDeclarator, VariableKind,
};
use kcl_macros::parse;
use pretty_assertions::assert_eq;
#[test]
fn basic() {
let actual = parse!("const y = 4");
let module_id = ModuleId::default();
let expected = Node {
inner: Program {
body: vec![BodyItem::VariableDeclaration(Box::new(Node::new(
VariableDeclaration {
declarations: vec![Node::new(
VariableDeclarator {
id: Node::new(
Identifier {
name: "y".to_owned(),
digest: None,
},
6,
7,
module_id,
),
init: Expr::Literal(Box::new(Node::new(
Literal {
value: LiteralValue::IInteger(4),
raw: "4".to_owned(),
digest: None,
},
10,
11,
module_id,
))),
digest: None,
},
6,
11,
module_id,
)],
visibility: ItemVisibility::Default,
kind: VariableKind::Const,
digest: None,
},
0,
11,
module_id,
)))],
non_code_meta: Default::default(),
digest: None,
},
start: 0,
end: 11,
module_id,
};
assert_eq!(expected, actual);
}

View File

@ -15,7 +15,7 @@ use hyper::{
service::{make_service_fn, service_fn},
Body, Error, Response, Server,
};
use kcl_lib::{ast::types::ModuleId, executor::ExecutorContext, settings::types::UnitLength, test_server::RequestBody};
use kcl_lib::{test_server::RequestBody, ExecState, ExecutorContext, Program, UnitLength};
use tokio::{
sync::{mpsc, oneshot},
task::JoinHandle,
@ -157,21 +157,18 @@ async fn snapshot_endpoint(body: Bytes, state: ExecutorContext) -> Response<Body
Err(e) => return bad_request(format!("Invalid request JSON: {e}")),
};
let RequestBody { kcl_program, test_name } = body;
let module_id = ModuleId::default();
let parser = match kcl_lib::token::lexer(&kcl_program, module_id) {
Ok(ts) => kcl_lib::parser::Parser::new(ts),
Err(e) => return bad_request(format!("tokenization error: {e}")),
};
let program = match parser.ast() {
let program = match Program::parse(&kcl_program) {
Ok(pr) => pr,
Err(e) => return bad_request(format!("Parse error: {e}")),
};
eprintln!("Executing {test_name}");
let mut id_generator = kcl_lib::executor::IdGenerator::default();
let mut exec_state = ExecState::default();
// This is a shitty source range, I don't know what else to use for it though.
// There's no actual KCL associated with this reset_scene call.
if let Err(e) = state
.reset_scene(&mut id_generator, kcl_lib::executor::SourceRange::default())
.reset_scene(&mut exec_state, kcl_lib::SourceRange::default())
.await
{
return kcl_err(e);
@ -179,7 +176,7 @@ async fn snapshot_endpoint(body: Bytes, state: ExecutorContext) -> Response<Body
// Let users know if the test is taking a long time.
let (done_tx, done_rx) = oneshot::channel::<()>();
let timer = time_until(done_rx);
let snapshot = match state.execute_and_prepare_snapshot(&program, id_generator, None).await {
let snapshot = match state.execute_and_prepare_snapshot(&program, &mut exec_state).await {
Ok(sn) => sn,
Err(e) => return kcl_err(e),
};

View File

@ -1,9 +1,8 @@
use anyhow::Result;
use indexmap::IndexMap;
use kcl_lib::{
engine::ExecutionKind,
errors::KclError,
executor::{DefaultPlanes, IdGenerator},
exec::{DefaultPlanes, IdGenerator},
ExecutionKind, KclError,
};
use kittycad_modeling_cmds::{
self as kcmc,
@ -23,8 +22,8 @@ const NEED_PLANES: bool = true;
#[derive(Debug, Clone)]
pub struct EngineConnection {
batch: Arc<Mutex<Vec<(WebSocketRequest, kcl_lib::executor::SourceRange)>>>,
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, kcl_lib::executor::SourceRange)>>>,
batch: Arc<Mutex<Vec<(WebSocketRequest, kcl_lib::SourceRange)>>>,
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, kcl_lib::SourceRange)>>>,
core_test: Arc<Mutex<String>>,
default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
execution_kind: Arc<Mutex<ExecutionKind>>,
@ -354,12 +353,12 @@ fn codegen_cpp_repl_uuid_setters(reps_id: &str, entity_ids: &[uuid::Uuid]) -> St
}
#[async_trait::async_trait]
impl kcl_lib::engine::EngineManager for EngineConnection {
fn batch(&self) -> Arc<Mutex<Vec<(WebSocketRequest, kcl_lib::executor::SourceRange)>>> {
impl kcl_lib::EngineManager for EngineConnection {
fn batch(&self) -> Arc<Mutex<Vec<(WebSocketRequest, kcl_lib::SourceRange)>>> {
self.batch.clone()
}
fn batch_end(&self) -> Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, kcl_lib::executor::SourceRange)>>> {
fn batch_end(&self) -> Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, kcl_lib::SourceRange)>>> {
self.batch_end.clone()
}
@ -378,7 +377,7 @@ impl kcl_lib::engine::EngineManager for EngineConnection {
async fn default_planes(
&self,
id_generator: &mut IdGenerator,
source_range: kcl_lib::executor::SourceRange,
source_range: kcl_lib::SourceRange,
) -> Result<DefaultPlanes, KclError> {
if NEED_PLANES {
{
@ -400,7 +399,7 @@ impl kcl_lib::engine::EngineManager for EngineConnection {
async fn clear_scene_post_hook(
&self,
_id_generator: &mut IdGenerator,
_source_range: kcl_lib::executor::SourceRange,
_source_range: kcl_lib::SourceRange,
) -> Result<(), KclError> {
Ok(())
}
@ -408,9 +407,9 @@ impl kcl_lib::engine::EngineManager for EngineConnection {
async fn inner_send_modeling_cmd(
&self,
id: uuid::Uuid,
_source_range: kcl_lib::executor::SourceRange,
_source_range: kcl_lib::SourceRange,
cmd: WebSocketRequest,
_id_to_source_range: std::collections::HashMap<uuid::Uuid, kcl_lib::executor::SourceRange>,
_id_to_source_range: std::collections::HashMap<uuid::Uuid, kcl_lib::SourceRange>,
) -> Result<WebSocketResponse, KclError> {
match cmd {
WebSocketRequest::ModelingCmdBatchReq(ModelingBatch {

View File

@ -1,5 +1,5 @@
use anyhow::Result;
use kcl_lib::executor::{ExecutorContext, IdGenerator};
use kcl_lib::{ExecState, ExecutorContext};
use std::sync::{Arc, Mutex};
#[cfg(not(target_arch = "wasm32"))]
@ -7,21 +7,15 @@ mod conn_mock_core;
///Converts the given kcl code to an engine test
pub async fn kcl_to_engine_core(code: &str) -> Result<String> {
let program = kcl_lib::parser::top_level_parse(code)?;
let program = kcl_lib::Program::parse(code)?;
let result = Arc::new(Mutex::new("".into()));
let ref_result = Arc::clone(&result);
let ctx = ExecutorContext {
engine: Arc::new(Box::new(
crate::conn_mock_core::EngineConnection::new(ref_result).await?,
)),
fs: Arc::new(kcl_lib::fs::FileManager::new()),
stdlib: Arc::new(kcl_lib::std::StdLib::new()),
settings: Default::default(),
context_type: kcl_lib::executor::ContextType::MockCustomForwarded,
};
let _memory = ctx.run(&program, None, IdGenerator::default(), None).await?;
let ctx = ExecutorContext::new_forwarded_mock(Arc::new(Box::new(
crate::conn_mock_core::EngineConnection::new(ref_result).await?,
)));
ctx.run(&program, &mut ExecState::default()).await?;
let result = result.lock().expect("mutex lock").clone();
Ok(result)

View File

@ -21,6 +21,7 @@ convert_case = "0.6.0"
dashmap = "6.1.0"
databake = { version = "0.1.8", features = ["derive"] }
derive-docs = { version = "0.1.29", path = "../derive-docs" }
fnv = "1.0.7"
form_urlencoded = "1.2.1"
futures = { version = "0.3.31" }
git_rev = "0.1.0"
@ -86,7 +87,7 @@ expectorate = "1.1.0"
handlebars = "6.2.0"
iai = "0.1"
image = { version = "0.25.5", default-features = false, features = ["png"] }
insta = { version = "1.41.1", features = ["json", "filters"] }
insta = { version = "1.41.1", features = ["json", "filters", "redactions"] }
itertools = "0.13.0"
pretty_assertions = "1.4.1"
tokio = { version = "1.40.0", features = ["rt-multi-thread", "macros", "time"] }

View File

@ -1,12 +1,5 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
pub fn bench_lex(c: &mut Criterion) {
let module_id = kcl_lib::ast::types::ModuleId::default();
c.bench_function("lex_cube", |b| b.iter(|| lex(CUBE_PROGRAM, module_id)));
c.bench_function("lex_big_kitt", |b| b.iter(|| lex(KITT_PROGRAM, module_id)));
c.bench_function("lex_pipes_on_pipes", |b| b.iter(|| lex(PIPES_PROGRAM, module_id)));
}
pub fn bench_parse(c: &mut Criterion) {
for (name, file) in [
("pipes_on_pipes", PIPES_PROGRAM),
@ -16,28 +9,20 @@ pub fn bench_parse(c: &mut Criterion) {
("mike_stress_test", MIKE_STRESS_TEST_PROGRAM),
("koch snowflake", LSYSTEM_KOCH_SNOWFLAKE_PROGRAM),
] {
let module_id = kcl_lib::ast::types::ModuleId::default();
let tokens = kcl_lib::token::lexer(file, module_id).unwrap();
c.bench_function(&format!("parse_{name}"), move |b| {
let tok = tokens.clone();
b.iter(move || {
let parser = kcl_lib::parser::Parser::new(tok.clone());
black_box(parser.ast().unwrap());
black_box(kcl_lib::Program::parse(file).unwrap());
})
});
}
}
fn lex(program: &str, module_id: kcl_lib::ast::types::ModuleId) {
black_box(kcl_lib::token::lexer(program, module_id).unwrap());
}
criterion_group!(benches, bench_lex, bench_parse);
criterion_group!(benches, bench_parse);
criterion_main!(benches);
const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl");
const PIPES_PROGRAM: &str = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
const MATH_PROGRAM: &str = include_str!("../../tests/executor/inputs/math.kcl");
const MIKE_STRESS_TEST_PROGRAM: &str = include_str!("../../tests/executor/inputs/mike_stress_test.kcl");
const MIKE_STRESS_TEST_PROGRAM: &str = include_str!("../tests/mike_stress_test/input.kcl");
const LSYSTEM_KOCH_SNOWFLAKE_PROGRAM: &str = include_str!("../../tests/executor/inputs/lsystem.kcl");

View File

@ -1,32 +1,7 @@
use iai::black_box;
pub fn parse(program: &str) {
let module_id = kcl_lib::ast::types::ModuleId::default();
let tokens = kcl_lib::token::lexer(program, module_id).unwrap();
let tok = tokens.clone();
let parser = kcl_lib::parser::Parser::new(tok.clone());
black_box(parser.ast().unwrap());
}
fn lex_kitt() {
let module_id = kcl_lib::ast::types::ModuleId::default();
black_box(kcl_lib::token::lexer(KITT_PROGRAM, module_id).unwrap());
}
fn lex_pipes() {
let module_id = kcl_lib::ast::types::ModuleId::default();
black_box(kcl_lib::token::lexer(PIPES_PROGRAM, module_id).unwrap());
}
fn lex_cube() {
let module_id = kcl_lib::ast::types::ModuleId::default();
black_box(kcl_lib::token::lexer(CUBE_PROGRAM, module_id).unwrap());
}
fn lex_math() {
let module_id = kcl_lib::ast::types::ModuleId::default();
black_box(kcl_lib::token::lexer(MATH_PROGRAM, module_id).unwrap());
}
fn lex_lsystem() {
let module_id = kcl_lib::ast::types::ModuleId::default();
black_box(kcl_lib::token::lexer(LSYSTEM_PROGRAM, module_id).unwrap());
black_box(kcl_lib::Program::parse(program).unwrap());
}
fn parse_kitt() {
@ -46,11 +21,6 @@ fn parse_lsystem() {
}
iai::main! {
lex_kitt,
lex_pipes,
lex_cube,
lex_math,
lex_lsystem,
parse_kitt,
parse_pipes,
parse_cube,

View File

@ -9,7 +9,7 @@ pub fn bench_digest(c: &mut Criterion) {
("mike_stress_test", MIKE_STRESS_TEST_PROGRAM),
("lsystem", LSYSTEM_PROGRAM),
] {
let prog = kcl_lib::parser::top_level_parse(file).unwrap();
let prog = kcl_lib::Program::parse(file).unwrap();
c.bench_function(&format!("digest_{name}"), move |b| {
let prog = prog.clone();
@ -28,5 +28,5 @@ const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_sv
const PIPES_PROGRAM: &str = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
const MATH_PROGRAM: &str = include_str!("../../tests/executor/inputs/math.kcl");
const MIKE_STRESS_TEST_PROGRAM: &str = include_str!("../../tests/executor/inputs/mike_stress_test.kcl");
const MIKE_STRESS_TEST_PROGRAM: &str = include_str!("../tests/mike_stress_test/input.kcl");
const LSYSTEM_PROGRAM: &str = include_str!("../../tests/executor/inputs/lsystem.kcl");

View File

@ -1,5 +1,5 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use kcl_lib::{settings::types::UnitLength::Mm, test_server};
use kcl_lib::{test_server, UnitLength::Mm};
use tokio::runtime::Runtime;
pub fn bench_execute(c: &mut Criterion) {

View File

@ -3,7 +3,7 @@ use iai::black_box;
async fn execute_server_rack_heavy() {
let code = SERVER_RACK_HEAVY_PROGRAM;
black_box(
kcl_lib::test_server::execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
kcl_lib::test_server::execute_and_snapshot(code, kcl_lib::UnitLength::Mm)
.await
.unwrap(),
);
@ -12,7 +12,7 @@ async fn execute_server_rack_heavy() {
async fn execute_server_rack_lite() {
let code = SERVER_RACK_LITE_PROGRAM;
black_box(
kcl_lib::test_server::execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
kcl_lib::test_server::execute_and_snapshot(code, kcl_lib::UnitLength::Mm)
.await
.unwrap(),
);

View File

@ -1,5 +1,5 @@
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
use kcl_lib::lsp::test_util::kcl_lsp_server;
use kcl_lib::kcl_lsp_server;
use tokio::runtime::Runtime;
use tower_lsp::LanguageServer;
@ -62,6 +62,6 @@ const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_sv
const PIPES_PROGRAM: &str = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
const MATH_PROGRAM: &str = include_str!("../../tests/executor/inputs/math.kcl");
const MIKE_STRESS_TEST_PROGRAM: &str = include_str!("../../tests/executor/inputs/mike_stress_test.kcl");
const MIKE_STRESS_TEST_PROGRAM: &str = include_str!("../tests/mike_stress_test/input.kcl");
const GLOBAL_TAGS_FILE: &str = include_str!("../../tests/executor/inputs/global-tags.kcl");
const LSYSTEM_PROGRAM: &str = include_str!("../../tests/executor/inputs/lsystem.kcl");

View File

@ -1,5 +1,5 @@
use iai::black_box;
use kcl_lib::lsp::test_util::kcl_lsp_server;
use kcl_lib::kcl_lsp_server;
use tower_lsp::LanguageServer;
async fn kcl_lsp_semantic_tokens(code: &str) {

View File

@ -9,11 +9,12 @@ use kittycad_modeling_cmds as kcmc;
use crate::{
ast::types::{
ArrayExpression, CallExpression, ConstraintLevel, FormatOptions, Literal, PipeExpression, PipeSubstitution,
Program, VariableDeclarator,
VariableDeclarator,
},
engine::EngineManager,
errors::{KclError, KclErrorDetails},
executor::{Point2d, SourceRange},
Program,
};
use super::types::{ModuleId, Node};
@ -22,13 +23,13 @@ type Point3d = kcmc::shared::Point3d<f64>;
#[derive(Debug)]
/// The control point data for a curve or line.
pub struct ControlPointData {
struct ControlPointData {
/// The control points for the curve or line.
pub points: Vec<Point3d>,
points: Vec<Point3d>,
/// The command that created this curve or line.
pub command: PathCommand,
_command: PathCommand,
/// The id of the curve or line.
pub id: uuid::Uuid,
_id: uuid::Uuid,
}
const EPSILON: f64 = 0.015625; // or 2^-6
@ -37,7 +38,7 @@ const EPSILON: f64 = 0.015625; // or 2^-6
/// a move or a new line.
pub async fn modify_ast_for_sketch(
engine: &Arc<Box<dyn EngineManager>>,
program: &mut Node<Program>,
program: &mut Program,
module_id: ModuleId,
// The name of the sketch.
sketch_name: &str,
@ -50,7 +51,7 @@ pub async fn modify_ast_for_sketch(
// If it is, we cannot modify it.
// Get the information about the sketch.
if let Some(ast_sketch) = program.get_variable(sketch_name) {
if let Some(ast_sketch) = program.ast.get_variable(sketch_name) {
let constraint_level = match ast_sketch {
super::types::Definition::Variable(var) => var.get_constraint_level(),
super::types::Definition::Import(import) => import.get_constraint_level(),
@ -130,8 +131,8 @@ pub async fn modify_ast_for_sketch(
control_points.push(ControlPointData {
points: data.control_points.clone(),
command: segment.command,
id: (*command_id).into(),
_command: segment.command,
_id: (*command_id).into(),
});
}
}
@ -179,12 +180,12 @@ pub async fn modify_ast_for_sketch(
)?;
// Add the sketch back to the program.
program.replace_variable(sketch_name, sketch);
program.ast.replace_variable(sketch_name, sketch);
let recasted = program.recast(&FormatOptions::default(), 0);
let recasted = program.ast.recast(&FormatOptions::default(), 0);
// Re-parse the ast so we get the correct source ranges.
*program = crate::parser::parse(&recasted, module_id)?;
*program = crate::parser::parse(&recasted, module_id)?.into();
Ok(recasted)
}

View File

@ -223,7 +223,7 @@ impl Node<Program> {
/// Check the provided Program for any lint findings.
pub fn lint<'a, RuleT>(&'a self, rule: RuleT) -> Result<Vec<crate::lint::Discovered>>
where
RuleT: crate::lint::rule::Rule<'a>,
RuleT: crate::lint::Rule<'a>,
{
let v = Arc::new(Mutex::new(vec![]));
crate::walk::walk(self, &|node: crate::walk::Node<'a>| {

View File

@ -1,4 +1,5 @@
//! Core dump related structures and functions.
#![allow(dead_code)]
#[cfg(not(target_arch = "wasm32"))]
pub mod local;

View File

@ -26,14 +26,14 @@ type Point3D = kcmc::shared::Point3d<f64>;
use crate::{
ast::types::{
BodyItem, Expr, FunctionExpression, ItemVisibility, KclNone, ModuleId, Node, NodeRef, Program, TagDeclarator,
TagNode,
BodyItem, Expr, FunctionExpression, ItemVisibility, KclNone, ModuleId, Node, NodeRef, TagDeclarator, TagNode,
},
engine::{EngineManager, ExecutionKind},
errors::{KclError, KclErrorDetails},
fs::{FileManager, FileSystem},
settings::types::UnitLength,
std::{FnAsArg, StdLib},
Program,
};
/// State for executing a program.
@ -152,6 +152,7 @@ impl ProgramMemory {
/// Find all solids in the memory that are on a specific sketch id.
/// This does not look inside closures. But as long as we do not allow
/// mutation of variables in KCL, closure memory should be a subset of this.
#[allow(clippy::vec_box)]
pub fn find_solids_on_sketch(&self, sketch_id: uuid::Uuid) -> Vec<Box<Solid>> {
self.environments
.iter()
@ -537,6 +538,7 @@ impl Geometry {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
#[allow(clippy::vec_box)]
pub enum Geometries {
Sketches(Vec<Box<Sketch>>),
Solids(Vec<Box<Solid>>),
@ -555,6 +557,7 @@ impl From<Geometry> for Geometries {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
#[allow(clippy::vec_box)]
pub enum SketchSet {
Sketch(Box<Sketch>),
Sketches(Vec<Box<Sketch>>),
@ -635,6 +638,7 @@ impl From<Box<Sketch>> for Vec<Box<Sketch>> {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
#[allow(clippy::vec_box)]
pub enum SolidSet {
Solid(Box<Solid>),
Solids(Vec<Box<Solid>>),
@ -801,6 +805,17 @@ impl Plane {
},
}
}
/// The standard planes are XY, YZ and XZ (in both positive and negative)
pub fn is_standard(&self) -> bool {
!self.is_custom()
}
/// The standard planes are XY, YZ and XZ (in both positive and negative)
/// Custom planes are any other plane that the user might specify.
pub fn is_custom(&self) -> bool {
matches!(self.value, PlaneType::Custom)
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
@ -1049,6 +1064,14 @@ impl KclValue {
}
}
pub fn as_plane(&self) -> Option<&Plane> {
if let KclValue::Plane(value) = &self {
Some(value)
} else {
None
}
}
pub fn as_solid(&self) -> Option<&Solid> {
if let KclValue::Solid(value) = &self {
Some(value)
@ -2170,6 +2193,70 @@ impl ExecutorContext {
})
}
#[cfg(not(target_arch = "wasm32"))]
pub async fn new_mock() -> Self {
ExecutorContext {
engine: Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new().await.unwrap(),
)),
fs: Arc::new(FileManager::new()),
stdlib: Arc::new(StdLib::new()),
settings: Default::default(),
context_type: ContextType::Mock,
}
}
#[cfg(target_arch = "wasm32")]
pub async fn new(
engine_manager: crate::engine::conn_wasm::EngineCommandManager,
fs_manager: crate::fs::wasm::FileSystemManager,
units: UnitLength,
) -> Result<Self, String> {
Ok(ExecutorContext {
engine: Arc::new(Box::new(
crate::engine::conn_wasm::EngineConnection::new(engine_manager)
.await
.map_err(|e| format!("{:?}", e))?,
)),
fs: Arc::new(FileManager::new(fs_manager)),
stdlib: Arc::new(StdLib::new()),
settings: ExecutorSettings {
units,
..Default::default()
},
context_type: ContextType::Live,
})
}
#[cfg(target_arch = "wasm32")]
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()
.await
.map_err(|e| format!("{:?}", e))?,
)),
fs: Arc::new(FileManager::new(fs_manager)),
stdlib: Arc::new(StdLib::new()),
settings: ExecutorSettings {
units,
..Default::default()
},
context_type: ContextType::Mock,
})
}
#[cfg(not(target_arch = "wasm32"))]
pub fn new_forwarded_mock(engine: Arc<Box<dyn EngineManager>>) -> Self {
ExecutorContext {
engine,
fs: Arc::new(FileManager::new()),
stdlib: Arc::new(StdLib::new()),
settings: Default::default(),
context_type: ContextType::MockCustomForwarded,
}
}
/// Create a new default executor context.
/// With a kittycad client.
/// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
@ -2193,9 +2280,17 @@ impl ExecutorContext {
/// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
/// variables.
#[cfg(not(target_arch = "wasm32"))]
pub async fn new_with_default_client(settings: ExecutorSettings) -> Result<Self> {
pub async fn new_with_default_client(units: UnitLength) -> Result<Self> {
// Create the client.
let ctx = Self::new_with_client(settings, None, None).await?;
let ctx = Self::new_with_client(
ExecutorSettings {
units,
..Default::default()
},
None,
None,
)
.await?;
Ok(ctx)
}
@ -2223,48 +2318,31 @@ impl ExecutorContext {
pub async fn reset_scene(
&self,
id_generator: &mut IdGenerator,
exec_state: &mut ExecState,
source_range: crate::executor::SourceRange,
) -> Result<()> {
self.engine.clear_scene(id_generator, source_range).await?;
self.engine
.clear_scene(&mut exec_state.id_generator, source_range)
.await?;
Ok(())
}
/// Perform the execution of a program.
/// You can optionally pass in some initialization memory.
/// Kurt uses this for partial execution.
pub async fn run(
&self,
program: NodeRef<'_, crate::ast::types::Program>,
memory: Option<ProgramMemory>,
id_generator: IdGenerator,
project_directory: Option<String>,
) -> Result<ExecState, KclError> {
self.run_with_session_data(program, memory, id_generator, project_directory)
.await
.map(|x| x.0)
pub async fn run(&self, program: &Program, exec_state: &mut ExecState) -> Result<(), KclError> {
self.run_with_session_data(program, exec_state).await?;
Ok(())
}
/// Perform the execution of a program.
/// You can optionally pass in some initialization memory.
/// Kurt uses this for partial execution.
pub async fn run_with_session_data(
&self,
program: NodeRef<'_, crate::ast::types::Program>,
memory: Option<ProgramMemory>,
id_generator: IdGenerator,
project_directory: Option<String>,
) -> Result<(ExecState, Option<ModelingSessionData>), KclError> {
let memory = if let Some(memory) = memory {
memory.clone()
} else {
Default::default()
};
let mut exec_state = ExecState {
memory,
id_generator,
project_directory,
..Default::default()
};
program: &Program,
exec_state: &mut ExecState,
) -> Result<Option<ModelingSessionData>, KclError> {
// TODO: Use the top-level file's path.
exec_state.add_module(std::path::PathBuf::from(""));
// Before we even start executing the program, set the units.
@ -2285,10 +2363,10 @@ impl ExecutorContext {
)
.await?;
self.inner_execute(program, &mut exec_state, crate::executor::BodyType::Root)
self.inner_execute(&program.ast, exec_state, crate::executor::BodyType::Root)
.await?;
let session_data = self.engine.get_session_data();
Ok((exec_state, session_data))
Ok(session_data)
}
/// Execute an AST's program.
@ -2539,23 +2617,15 @@ impl ExecutorContext {
/// Execute the program, then get a PNG screenshot.
pub async fn execute_and_prepare_snapshot(
&self,
program: NodeRef<'_, Program>,
id_generator: IdGenerator,
project_directory: Option<String>,
program: &Program,
exec_state: &mut ExecState,
) -> Result<TakeSnapshot> {
self.execute_and_prepare(program, id_generator, project_directory)
.await
.map(|(_state, snap)| snap)
self.execute_and_prepare(program, exec_state).await
}
/// Execute the program, return the interpreter and outputs.
pub async fn execute_and_prepare(
&self,
program: NodeRef<'_, Program>,
id_generator: IdGenerator,
project_directory: Option<String>,
) -> Result<(ExecState, TakeSnapshot)> {
let state = self.run(program, None, id_generator, project_directory).await?;
pub async fn execute_and_prepare(&self, program: &Program, exec_state: &mut ExecState) -> Result<TakeSnapshot> {
self.run(program, exec_state).await?;
// Zoom to fit.
self.engine
@ -2588,7 +2658,7 @@ impl ExecutorContext {
else {
anyhow::bail!("Unexpected response from engine: {:?}", resp);
};
Ok((state, contents))
Ok(contents)
}
}
@ -2695,7 +2765,7 @@ mod tests {
use crate::ast::types::{Identifier, Node, Parameter};
pub async fn parse_execute(code: &str) -> Result<ProgramMemory> {
let program = crate::parser::top_level_parse(code)?;
let program = Program::parse(code)?;
let ctx = ExecutorContext {
engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await?)),
@ -2704,7 +2774,8 @@ mod tests {
settings: Default::default(),
context_type: ContextType::Mock,
};
let exec_state = ctx.run(&program, None, IdGenerator::default(), None).await?;
let mut exec_state = ExecState::default();
ctx.run(&program, &mut exec_state).await?;
Ok(exec_state.memory)
}
@ -3033,14 +3104,14 @@ for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
#[tokio::test(flavor = "multi_thread")]
async fn test_get_member_of_array_with_function() {
let ast = r#"fn box = (array) => {
let ast = r#"fn box = (arr) => {
let myBox =startSketchOn('XY')
|> startProfileAt(array[0], %)
|> line([0, array[1]], %)
|> line([array[2], 0], %)
|> line([0, -array[1]], %)
|> startProfileAt(arr[0], %)
|> line([0, arr[1]], %)
|> line([arr[2], 0], %)
|> line([0, -arr[1]], %)
|> close(%)
|> extrude(array[3], %)
|> extrude(arr[3], %)
return myBox
}

View File

@ -13,26 +13,112 @@ macro_rules! println {
}
}
pub mod ast;
pub mod coredump;
pub mod docs;
pub mod engine;
pub mod errors;
pub mod executor;
pub mod fs;
mod ast;
mod coredump;
mod docs;
mod engine;
mod errors;
mod executor;
mod fs;
mod function_param;
pub mod lint;
pub mod lsp;
pub mod parser;
pub mod settings;
mod lsp;
mod parser;
mod settings;
#[cfg(test)]
mod simulation_tests;
pub mod std;
mod std;
#[cfg(not(target_arch = "wasm32"))]
pub mod test_server;
pub mod thread;
pub mod token;
mod thread;
mod token;
mod unparser;
pub mod walk;
mod walk;
#[cfg(target_arch = "wasm32")]
pub mod wasm;
mod wasm;
pub use ast::modify::modify_ast_for_sketch;
pub use ast::types::{FormatOptions, ModuleId};
pub use coredump::CoreDump;
pub use engine::{EngineManager, ExecutionKind};
pub use errors::KclError;
pub use executor::{ExecState, ExecutorContext, ExecutorSettings, SourceRange};
pub use lsp::copilot::Backend as CopilotLspBackend;
pub use lsp::kcl::Backend as KclLspBackend;
pub use lsp::kcl::Server as KclLspServerSubCommand;
pub use settings::types::{project::ProjectConfiguration, Configuration, UnitLength};
pub use token::lexer;
// Rather than make executor public and make lots of it pub(crate), just re-export into a new module.
// Ideally we wouldn't export these things at all, they should only be used for testing.
pub mod exec {
pub use crate::executor::{DefaultPlanes, IdGenerator, KclValue, PlaneType, ProgramMemory, Sketch};
}
#[cfg(target_arch = "wasm32")]
pub mod wasm_engine {
pub use crate::coredump::wasm::{CoreDumpManager, CoreDumper};
pub use crate::engine::conn_wasm::{EngineCommandManager, EngineConnection};
pub use crate::fs::wasm::FileSystemManager;
}
#[cfg(not(target_arch = "wasm32"))]
pub mod native_engine {
pub use crate::engine::conn::EngineConnection;
}
pub mod std_utils {
pub use crate::std::utils::{get_tangential_arc_to_info, is_points_ccw_wasm, TangentialArcInfoInput};
}
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Program {
#[serde(flatten)]
ast: ast::types::Node<ast::types::Program>,
}
#[cfg(any(test, feature = "lsp-test-util"))]
pub use lsp::test_util::copilot_lsp_server;
#[cfg(any(test, feature = "lsp-test-util"))]
pub use lsp::test_util::kcl_lsp_server;
impl Program {
pub fn parse(input: &str) -> Result<Program, KclError> {
let module_id = ModuleId::default();
let tokens = token::lexer(input, module_id)?;
let parser = parser::Parser::new(tokens);
let ast = parser.ast()?;
Ok(Program { ast })
}
/// Deserialize the ast from a stringified json
pub fn compute_digest(&mut self) -> ast::types::digest::Digest {
self.ast.compute_digest()
}
pub fn lint_all(&self) -> Result<Vec<lint::Discovered>, anyhow::Error> {
self.ast.lint_all()
}
pub fn lint<'a>(&'a self, rule: impl lint::Rule<'a>) -> Result<Vec<lint::Discovered>, anyhow::Error> {
self.ast.lint(rule)
}
pub fn recast(&self) -> String {
// Use the default options until we integrate into the UI the ability to change them.
self.ast.recast(&Default::default(), 0)
}
pub fn recast_with_options(&self, options: &FormatOptions) -> String {
self.ast.recast(options, 0)
}
}
impl From<ast::types::Node<ast::types::Program>> for Program {
fn from(ast: ast::types::Node<ast::types::Program>) -> Program {
Self { ast }
}
}

View File

@ -2,7 +2,6 @@ mod camel_case;
mod offset_plane;
mod std_lib_args;
#[allow(unused_imports)]
pub use camel_case::{lint_object_properties, lint_variables, Z0001};
pub use offset_plane::{lint_should_be_offset_plane, Z0003};
pub use std_lib_args::{lint_call_expressions, Z0002};

View File

@ -1,4 +1,4 @@
pub mod checks;
pub mod rule;
mod rule;
pub use rule::{Discovered, Finding};
pub use rule::{Discovered, Finding, Rule};

View File

@ -1,4 +1,5 @@
//! The copilot lsp server for ghost text.
#![allow(dead_code)]
pub mod cache;
pub mod types;
@ -26,6 +27,7 @@ use tower_lsp::{
use crate::lsp::{
backend::Backend as _,
copilot::cache::CopilotCache,
copilot::types::{
CopilotAcceptCompletionParams, CopilotCompletionResponse, CopilotCompletionTelemetry, CopilotEditorInfo,
CopilotLspCompletionParams, CopilotRejectCompletionParams, DocParams,
@ -131,6 +133,38 @@ impl crate::lsp::backend::Backend for Backend {
}
impl Backend {
#[cfg(target_arch = "wasm32")]
pub fn new_wasm(
client: tower_lsp::Client,
fs: crate::fs::wasm::FileSystemManager,
zoo_client: kittycad::Client,
dev_mode: bool,
) -> Self {
Self::new(client, crate::fs::FileManager::new(fs), zoo_client, dev_mode)
}
pub fn new(
client: tower_lsp::Client,
fs: crate::fs::FileManager,
zoo_client: kittycad::Client,
dev_mode: bool,
) -> Self {
Self {
client,
fs: Arc::new(fs),
workspace_folders: Default::default(),
code_map: Default::default(),
editor_info: Arc::new(RwLock::new(CopilotEditorInfo::default())),
cache: Arc::new(CopilotCache::new()),
telemetry: Default::default(),
zoo_client,
is_initialized: Default::default(),
diagnostics_map: Default::default(),
dev_mode,
}
}
/// Get completions from the kittycad api.
pub async fn get_completions(&self, language: String, prompt: String, suffix: String) -> Result<Vec<String>> {
let body = kittycad::types::KclCodeCompletionRequest {

View File

@ -1,4 +1,5 @@
//! Functions for the `kcl` lsp server.
#![allow(dead_code)]
use std::{
collections::HashMap,
@ -40,11 +41,11 @@ use tower_lsp::{
};
use crate::{
ast::types::{Expr, ModuleId, Node, NodeRef, VariableKind},
executor::{IdGenerator, SourceRange},
ast::types::{Expr, ModuleId, Node, VariableKind},
lsp::{backend::Backend as _, util::IntoDiagnostic},
parser::PIPE_OPERATOR,
token::TokenType,
ExecState, Program, SourceRange,
};
lazy_static::lazy_static! {
@ -122,6 +123,73 @@ pub struct Backend {
pub is_initialized: Arc<RwLock<bool>>,
}
impl Backend {
#[cfg(target_arch = "wasm32")]
pub fn new_wasm(
client: Client,
executor_ctx: Option<crate::executor::ExecutorContext>,
fs: crate::fs::wasm::FileSystemManager,
zoo_client: kittycad::Client,
can_send_telemetry: bool,
) -> Result<Self, String> {
Self::with_file_manager(
client,
executor_ctx,
crate::fs::FileManager::new(fs),
zoo_client,
can_send_telemetry,
)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn new(
client: Client,
executor_ctx: Option<crate::executor::ExecutorContext>,
zoo_client: kittycad::Client,
can_send_telemetry: bool,
) -> Result<Self, String> {
Self::with_file_manager(
client,
executor_ctx,
crate::fs::FileManager::new(),
zoo_client,
can_send_telemetry,
)
}
fn with_file_manager(
client: Client,
executor_ctx: Option<crate::executor::ExecutorContext>,
fs: crate::fs::FileManager,
zoo_client: kittycad::Client,
can_send_telemetry: bool,
) -> Result<Self, String> {
let stdlib = crate::std::StdLib::new();
let stdlib_completions = get_completions_from_stdlib(&stdlib).map_err(|e| e.to_string())?;
let stdlib_signatures = get_signatures_from_stdlib(&stdlib).map_err(|e| e.to_string())?;
Ok(Self {
client,
fs: Arc::new(fs),
stdlib_completions,
stdlib_signatures,
zoo_client,
can_send_telemetry,
can_execute: Arc::new(RwLock::new(executor_ctx.is_some())),
executor_ctx: Arc::new(RwLock::new(executor_ctx)),
workspace_folders: Default::default(),
token_map: Default::default(),
ast_map: Default::default(),
memory_map: Default::default(),
code_map: Default::default(),
diagnostics_map: Default::default(),
symbols_map: Default::default(),
semantic_tokens_map: Default::default(),
is_initialized: Default::default(),
})
}
}
// Implement the shared backend trait for the language server.
#[async_trait::async_trait]
impl crate::lsp::backend::Backend for Backend {
@ -289,7 +357,7 @@ impl crate::lsp::backend::Backend for Backend {
// Execute the code if we have an executor context.
// This function automatically executes if we should & updates the diagnostics if we got
// errors.
if self.execute(&params, &ast).await.is_err() {
if self.execute(&params, &ast.into()).await.is_err() {
return;
}
@ -572,7 +640,7 @@ impl Backend {
self.client.publish_diagnostics(params.uri.clone(), items, None).await;
}
async fn execute(&self, params: &TextDocumentItem, ast: NodeRef<'_, crate::ast::types::Program>) -> Result<()> {
async fn execute(&self, params: &TextDocumentItem, ast: &Program) -> Result<()> {
// Check if we can execute.
if !self.can_execute().await {
return Ok(());
@ -589,25 +657,22 @@ impl Backend {
return Ok(());
}
let mut id_generator = IdGenerator::default();
let mut exec_state = ExecState::default();
// Clear the scene, before we execute so it's not fugly as shit.
executor_ctx
.engine
.clear_scene(&mut id_generator, SourceRange::default())
.clear_scene(&mut exec_state.id_generator, SourceRange::default())
.await?;
let exec_state = match executor_ctx.run(ast, None, id_generator, None).await {
Ok(exec_state) => exec_state,
Err(err) => {
self.memory_map.remove(params.uri.as_str());
self.add_to_diagnostics(params, &[err], false).await;
if let Err(err) = executor_ctx.run(ast, &mut exec_state).await {
self.memory_map.remove(params.uri.as_str());
self.add_to_diagnostics(params, &[err], false).await;
// Since we already published the diagnostics we don't really care about the error
// string.
return Err(anyhow::anyhow!("failed to execute code"));
}
};
// Since we already published the diagnostics we don't really care about the error
// string.
return Err(anyhow::anyhow!("failed to execute code"));
}
self.memory_map
.insert(params.uri.to_string(), exec_state.memory.clone());

View File

@ -2,7 +2,7 @@ use std::collections::BTreeMap;
use pretty_assertions::assert_eq;
use tower_lsp::{
lsp_types::{SemanticTokenModifier, SemanticTokenType},
lsp_types::{Diagnostic, SemanticTokenModifier, SemanticTokenType},
LanguageServer,
};
@ -2369,7 +2369,14 @@ async fn kcl_test_kcl_lsp_diagnostics_on_execution_error() {
// Get the diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl");
assert!(diagnostics.is_none());
if let Some(diagnostics) = diagnostics {
let ds: Vec<Diagnostic> = diagnostics.to_owned();
eprintln!("Expected no diagnostics, but found some.");
for d in ds {
eprintln!("{:?}: {}", d.severity, d.message);
}
panic!();
}
}
#[tokio::test(flavor = "multi_thread")]

View File

@ -12,6 +12,7 @@ pub(crate) mod parser_impl;
pub const PIPE_SUBSTITUTION_OPERATOR: &str = "%";
pub const PIPE_OPERATOR: &str = "|>";
#[cfg(test)]
/// Parse the given KCL code into an AST. This is the top-level.
pub fn top_level_parse(code: &str) -> Result<Node<Program>, KclError> {
let module_id = ModuleId::default();

View File

@ -2040,11 +2040,39 @@ fn fn_call(i: TokenSlice) -> PResult<Node<CallExpression>> {
#[cfg(test)]
mod tests {
use itertools::Itertools;
use pretty_assertions::assert_eq;
use super::*;
use crate::ast::types::{BodyItem, Expr, ModuleId, VariableKind};
fn assert_reserved(word: &str) {
// Try to use it as a variable name.
let code = format!(r#"{} = 0"#, word);
let result = crate::parser::top_level_parse(code.as_str());
let err = result.unwrap_err();
// Which token causes the error may change. In "return = 0", for
// example, "return" is the problem.
assert!(
err.message().starts_with("Unexpected token: ")
|| err
.message()
.starts_with("Cannot assign a variable to a reserved keyword: "),
"Error message is: {}",
err.message(),
);
}
#[test]
fn reserved_words() {
// Since these are stored in a set, we sort to make the tests
// deterministic.
for word in crate::token::RESERVED_WORDS.keys().sorted() {
assert_reserved(word);
}
assert_reserved("import");
}
#[test]
fn parse_args() {
for (i, (test, expected_len)) in [("someVar", 1), ("5, 3", 2), (r#""a""#, 1)].into_iter().enumerate() {

View File

@ -9,7 +9,6 @@ use serde::{Deserialize, Serialize};
use validator::{Validate, ValidateRange};
const DEFAULT_THEME_COLOR: f64 = 264.5;
pub const DEFAULT_PROJECT_KCL_FILE: &str = "main.kcl";
const DEFAULT_PROJECT_NAME_TEMPLATE: &str = "project-$nnn";
/// High level configuration.

File diff suppressed because it is too large Load Diff

View File

@ -692,7 +692,7 @@ macro_rules! let_field_of {
impl<'a> FromKclValue<'a> for crate::std::import::ImportFormat {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let obj = arg.as_object()?;
let_field_of!(obj, typ "type");
let_field_of!(obj, typ "format");
match typ {
"fbx" => Some(Self::Fbx {}),
"gltf" => Some(Self::Gltf {}),
@ -794,6 +794,45 @@ impl<'a> FromKclValue<'a> for crate::std::planes::StandardPlane {
}
}
impl<'a> FromKclValue<'a> for crate::executor::Plane {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
if let Some(plane) = arg.as_plane() {
return Some(plane.clone());
}
let obj = arg.as_object()?;
let_field_of!(obj, id);
let_field_of!(obj, value);
let_field_of!(obj, origin);
let_field_of!(obj, x_axis "xAxis");
let_field_of!(obj, y_axis "yAxis");
let_field_of!(obj, z_axis "zAxis");
let_field_of!(obj, meta "__meta");
Some(Self {
id,
value,
origin,
x_axis,
y_axis,
z_axis,
meta,
})
}
}
impl<'a> FromKclValue<'a> for crate::executor::PlaneType {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let plane_type = match arg.as_str()? {
"XY" | "xy" => Self::XY,
"XZ" | "xz" => Self::XZ,
"YZ" | "yz" => Self::YZ,
"Custom" => Self::Custom,
_ => return None,
};
Some(plane_type)
}
}
impl<'a> FromKclValue<'a> for kittycad_modeling_cmds::units::UnitLength {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let s = arg.as_str()?;
@ -1032,6 +1071,15 @@ impl<'a> FromKclValue<'a> for super::sketch::ArcData {
}
}
impl<'a> FromKclValue<'a> for super::sketch::ArcToData {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let obj = arg.as_object()?;
let_field_of!(obj, end);
let_field_of!(obj, interior);
Some(Self { end, interior })
}
}
impl<'a> FromKclValue<'a> for super::revolve::RevolveData {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let obj = arg.as_object()?;
@ -1264,11 +1312,15 @@ impl<'a> FromKclValue<'a> for crate::executor::Solid {
impl<'a> FromKclValue<'a> for super::sketch::SketchData {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let case1 = super::sketch::PlaneData::from_kcl_val;
let case2 = crate::executor::Solid::from_kcl_val;
// Order is critical since PlaneData is a subset of Plane.
let case1 = crate::executor::Plane::from_kcl_val;
let case2 = super::sketch::PlaneData::from_kcl_val;
let case3 = crate::executor::Solid::from_kcl_val;
case1(arg)
.map(Box::new)
.map(Self::Plane)
.or_else(|| case2(arg).map(Box::new).map(Self::Solid))
.or_else(|| case2(arg).map(Self::PlaneOrientation))
.or_else(|| case3(arg).map(Box::new).map(Self::Solid))
}
}

View File

@ -107,14 +107,14 @@ pub async fn reduce(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
///
/// // This function adds an array of numbers.
/// // It uses the `reduce` function, to call the `add` function on every
/// // element of the `array` parameter. The starting value is 0.
/// fn sum = (array) => { return reduce(array, 0, add) }
/// // element of the `arr` parameter. The starting value is 0.
/// fn sum = (arr) => { return reduce(arr, 0, add) }
///
/// /*
/// The above is basically like this pseudo-code:
/// fn sum(array):
/// fn sum(arr):
/// let sumSoFar = 0
/// for i in array:
/// for i in arr:
/// sumSoFar = add(sumSoFar, i)
/// return sumSoFar
/// */
@ -127,8 +127,8 @@ pub async fn reduce(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// // This example works just like the previous example above, but it uses
/// // an anonymous `add` function as its parameter, instead of declaring a
/// // named function outside.
/// array = [1, 2, 3]
/// sum = reduce(array, 0, (i, result_so_far) => { return i + result_so_far })
/// arr = [1, 2, 3]
/// sum = reduce(arr, 0, (i, result_so_far) => { return i + result_so_far })
///
/// // We use `assertEqual` to check that our `sum` function gives the
/// // expected result. It's good to check your work!

View File

@ -42,7 +42,7 @@ const ZOO_COORD_SYSTEM: System = System {
/// Import format specifier
#[derive(serde :: Serialize, serde :: Deserialize, PartialEq, Debug, Clone, schemars :: JsonSchema)]
#[cfg_attr(feature = "tabled", derive(tabled::Tabled))]
#[serde(tag = "type")]
#[serde(tag = "format")]
pub enum ImportFormat {
/// Autodesk Filmbox (FBX) format
#[serde(rename = "fbx")]
@ -152,7 +152,7 @@ pub async fn import(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// ```
///
/// ```no_run
/// const model = import("tests/inputs/cube.obj", {type: "obj", units: "m"})
/// const model = import("tests/inputs/cube.obj", {format: "obj", units: "m"})
/// ```
///
/// ```no_run

View File

@ -4,7 +4,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
ast::types::{BodyItem, Expr, FunctionExpression, Node, Program},
ast::types::{FunctionExpression, Program},
docs::{StdLibFn, StdLibFnData},
};
@ -77,18 +77,3 @@ impl Serialize for Box<dyn KclStdLibFn> {
self.to_json().unwrap().serialize(serializer)
}
}
/// Parse a KCL program. Expect it to have a single body item, which is a function.
/// Return the program and its single function.
/// Return None if those expectations aren't met.
pub fn extract_function(source: &str) -> Option<(Node<Program>, crate::ast::types::BoxNode<FunctionExpression>)> {
let src = crate::parser::top_level_parse(source).ok()?;
assert_eq!(src.body.len(), 1);
let BodyItem::ExpressionStatement(expr) = src.body.last()? else {
panic!("expected expression statement");
};
let Expr::FunctionExpression(function) = expr.expression.clone() else {
panic!("expected function expr");
};
Some((src, function))
}

View File

@ -48,8 +48,6 @@ pub type StdFn = fn(
Args,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<KclValue, KclError>> + Send + '_>>;
pub type FnMap = HashMap<String, StdFn>;
lazy_static! {
static ref CORE_FNS: Vec<Box<dyn StdLibFn>> = vec![
Box::new(LegLen),
@ -91,6 +89,7 @@ lazy_static! {
Box::new(crate::std::sketch::ProfileStart),
Box::new(crate::std::sketch::Close),
Box::new(crate::std::sketch::Arc),
Box::new(crate::std::sketch::ArcTo),
Box::new(crate::std::sketch::TangentialArc),
Box::new(crate::std::sketch::TangentialArcTo),
Box::new(crate::std::sketch::TangentialArcToRelative),

View File

@ -1,17 +1,20 @@
//! Standard library plane helpers.
use derive_docs::stdlib;
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::Color, ModelingCmd};
use kittycad_modeling_cmds as kcmc;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
errors::KclError,
executor::{ExecState, KclValue, Plane},
executor::{ExecState, KclValue, Plane, PlaneType},
std::{sketch::PlaneData, Args},
};
/// One of the standard planes.
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, JsonSchema)]
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub enum StandardPlane {
/// The XY plane.
@ -50,8 +53,8 @@ impl From<StandardPlane> for PlaneData {
/// Offset a plane by a distance along its normal.
pub async fn offset_plane(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (std_plane, offset): (StandardPlane, f64) = args.get_data_and_float()?;
let plane_data = inner_offset_plane(std_plane, offset, exec_state).await?;
let plane = Plane::from_plane_data(plane_data, exec_state);
let plane = inner_offset_plane(std_plane, offset, exec_state).await?;
make_offset_plane_in_engine(&plane, exec_state, &args).await?;
Ok(KclValue::Plane(Box::new(plane)))
}
@ -144,11 +147,14 @@ async fn inner_offset_plane(
std_plane: StandardPlane,
offset: f64,
exec_state: &mut ExecState,
) -> Result<PlaneData, KclError> {
) -> Result<Plane, KclError> {
// Convert to the plane type.
let plane_data: PlaneData = std_plane.into();
// Convert to a plane.
let mut plane = Plane::from_plane_data(plane_data, exec_state);
// Though offset planes are derived from standard planes, they are not
// standard planes themselves.
plane.value = PlaneType::Custom;
match std_plane {
StandardPlane::XY => {
@ -171,10 +177,44 @@ async fn inner_offset_plane(
}
}
Ok(PlaneData::Plane {
origin: Box::new(plane.origin),
x_axis: Box::new(plane.x_axis),
y_axis: Box::new(plane.y_axis),
z_axis: Box::new(plane.z_axis),
})
Ok(plane)
}
// Engine-side effectful creation of an actual plane object.
// offset planes are shown by default, and hidden by default if they
// are used as a sketch plane. That hiding command is sent within inner_start_profile_at
async fn make_offset_plane_in_engine(plane: &Plane, exec_state: &mut ExecState, args: &Args) -> Result<(), KclError> {
// Create new default planes.
let default_size = 100.0;
let color = Color {
r: 0.6,
g: 0.6,
b: 0.6,
a: 0.3,
};
args.batch_modeling_cmd(
plane.id,
ModelingCmd::from(mcmd::MakePlane {
clobber: false,
origin: plane.origin.into(),
size: LengthUnit(default_size),
x_axis: plane.x_axis.into(),
y_axis: plane.y_axis.into(),
hide: Some(false),
}),
)
.await?;
// Set the color.
args.batch_modeling_cmd(
exec_state.id_generator.next_uuid(),
ModelingCmd::from(mcmd::PlaneSetColor {
color,
plane_id: plane.id,
}),
)
.await?;
Ok(())
}

View File

@ -894,7 +894,7 @@ pub async fn start_sketch_at(exec_state: &mut ExecState, args: Args) -> Result<K
async fn inner_start_sketch_at(data: [f64; 2], exec_state: &mut ExecState, args: Args) -> Result<Sketch, KclError> {
// Let's assume it's the XY plane for now, this is just for backwards compatibility.
let xy_plane = PlaneData::XY;
let sketch_surface = inner_start_sketch_on(SketchData::Plane(xy_plane), None, exec_state, &args).await?;
let sketch_surface = inner_start_sketch_on(SketchData::PlaneOrientation(xy_plane), None, exec_state, &args).await?;
let sketch = inner_start_profile_at(data, sketch_surface, None, exec_state, args).await?;
Ok(sketch)
}
@ -905,11 +905,12 @@ async fn inner_start_sketch_at(data: [f64; 2], exec_state: &mut ExecState, args:
#[ts(export)]
#[serde(rename_all = "camelCase", untagged)]
pub enum SketchData {
Plane(PlaneData),
PlaneOrientation(PlaneData),
Plane(Box<Plane>),
Solid(Box<Solid>),
}
/// Data for a plane.
/// Orientation data that can be used to construct a plane, not a plane in itself.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
@ -1069,10 +1070,11 @@ async fn inner_start_sketch_on(
args: &Args,
) -> Result<SketchSurface, KclError> {
match data {
SketchData::Plane(plane_data) => {
let plane = start_sketch_on_plane(plane_data, exec_state, args).await?;
SketchData::PlaneOrientation(plane_data) => {
let plane = make_sketch_plane_from_orientation(plane_data, exec_state, args).await?;
Ok(SketchSurface::Plane(plane))
}
SketchData::Plane(plane) => Ok(SketchSurface::Plane(plane)),
SketchData::Solid(solid) => {
let Some(tag) = tag else {
return Err(KclError::Type(KclErrorDetails {
@ -1106,7 +1108,7 @@ async fn start_sketch_on_face(
}))
}
async fn start_sketch_on_plane(
async fn make_sketch_plane_from_orientation(
data: PlaneData,
exec_state: &mut ExecState,
args: &Args,
@ -1122,10 +1124,10 @@ async fn start_sketch_on_plane(
plane.id = match data {
PlaneData::XY => default_planes.xy,
PlaneData::XZ => default_planes.xz,
PlaneData::YZ => default_planes.yz,
PlaneData::NegXY => default_planes.neg_xy,
PlaneData::XZ => default_planes.xz,
PlaneData::NegXZ => default_planes.neg_xz,
PlaneData::YZ => default_planes.yz,
PlaneData::NegYZ => default_planes.neg_yz,
PlaneData::Plane {
origin,
@ -1210,11 +1212,26 @@ pub(crate) async fn inner_start_profile_at(
exec_state: &mut ExecState,
args: Args,
) -> Result<Sketch, KclError> {
if let SketchSurface::Face(face) = &sketch_surface {
// Flush the batch for our fillets/chamfers if there are any.
// If we do not do these for sketch on face, things will fail with face does not exist.
args.flush_batch_for_solid_set(exec_state, face.solid.clone().into())
match &sketch_surface {
SketchSurface::Face(face) => {
// Flush the batch for our fillets/chamfers if there are any.
// If we do not do these for sketch on face, things will fail with face does not exist.
args.flush_batch_for_solid_set(exec_state, face.solid.clone().into())
.await?;
}
SketchSurface::Plane(plane) if !plane.is_standard() => {
// Hide whatever plane we are sketching on.
// This is especially helpful for offset planes, which would be visible otherwise.
args.batch_end_cmd(
exec_state.id_generator.next_uuid(),
ModelingCmd::from(mcmd::ObjectVisible {
object_id: plane.id,
hidden: true,
}),
)
.await?;
}
_ => {}
}
// Enter sketch mode on the surface.
@ -1469,6 +1486,17 @@ pub enum ArcData {
},
}
/// Data to draw a three point arc (arcTo).
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ArcToData {
/// End point of the arc. A point in 3D space
pub end: [f64; 2],
/// Interior point of the arc. A point in 3D space
pub interior: [f64; 2],
}
/// Draw an arc.
pub async fn arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, sketch, tag): (ArcData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
@ -1499,7 +1527,7 @@ pub async fn arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl
/// radius: 16
/// }, %)
/// |> close(%)
// const example = extrude(10, exampleSketch)
/// const example = extrude(10, exampleSketch)
/// ```
#[stdlib {
name = "arc",
@ -1578,6 +1606,104 @@ pub(crate) async fn inner_arc(
Ok(new_sketch)
}
/// Draw a three point arc.
pub async fn arc_to(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, sketch, tag): (ArcToData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
let new_sketch = inner_arc_to(data, sketch, tag, exec_state, args).await?;
Ok(KclValue::Sketch {
value: Box::new(new_sketch),
})
}
/// Draw a 3 point arc.
///
/// The arc is constructed such that the start point is the current position of the sketch and two more points defined as the end and interior point.
/// The interior point is placed between the start point and end point. The radius of the arc will be controlled by how far the interior point is placed from
/// the start and end.
///
/// ```no_run
/// const exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> arcTo({
/// end: [10,0],
/// interior: [5,5]
/// }, %)
/// |> close(%)
/// const example = extrude(10, exampleSketch)
/// ```
#[stdlib {
name = "arcTo",
}]
pub(crate) async fn inner_arc_to(
data: ArcToData,
sketch: Sketch,
tag: Option<TagNode>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Sketch, KclError> {
let from: Point2d = sketch.current_pen_position()?;
let id = exec_state.id_generator.next_uuid();
// The start point is taken from the path you are extending.
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::ExtendPath {
path: sketch.id.into(),
segment: PathSegment::ArcTo {
end: kcmc::shared::Point3d {
x: LengthUnit(data.end[0]),
y: LengthUnit(data.end[1]),
z: LengthUnit(0.0),
},
interior: kcmc::shared::Point3d {
x: LengthUnit(data.interior[0]),
y: LengthUnit(data.interior[1]),
z: LengthUnit(0.0),
},
relative: false,
},
}),
)
.await?;
let start = [from.x, from.y];
let interior = [data.interior[0], data.interior[1]];
let end = [data.end[0], data.end[1]];
// compute the center of the circle since we do not have the value returned from the engine
let center = calculate_circle_center(start, interior, end);
// compute the radius since we do not have the value returned from the engine
// Pick any of the 3 points since they all lie along the circle
let sum_of_square_differences =
(center[0] - start[0] * center[0] - start[0]) + (center[1] - start[1] * center[1] - start[1]);
let radius = sum_of_square_differences.sqrt();
let current_path = Path::Arc {
base: BasePath {
from: from.into(),
to: data.end,
tag: tag.clone(),
geo_meta: GeoMeta {
id,
metadata: args.source_range.into(),
},
},
center,
radius,
};
let mut new_sketch = sketch.clone();
if let Some(tag) = &tag {
new_sketch.add_tag(tag, &current_path);
}
new_sketch.paths.push(current_path);
Ok(new_sketch)
}
/// Data to draw a tangential arc.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
#[ts(export)]
@ -1897,6 +2023,42 @@ async fn inner_tangential_arc_to_relative(
Ok(new_sketch)
}
// Calculate the center of 3 points
// To calculate the center of the 3 point circle 2 perpendicular lines are created
// These perpendicular lines will intersect at the center of the circle.
fn calculate_circle_center(p1: [f64; 2], p2: [f64; 2], p3: [f64; 2]) -> [f64; 2] {
// y2 - y1
let y_2_1 = p2[1] - p1[1];
// y3 - y2
let y_3_2 = p3[1] - p2[1];
// x2 - x1
let x_2_1 = p2[0] - p1[0];
// x3 - x2
let x_3_2 = p3[0] - p2[0];
// Slope of two perpendicular lines
let slope_a = y_2_1 / x_2_1;
let slope_b = y_3_2 / x_3_2;
// Values for line intersection
// y1 - y3
let y_1_3 = p1[1] - p3[1];
// x1 + x2
let x_1_2 = p1[0] + p2[0];
// x2 + x3
let x_2_3 = p2[0] + p3[0];
// y1 + y2
let y_1_2 = p1[1] + p2[1];
// Solve for the intersection of these two lines
let numerator = (slope_a * slope_b * y_1_3) + (slope_b * x_1_2) - (slope_a * x_2_3);
let x = numerator / (2.0 * (slope_b - slope_a));
let y = ((-1.0 / slope_a) * (x - (x_1_2 / 2.0))) + (y_1_2 / 2.0);
[x, y]
}
/// Data to draw a bezier curve.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
@ -2073,7 +2235,7 @@ mod tests {
use pretty_assertions::assert_eq;
use crate::{executor::TagIdentifier, std::sketch::PlaneData};
use crate::{executor::TagIdentifier, std::sketch::calculate_circle_center, std::sketch::PlaneData};
#[test]
fn test_deserialize_plane_data() {
@ -2144,4 +2306,11 @@ mod tests {
crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
);
}
#[test]
fn test_circle_center() {
let actual = calculate_circle_center([0.0, 0.0], [5.0, 5.0], [10.0, 0.0]);
assert_eq!(actual[0], 5.0);
assert_eq!(actual[1], 0.0);
}
}

View File

@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
#[ts(export)]
pub struct Uint(f64);
#[allow(dead_code)]
impl Uint {
pub fn new(value: f64) -> Self {
if value < 0.0 {

View File

@ -53,20 +53,6 @@ pub fn delta(from_angle: Angle, to_angle: Angle) -> Angle {
Angle::default()
}
pub fn clockwise_sign(points: &[Point2d]) -> i32 {
let mut sum = 0.0;
for i in 0..points.len() {
let current_point = points[i];
let next_point = points[(i + 1) % points.len()];
sum += (next_point.x - current_point.x) * (next_point.y + current_point.y);
}
if sum >= 0.0 {
1
} else {
-1
}
}
pub fn normalize_rad(angle: f64) -> f64 {
let draft = angle % (2.0 * PI);
if draft < 0.0 {
@ -76,32 +62,6 @@ pub fn normalize_rad(angle: f64) -> f64 {
}
}
/// Calculates the distance between two points.
///
/// # Examples
///
/// ```
/// use kcl_lib::executor::Point2d;
///
/// assert_eq!(
/// kcl_lib::std::utils::distance_between_points(Point2d::ZERO, Point2d { x: 0.0, y: 5.0 }),
/// 5.0
/// );
/// assert_eq!(
/// kcl_lib::std::utils::distance_between_points(Point2d::ZERO, Point2d { x: 3.0, y: 4.0 }),
/// 5.0
/// );
/// ```
#[allow(dead_code)]
pub fn distance_between_points(point_a: Point2d, point_b: Point2d) -> f64 {
let x1 = point_a.x;
let y1 = point_a.y;
let x2 = point_b.x;
let y2 = point_b.y;
((y2 - y1).powi(2) + (x2 - x1).powi(2)).sqrt()
}
pub fn calculate_intersection_of_two_lines(line1: &[Point2d; 2], line2_angle: f64, line2_point: Point2d) -> Point2d {
let line2_point_b = Point2d {
x: line2_point.x + f64::cos(line2_angle.to_radians()) * 10.0,
@ -563,6 +523,7 @@ pub struct TangentialArcInfoInput {
}
/// Structure to hold the output data from calculating tangential arc information.
#[allow(dead_code)]
pub struct TangentialArcInfoOutput {
/// The center point of the arc.
pub center: Coords2d,

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