Compare commits
45 Commits
pierremtb/
...
mike/multi
Author | SHA1 | Date | |
---|---|---|---|
319ffe6670 | |||
4b6bbbe2c5 | |||
6ff8addc8b | |||
da05c38b9e | |||
191b9b71fd | |||
05163fdded | |||
7ed26e21c6 | |||
c668d40efc | |||
f38c6b90b7 | |||
7bc8bae0ec | |||
3804aca27e | |||
b127680f2f | |||
b7de8e60cf | |||
058fccb5e1 | |||
00e97257ae | |||
aeb656d176 | |||
ac49ebd6e0 | |||
b40f03ad25 | |||
a8ad86e645 | |||
87f50cd5e9 | |||
0400e6228e | |||
26f150fd6c | |||
3049f405f5 | |||
53d40301dc | |||
671c01e36f | |||
e80151979b | |||
668e2afb99 | |||
548c664db0 | |||
d3a3f4410c | |||
22eb343171 | |||
f2cfa4d5cf | |||
3f1f40eeba | |||
ff2d161606 | |||
210c78029d | |||
e27840219b | |||
c943a3f192 | |||
6aa588f09f | |||
59a6333aad | |||
403f1507ae | |||
eac7b83504 | |||
667500d1b9 | |||
b15aac9f48 | |||
54153aa646 | |||
943cf21d34 | |||
5a6728c45a |
@ -1,3 +1,3 @@
|
|||||||
[codespell]
|
[codespell]
|
||||||
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast,ue,afterall
|
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast,ue,afterall,ket
|
||||||
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./src/lib/machine-api.d.ts
|
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./src/lib/machine-api.d.ts
|
||||||
|
3
.github/workflows/build-apps.yml
vendored
@ -165,7 +165,6 @@ jobs:
|
|||||||
- name: Build the app (release)
|
- name: Build the app (release)
|
||||||
if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }}
|
if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }}
|
||||||
env:
|
env:
|
||||||
PUBLISH_FOR_PULL_REQUEST: true
|
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
@ -173,7 +172,6 @@ jobs:
|
|||||||
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
|
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||||
CSC_FOR_PULL_REQUEST: true
|
|
||||||
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
|
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
|
||||||
run: yarn electron-builder --config --publish always
|
run: yarn electron-builder --config --publish always
|
||||||
|
|
||||||
@ -229,7 +227,6 @@ jobs:
|
|||||||
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
|
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||||
CSC_FOR_PULL_REQUEST: true
|
|
||||||
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
|
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
|
||||||
run: yarn electron-builder --config --publish always
|
run: yarn electron-builder --config --publish always
|
||||||
|
|
||||||
|
2
.github/workflows/cargo-test.yml
vendored
@ -71,7 +71,7 @@ jobs:
|
|||||||
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
|
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
|
||||||
RUST_MIN_STACK: 10485760000
|
RUST_MIN_STACK: 10485760000
|
||||||
- name: Upload to codecov.io
|
- name: Upload to codecov.io
|
||||||
uses: codecov/codecov-action@v4
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
token: ${{secrets.CODECOV_TOKEN}}
|
token: ${{secrets.CODECOV_TOKEN}}
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
|
@ -22,3 +22,5 @@ once fixed in engine will just start working here with no language changes.
|
|||||||
|
|
||||||
- **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple
|
- **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple
|
||||||
chamfer cases work currently.
|
chamfer cases work currently.
|
||||||
|
|
||||||
|
- **Appearance**: Changing the appearance on a loft does not work.
|
||||||
|
239
docs/kcl/appearance.md
Normal file
49
docs/kcl/atan2.md
Normal file
@ -19,6 +19,7 @@ layout: manual
|
|||||||
* [`angledLineThatIntersects`](kcl/angledLineThatIntersects)
|
* [`angledLineThatIntersects`](kcl/angledLineThatIntersects)
|
||||||
* [`angledLineToX`](kcl/angledLineToX)
|
* [`angledLineToX`](kcl/angledLineToX)
|
||||||
* [`angledLineToY`](kcl/angledLineToY)
|
* [`angledLineToY`](kcl/angledLineToY)
|
||||||
|
* [`appearance`](kcl/appearance)
|
||||||
* [`arc`](kcl/arc)
|
* [`arc`](kcl/arc)
|
||||||
* [`arcTo`](kcl/arcTo)
|
* [`arcTo`](kcl/arcTo)
|
||||||
* [`asin`](kcl/asin)
|
* [`asin`](kcl/asin)
|
||||||
@ -29,6 +30,7 @@ layout: manual
|
|||||||
* [`assertLessThan`](kcl/assertLessThan)
|
* [`assertLessThan`](kcl/assertLessThan)
|
||||||
* [`assertLessThanOrEq`](kcl/assertLessThanOrEq)
|
* [`assertLessThanOrEq`](kcl/assertLessThanOrEq)
|
||||||
* [`atan`](kcl/atan)
|
* [`atan`](kcl/atan)
|
||||||
|
* [`atan2`](kcl/atan2)
|
||||||
* [`bezierCurve`](kcl/bezierCurve)
|
* [`bezierCurve`](kcl/bezierCurve)
|
||||||
* [`ceil`](kcl/ceil)
|
* [`ceil`](kcl/ceil)
|
||||||
* [`chamfer`](kcl/chamfer)
|
* [`chamfer`](kcl/chamfer)
|
||||||
@ -101,6 +103,7 @@ layout: manual
|
|||||||
* [`startProfileAt`](kcl/startProfileAt)
|
* [`startProfileAt`](kcl/startProfileAt)
|
||||||
* [`startSketchAt`](kcl/startSketchAt)
|
* [`startSketchAt`](kcl/startSketchAt)
|
||||||
* [`startSketchOn`](kcl/startSketchOn)
|
* [`startSketchOn`](kcl/startSketchOn)
|
||||||
|
* [`sweep`](kcl/sweep)
|
||||||
* [`tan`](kcl/tan)
|
* [`tan`](kcl/tan)
|
||||||
* [`tangentToEnd`](kcl/tangentToEnd)
|
* [`tangentToEnd`](kcl/tangentToEnd)
|
||||||
* [`tangentialArc`](kcl/tangentialArc)
|
* [`tangentialArc`](kcl/tangentialArc)
|
||||||
|
@ -45,7 +45,7 @@ circles = map([1..3], drawCircle)
|
|||||||
```js
|
```js
|
||||||
r = 10 // radius
|
r = 10 // radius
|
||||||
// Call `map`, using an anonymous function instead of a named one.
|
// Call `map`, using an anonymous function instead of a named one.
|
||||||
circles = map([1..3], (id) {
|
circles = map([1..3], fn(id) {
|
||||||
return startSketchOn("XY")
|
return startSketchOn("XY")
|
||||||
|> circle({ center = [id * 2 * r, 0], radius = r }, %)
|
|> circle({ center = [id * 2 * r, 0], radius = r }, %)
|
||||||
})
|
})
|
||||||
|
@ -43,7 +43,7 @@ fn sum(arr) {
|
|||||||
|
|
||||||
/* The above is basically like this pseudo-code:
|
/* The above is basically like this pseudo-code:
|
||||||
fn sum(arr):
|
fn sum(arr):
|
||||||
let sumSoFar = 0
|
sumSoFar = 0
|
||||||
for i in arr:
|
for i in arr:
|
||||||
sumSoFar = add(sumSoFar, i)
|
sumSoFar = add(sumSoFar, i)
|
||||||
return sumSoFar */
|
return sumSoFar */
|
||||||
@ -61,7 +61,7 @@ assertEqual(sum([1, 2, 3]), 6, 0.00001, "1 + 2 + 3 summed is 6")
|
|||||||
// an anonymous `add` function as its parameter, instead of declaring a
|
// an anonymous `add` function as its parameter, instead of declaring a
|
||||||
// named function outside.
|
// named function outside.
|
||||||
arr = [1, 2, 3]
|
arr = [1, 2, 3]
|
||||||
sum = reduce(arr, 0, (i, result_so_far) {
|
sum = reduce(arr, 0, fn(i, result_so_far) {
|
||||||
return i + result_so_far
|
return i + result_so_far
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -84,7 +84,7 @@ fn decagon(radius) {
|
|||||||
// Use a `reduce` to draw the remaining decagon sides.
|
// Use a `reduce` to draw the remaining decagon sides.
|
||||||
// For each number in the array 1..10, run the given function,
|
// For each number in the array 1..10, run the given function,
|
||||||
// which takes a partially-sketched decagon and adds one more edge to it.
|
// which takes a partially-sketched decagon and adds one more edge to it.
|
||||||
fullDecagon = reduce([1..10], startOfDecagonSketch, (i, partialDecagon) {
|
fullDecagon = reduce([1..10], startOfDecagonSketch, fn(i, partialDecagon) {
|
||||||
// Draw one edge of the decagon.
|
// Draw one edge of the decagon.
|
||||||
x = cos(stepAngle * i) * radius
|
x = cos(stepAngle * i) * radius
|
||||||
y = sin(stepAngle * i) * radius
|
y = sin(stepAngle * i) * radius
|
||||||
@ -96,14 +96,14 @@ fn decagon(radius) {
|
|||||||
|
|
||||||
/* The `decagon` above is basically like this pseudo-code:
|
/* The `decagon` above is basically like this pseudo-code:
|
||||||
fn decagon(radius):
|
fn decagon(radius):
|
||||||
let stepAngle = (1/10) * tau()
|
stepAngle = (1/10) * tau()
|
||||||
let startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)])
|
startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)])
|
||||||
|
|
||||||
// Here's the reduce part.
|
// Here's the reduce part.
|
||||||
let partialDecagon = startOfDecagonSketch
|
partialDecagon = startOfDecagonSketch
|
||||||
for i in [1..10]:
|
for i in [1..10]:
|
||||||
let x = cos(stepAngle * i) * radius
|
x = cos(stepAngle * i) * radius
|
||||||
let y = sin(stepAngle * i) * radius
|
y = sin(stepAngle * i) * radius
|
||||||
partialDecagon = lineTo([x, y], partialDecagon)
|
partialDecagon = lineTo([x, y], partialDecagon)
|
||||||
fullDecagon = partialDecagon // it's now full
|
fullDecagon = partialDecagon // it's now full
|
||||||
return fullDecagon */
|
return fullDecagon */
|
||||||
|
7206
docs/kcl/std.json
55
docs/kcl/sweep.md
Normal file
23
docs/kcl/types/AppearanceData.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
title: "AppearanceData"
|
||||||
|
excerpt: "Data for appearance."
|
||||||
|
layout: manual
|
||||||
|
---
|
||||||
|
|
||||||
|
Data for appearance.
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Type | Description | Required |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| `color` |`string`| Color of the new material, a hex string like "#ff0000". | No |
|
||||||
|
| `metalness` |`number` (**maximum:** 100.0)| Metalness of the new material, a percentage like 95.7. | No |
|
||||||
|
| `roughness` |`number` (**maximum:** 100.0)| Roughness of the new material, a percentage like 95.7. | No |
|
||||||
|
|
||||||
|
|
@ -12,5 +12,10 @@ KCL value for an optional parameter which was not given an argument. (remember,
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Type | Description | Required |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
|
||||||
|
|
||||||
|
|
||||||
|
23
docs/kcl/types/SweepData.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
title: "SweepData"
|
||||||
|
excerpt: "Data for a sweep."
|
||||||
|
layout: manual
|
||||||
|
---
|
||||||
|
|
||||||
|
Data for a sweep.
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Type | Description | Required |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| `path` |[`Sketch`](/docs/kcl/types/Sketch)| The path to sweep along. | No |
|
||||||
|
| `sectional` |`boolean`| If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components. | No |
|
||||||
|
| `tolerance` |`number`| Tolerance for the sweep operation. | No |
|
||||||
|
|
||||||
|
|
@ -7,6 +7,7 @@ export class ToolbarFixture {
|
|||||||
|
|
||||||
extrudeButton!: Locator
|
extrudeButton!: Locator
|
||||||
loftButton!: Locator
|
loftButton!: Locator
|
||||||
|
shellButton!: Locator
|
||||||
offsetPlaneButton!: Locator
|
offsetPlaneButton!: Locator
|
||||||
startSketchBtn!: Locator
|
startSketchBtn!: Locator
|
||||||
lineBtn!: Locator
|
lineBtn!: Locator
|
||||||
@ -28,6 +29,7 @@ export class ToolbarFixture {
|
|||||||
this.page = page
|
this.page = page
|
||||||
this.extrudeButton = page.getByTestId('extrude')
|
this.extrudeButton = page.getByTestId('extrude')
|
||||||
this.loftButton = page.getByTestId('loft')
|
this.loftButton = page.getByTestId('loft')
|
||||||
|
this.shellButton = page.getByTestId('shell')
|
||||||
this.offsetPlaneButton = page.getByTestId('plane-offset')
|
this.offsetPlaneButton = page.getByTestId('plane-offset')
|
||||||
this.startSketchBtn = page.getByTestId('sketch')
|
this.startSketchBtn = page.getByTestId('sketch')
|
||||||
this.lineBtn = page.getByTestId('line')
|
this.lineBtn = page.getByTestId('line')
|
||||||
|
@ -768,3 +768,168 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const shellPointAndClickCapCases = [
|
||||||
|
{ shouldPreselect: true },
|
||||||
|
{ shouldPreselect: false },
|
||||||
|
]
|
||||||
|
shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
|
||||||
|
test(`Shell point-and-click cap (preselected sketches: ${shouldPreselect})`, async ({
|
||||||
|
app,
|
||||||
|
scene,
|
||||||
|
editor,
|
||||||
|
toolbar,
|
||||||
|
cmdBar,
|
||||||
|
}) => {
|
||||||
|
const initialCode = `sketch001 = startSketchOn('XZ')
|
||||||
|
|> circle({ center = [0, 0], radius = 30 }, %)
|
||||||
|
extrude001 = extrude(30, sketch001)
|
||||||
|
`
|
||||||
|
await app.initialise(initialCode)
|
||||||
|
|
||||||
|
// One dumb hardcoded screen pixel value
|
||||||
|
const testPoint = { x: 575, y: 200 }
|
||||||
|
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||||
|
const shellDeclaration =
|
||||||
|
"shell001 = shell({ faces = ['end'], thickness = 5 }, extrude001)"
|
||||||
|
|
||||||
|
await test.step(`Look for the grey of the shape`, async () => {
|
||||||
|
await scene.expectPixelColor([127, 127, 127], testPoint, 15)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!shouldPreselect) {
|
||||||
|
await test.step(`Go through the command bar flow without preselected faces`, async () => {
|
||||||
|
await toolbar.shellButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'selection',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '',
|
||||||
|
Thickness: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'selection',
|
||||||
|
commandName: 'Shell',
|
||||||
|
})
|
||||||
|
await clickOnCap()
|
||||||
|
await app.page.waitForTimeout(500)
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '1 cap',
|
||||||
|
Thickness: '5',
|
||||||
|
},
|
||||||
|
commandName: 'Shell',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await test.step(`Preselect the cap`, async () => {
|
||||||
|
await clickOnCap()
|
||||||
|
await app.page.waitForTimeout(500)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => {
|
||||||
|
await toolbar.shellButton.click()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '1 cap',
|
||||||
|
Thickness: '5',
|
||||||
|
},
|
||||||
|
commandName: 'Shell',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
||||||
|
await editor.expectEditor.toContain(shellDeclaration)
|
||||||
|
await editor.expectState({
|
||||||
|
diagnostics: [],
|
||||||
|
activeLines: [shellDeclaration],
|
||||||
|
highlightedCode: '',
|
||||||
|
})
|
||||||
|
await scene.expectPixelColor([146, 146, 146], testPoint, 15)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Shell point-and-click wall', async ({
|
||||||
|
app,
|
||||||
|
page,
|
||||||
|
scene,
|
||||||
|
editor,
|
||||||
|
toolbar,
|
||||||
|
cmdBar,
|
||||||
|
}) => {
|
||||||
|
const initialCode = `sketch001 = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-20, 20], %)
|
||||||
|
|> xLine(40, %)
|
||||||
|
|> yLine(-60, %)
|
||||||
|
|> xLine(-40, %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)
|
||||||
|
extrude001 = extrude(40, sketch001)
|
||||||
|
`
|
||||||
|
await app.initialise(initialCode)
|
||||||
|
|
||||||
|
// One dumb hardcoded screen pixel value
|
||||||
|
const testPoint = { x: 580, y: 180 }
|
||||||
|
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||||
|
const [clickOnWall] = scene.makeMouseHelpers(testPoint.x, testPoint.y + 70)
|
||||||
|
const mutatedCode = 'xLine(-40, %, $seg01)'
|
||||||
|
const shellDeclaration =
|
||||||
|
"shell001 = shell({ faces = ['end', seg01], thickness = 5}, extrude001)"
|
||||||
|
const formattedOutLastLine = '}, extrude001)'
|
||||||
|
|
||||||
|
await test.step(`Look for the grey of the shape`, async () => {
|
||||||
|
await scene.expectPixelColor([99, 99, 99], testPoint, 15)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Go through the command bar flow, selecting a wall and keeping default thickness`, async () => {
|
||||||
|
await toolbar.shellButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'selection',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '',
|
||||||
|
Thickness: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'selection',
|
||||||
|
commandName: 'Shell',
|
||||||
|
})
|
||||||
|
await clickOnCap()
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
await clickOnWall()
|
||||||
|
await app.page.waitForTimeout(500)
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '1 cap, 1 face',
|
||||||
|
Thickness: '5',
|
||||||
|
},
|
||||||
|
commandName: 'Shell',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
||||||
|
await editor.expectEditor.toContain(mutatedCode)
|
||||||
|
await editor.expectEditor.toContain(shellDeclaration)
|
||||||
|
await editor.expectState({
|
||||||
|
diagnostics: [],
|
||||||
|
activeLines: [formattedOutLastLine],
|
||||||
|
highlightedCode: '',
|
||||||
|
})
|
||||||
|
await scene.expectPixelColor([49, 49, 49], testPoint, 15)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
@ -950,7 +950,75 @@ test(
|
|||||||
|
|
||||||
test.describe('Grid visibility', { tag: '@snapshot' }, () => {
|
test.describe('Grid visibility', { tag: '@snapshot' }, () => {
|
||||||
// FIXME: Skip on macos its being weird.
|
// FIXME: Skip on macos its being weird.
|
||||||
test.skip(process.platform === 'darwin', 'Skip on macos')
|
// test.skip(process.platform === 'darwin', 'Skip on macos')
|
||||||
|
|
||||||
|
test('Grid turned off to on via command bar', async ({ page }) => {
|
||||||
|
const u = await getUtils(page)
|
||||||
|
const stream = page.getByTestId('stream')
|
||||||
|
const mask = [
|
||||||
|
page.locator('#app-header'),
|
||||||
|
page.locator('#sidebar-top-ribbon'),
|
||||||
|
page.locator('#sidebar-bottom-ribbon'),
|
||||||
|
]
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
await u.openDebugPanel()
|
||||||
|
// wait for execution done
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-message-type="execution-done"]')
|
||||||
|
).toHaveCount(1)
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
await u.closeKclCodePanel()
|
||||||
|
// TODO: Find a way to truly know that the objects have finished
|
||||||
|
// rendering, because an execution-done message is not sufficient.
|
||||||
|
await page.waitForTimeout(1000)
|
||||||
|
|
||||||
|
// Open the command bar.
|
||||||
|
await page
|
||||||
|
.getByRole('button', { name: 'Commands', exact: false })
|
||||||
|
.or(page.getByRole('button', { name: '⌘K' }))
|
||||||
|
.click()
|
||||||
|
const commandName = 'show scale grid'
|
||||||
|
const commandOption = page.getByRole('option', {
|
||||||
|
name: commandName,
|
||||||
|
exact: false,
|
||||||
|
})
|
||||||
|
const cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||||
|
// This selector changes after we set the setting
|
||||||
|
await cmdSearchBar.fill(commandName)
|
||||||
|
await expect(commandOption).toBeVisible()
|
||||||
|
await commandOption.click()
|
||||||
|
|
||||||
|
const toggleInput = page.getByPlaceholder('Off')
|
||||||
|
await expect(toggleInput).toBeVisible()
|
||||||
|
await expect(toggleInput).toBeFocused()
|
||||||
|
|
||||||
|
// Select On
|
||||||
|
await page.keyboard.press('ArrowDown')
|
||||||
|
await expect(page.getByRole('option', { name: 'Off' })).toHaveAttribute(
|
||||||
|
'data-headlessui-state',
|
||||||
|
'active selected'
|
||||||
|
)
|
||||||
|
await page.keyboard.press('ArrowUp')
|
||||||
|
await expect(page.getByRole('option', { name: 'On' })).toHaveAttribute(
|
||||||
|
'data-headlessui-state',
|
||||||
|
'active'
|
||||||
|
)
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
|
||||||
|
// Check the toast appeared
|
||||||
|
await expect(
|
||||||
|
page.getByText(`Set show scale grid to "true" as a user default`)
|
||||||
|
).toBeVisible()
|
||||||
|
|
||||||
|
await expect(stream).toHaveScreenshot({
|
||||||
|
maxDiffPixels: 100,
|
||||||
|
mask,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test('Grid turned off', async ({ page }) => {
|
test('Grid turned off', async ({ page }) => {
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
@ -1096,3 +1164,109 @@ test.fixme('theme persists', async ({ page, context }) => {
|
|||||||
maxDiffPixels: 100,
|
maxDiffPixels: 100,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test.describe('code color goober', { tag: '@snapshot' }, () => {
|
||||||
|
test('code color goober', async ({ page, context }) => {
|
||||||
|
const u = await getUtils(page)
|
||||||
|
await context.addInitScript(async () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`// Create a pipe using a sweep.
|
||||||
|
|
||||||
|
// Create a path for the sweep.
|
||||||
|
sweepPath = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([0.05, 0.05], %)
|
||||||
|
|> line([0, 7], %)
|
||||||
|
|> tangentialArc({ offset = 90, radius = 5 }, %)
|
||||||
|
|> line([-3, 0], %)
|
||||||
|
|> tangentialArc({ offset = -90, radius = 5 }, %)
|
||||||
|
|> line([0, 7], %)
|
||||||
|
|
||||||
|
sweepSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([2, 0], %)
|
||||||
|
|> arc({
|
||||||
|
angleEnd = 360,
|
||||||
|
angleStart = 0,
|
||||||
|
radius = 2
|
||||||
|
}, %)
|
||||||
|
|> sweep({
|
||||||
|
path = sweepPath,
|
||||||
|
}, %)
|
||||||
|
|> appearance({
|
||||||
|
color = "#bb00ff",
|
||||||
|
metalness = 90,
|
||||||
|
roughness = 90
|
||||||
|
}, %)
|
||||||
|
`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1200, height: 1000 })
|
||||||
|
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
await u.clearAndCloseDebugPanel()
|
||||||
|
|
||||||
|
await expect(page, 'expect small color widget').toHaveScreenshot({
|
||||||
|
maxDiffPixels: 100,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('code color goober opening window', async ({ page, context }) => {
|
||||||
|
const u = await getUtils(page)
|
||||||
|
await context.addInitScript(async () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`// Create a pipe using a sweep.
|
||||||
|
|
||||||
|
// Create a path for the sweep.
|
||||||
|
sweepPath = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([0.05, 0.05], %)
|
||||||
|
|> line([0, 7], %)
|
||||||
|
|> tangentialArc({ offset = 90, radius = 5 }, %)
|
||||||
|
|> line([-3, 0], %)
|
||||||
|
|> tangentialArc({ offset = -90, radius = 5 }, %)
|
||||||
|
|> line([0, 7], %)
|
||||||
|
|
||||||
|
sweepSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([2, 0], %)
|
||||||
|
|> arc({
|
||||||
|
angleEnd = 360,
|
||||||
|
angleStart = 0,
|
||||||
|
radius = 2
|
||||||
|
}, %)
|
||||||
|
|> sweep({
|
||||||
|
path = sweepPath,
|
||||||
|
}, %)
|
||||||
|
|> appearance({
|
||||||
|
color = "#bb00ff",
|
||||||
|
metalness = 90,
|
||||||
|
roughness = 90
|
||||||
|
}, %)
|
||||||
|
`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1200, height: 1000 })
|
||||||
|
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
await u.clearAndCloseDebugPanel()
|
||||||
|
|
||||||
|
await expect(page.locator('.cm-css-color-picker-wrapper')).toBeVisible()
|
||||||
|
|
||||||
|
// Click the color widget
|
||||||
|
await page.locator('.cm-css-color-picker-wrapper input').click()
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page,
|
||||||
|
'expect small color widget to have window open'
|
||||||
|
).toHaveScreenshot({
|
||||||
|
maxDiffPixels: 100,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 52 KiB |
After Width: | Height: | Size: 54 KiB |
After Width: | Height: | Size: 144 KiB |
After Width: | Height: | Size: 130 KiB |
After Width: | Height: | Size: 139 KiB |
After Width: | Height: | Size: 124 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
@ -14,7 +14,7 @@ export const TEST_SETTINGS = {
|
|||||||
},
|
},
|
||||||
modeling: {
|
modeling: {
|
||||||
defaultUnit: 'in',
|
defaultUnit: 'in',
|
||||||
mouseControls: 'KittyCAD',
|
mouseControls: 'Zoo',
|
||||||
cameraProjection: 'perspective',
|
cameraProjection: 'perspective',
|
||||||
showDebugPanel: true,
|
showDebugPanel: true,
|
||||||
},
|
},
|
||||||
|
@ -479,4 +479,26 @@ test.describe('Testing Camera Movement', () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Right-click opens context menu when not dragged', async ({ page }) => {
|
||||||
|
const u = await getUtils(page)
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
await test.step(`The menu should not show if we drag the mouse`, async () => {
|
||||||
|
await page.mouse.move(900, 200)
|
||||||
|
await page.mouse.down({ button: 'right' })
|
||||||
|
await page.mouse.move(900, 300)
|
||||||
|
await page.mouse.up({ button: 'right' })
|
||||||
|
|
||||||
|
await expect(page.getByTestId('view-controls-menu')).not.toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`The menu should show if we don't drag the mouse`, async () => {
|
||||||
|
await page.mouse.move(900, 200)
|
||||||
|
await page.mouse.down({ button: 'right' })
|
||||||
|
await page.mouse.up({ button: 'right' })
|
||||||
|
|
||||||
|
await expect(page.getByTestId('view-controls-menu')).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -26,7 +26,17 @@ test.describe('Testing constraints', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
// constants and locators
|
||||||
|
const lengthValue = {
|
||||||
|
old: '20',
|
||||||
|
new: '25',
|
||||||
|
}
|
||||||
|
const cmdBarKclInput = page
|
||||||
|
.getByTestId('cmd-bar-arg-value')
|
||||||
|
.getByRole('textbox')
|
||||||
|
const cmdBarSubmitButton = page.getByRole('button', {
|
||||||
|
name: 'arrow right Continue',
|
||||||
|
})
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
@ -36,26 +46,26 @@ test.describe('Testing constraints', () => {
|
|||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
// Click the line of code for line.
|
// Click the line of code for line.
|
||||||
await page.getByText(`line([0, 20], %)`).click() // TODO remove this and reinstate // await topHorzSegmentClick()
|
// TODO remove this and reinstate `await topHorzSegmentClick()`
|
||||||
|
await page.getByText(`line([0, ${lengthValue.old}], %)`).click()
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
// enter sketch again
|
// enter sketch again
|
||||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||||
await page.waitForTimeout(500) // wait for animation
|
await page.waitForTimeout(500) // wait for animation
|
||||||
|
|
||||||
const startXPx = 500
|
|
||||||
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
|
|
||||||
await page.keyboard.down('Shift')
|
|
||||||
await page.mouse.click(834, 244)
|
|
||||||
await page.keyboard.up('Shift')
|
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByRole('button', { name: 'dimension Length', exact: true })
|
.getByRole('button', { name: 'dimension Length', exact: true })
|
||||||
.click()
|
.click()
|
||||||
await page.getByText('Add constraining value').click()
|
await expect(cmdBarKclInput).toHaveText('20')
|
||||||
|
await cmdBarKclInput.fill(lengthValue.new)
|
||||||
|
await expect(
|
||||||
|
page.getByText(`Can't calculate`),
|
||||||
|
`Something went wrong with the KCL expression evaluation`
|
||||||
|
).not.toBeVisible()
|
||||||
|
await cmdBarSubmitButton.click()
|
||||||
|
|
||||||
await expect(page.locator('.cm-content')).toHaveText(
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
`length001 = 20sketch001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> angledLine([90, length001], %) |> xLine(-20, %)`
|
`length001 = ${lengthValue.new}sketch001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> angledLine([90, length001], %) |> xLine(-20, %)`
|
||||||
)
|
)
|
||||||
|
|
||||||
// Make sure we didn't pop out of sketch mode.
|
// Make sure we didn't pop out of sketch mode.
|
||||||
@ -66,7 +76,6 @@ test.describe('Testing constraints', () => {
|
|||||||
await page.waitForTimeout(500) // wait for animation
|
await page.waitForTimeout(500) // wait for animation
|
||||||
|
|
||||||
// Exit sketch
|
// Exit sketch
|
||||||
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
|
|
||||||
await page.keyboard.press('Escape')
|
await page.keyboard.press('Escape')
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Exit Sketch' })
|
page.getByRole('button', { name: 'Exit Sketch' })
|
||||||
@ -524,7 +533,7 @@ part002 = startSketchOn('XZ')
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
test.describe('Test Angle/Length constraint single selection', () => {
|
test.describe('Test Angle constraint single selection', () => {
|
||||||
const cases = [
|
const cases = [
|
||||||
{
|
{
|
||||||
testName: 'Angle - Add variable',
|
testName: 'Angle - Add variable',
|
||||||
@ -538,18 +547,6 @@ part002 = startSketchOn('XZ')
|
|||||||
constraint: 'angle',
|
constraint: 'angle',
|
||||||
value: '83, 78.33',
|
value: '83, 78.33',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
testName: 'Length - Add variable',
|
|
||||||
addVariable: true,
|
|
||||||
constraint: 'length',
|
|
||||||
value: '83, length001',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
testName: 'Length - No variable',
|
|
||||||
addVariable: false,
|
|
||||||
constraint: 'length',
|
|
||||||
value: '83, 78.33',
|
|
||||||
},
|
|
||||||
] as const
|
] as const
|
||||||
for (const { testName, addVariable, value, constraint } of cases) {
|
for (const { testName, addVariable, value, constraint } of cases) {
|
||||||
test(`${testName}`, async ({ page }) => {
|
test(`${testName}`, async ({ page }) => {
|
||||||
@ -608,6 +605,90 @@ part002 = startSketchOn('XZ')
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
test.describe('Test Length constraint single selection', () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
testName: 'Length - Add variable',
|
||||||
|
addVariable: true,
|
||||||
|
constraint: 'length',
|
||||||
|
value: '83, length001',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: 'Length - No variable',
|
||||||
|
addVariable: false,
|
||||||
|
constraint: 'length',
|
||||||
|
value: '83, 78.33',
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
for (const { testName, addVariable, value, constraint } of cases) {
|
||||||
|
test(`${testName}`, async ({ page }) => {
|
||||||
|
// constants and locators
|
||||||
|
const cmdBarKclInput = page
|
||||||
|
.getByTestId('cmd-bar-arg-value')
|
||||||
|
.getByRole('textbox')
|
||||||
|
const cmdBarKclVariableNameInput =
|
||||||
|
page.getByPlaceholder('Variable name')
|
||||||
|
const cmdBarSubmitButton = page.getByRole('button', {
|
||||||
|
name: 'arrow right Continue',
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.addInitScript(async () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`yo = 5
|
||||||
|
part001 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([-7.54, -26.74], %)
|
||||||
|
|> line([74.36, 130.4], %)
|
||||||
|
|> line([78.92, -120.11], %)
|
||||||
|
|> line([9.16, 77.79], %)
|
||||||
|
|> line([51.19, 48.97], %)
|
||||||
|
part002 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([299.05, 231.45], %)
|
||||||
|
|> xLine(-425.34, %, $seg_what)
|
||||||
|
|> yLine(-264.06, %)
|
||||||
|
|> xLine(segLen(seg_what), %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
const u = await getUtils(page)
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
await page.getByText('line([74.36, 130.4], %)').click()
|
||||||
|
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||||
|
|
||||||
|
const line3 = await u.getSegmentBodyCoords(
|
||||||
|
`[data-overlay-index="${2}"]`
|
||||||
|
)
|
||||||
|
|
||||||
|
await page.mouse.click(line3.x, line3.y)
|
||||||
|
await page
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'Length: open menu',
|
||||||
|
})
|
||||||
|
.click()
|
||||||
|
await page.getByTestId('dropdown-constraint-' + constraint).click()
|
||||||
|
|
||||||
|
if (!addVariable) {
|
||||||
|
await test.step(`Clear the variable input`, async () => {
|
||||||
|
await cmdBarKclVariableNameInput.clear()
|
||||||
|
await cmdBarKclVariableNameInput.press('Backspace')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await expect(cmdBarKclInput).toHaveText('78.33')
|
||||||
|
await cmdBarSubmitButton.click()
|
||||||
|
|
||||||
|
const changedCode = `|> angledLine([${value}], %)`
|
||||||
|
await expect(page.locator('.cm-content')).toContainText(changedCode)
|
||||||
|
// checking active assures the cursor is where it should be
|
||||||
|
await expect(page.locator('.cm-activeLine')).toHaveText(changedCode)
|
||||||
|
|
||||||
|
// checking the count of the overlays is a good proxy check that the client sketch scene is in a good state
|
||||||
|
await expect(page.getByTestId('segment-overlay')).toHaveCount(4)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
test.describe('Many segments - no modal constraints', () => {
|
test.describe('Many segments - no modal constraints', () => {
|
||||||
const cases = [
|
const cases = [
|
||||||
{
|
{
|
||||||
@ -868,6 +949,15 @@ part002 = startSketchOn('XZ')
|
|||||||
|> line([3.13, -2.4], %)`
|
|> line([3.13, -2.4], %)`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// constants and locators
|
||||||
|
const cmdBarKclInput = page
|
||||||
|
.getByTestId('cmd-bar-arg-value')
|
||||||
|
.getByRole('textbox')
|
||||||
|
const cmdBarSubmitButton = page.getByRole('button', {
|
||||||
|
name: 'arrow right Continue',
|
||||||
|
})
|
||||||
|
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
|
||||||
@ -928,8 +1018,8 @@ part002 = startSketchOn('XZ')
|
|||||||
// await page.getByRole('button', { name: 'length', exact: true }).click()
|
// await page.getByRole('button', { name: 'length', exact: true }).click()
|
||||||
await page.getByTestId('dropdown-constraint-length').click()
|
await page.getByTestId('dropdown-constraint-length').click()
|
||||||
|
|
||||||
await page.getByLabel('length Value').fill('10')
|
await cmdBarKclInput.fill('10')
|
||||||
await page.getByRole('button', { name: 'Add constraining value' }).click()
|
await cmdBarSubmitButton.click()
|
||||||
|
|
||||||
activeLinesContent = await page.locator('.cm-activeLine').all()
|
activeLinesContent = await page.locator('.cm-activeLine').all()
|
||||||
await expect(activeLinesContent[0]).toHaveText(`|> xLine(length001, %)`)
|
await expect(activeLinesContent[0]).toHaveText(`|> xLine(length001, %)`)
|
||||||
|
@ -91,7 +91,14 @@ test.describe('Testing segment overlays', () => {
|
|||||||
await page.getByTestId('constraint-symbol-popover').count()
|
await page.getByTestId('constraint-symbol-popover').count()
|
||||||
).toBeGreaterThan(0)
|
).toBeGreaterThan(0)
|
||||||
await unconstrainedLocator.click()
|
await unconstrainedLocator.click()
|
||||||
await page.getByText('Add variable').click()
|
await expect(
|
||||||
|
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
|
||||||
|
).toBeFocused()
|
||||||
|
await page
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'arrow right Continue',
|
||||||
|
})
|
||||||
|
.click()
|
||||||
await expect(page.locator('.cm-content')).toContainText(expectFinal)
|
await expect(page.locator('.cm-content')).toContainText(expectFinal)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,7 +158,14 @@ test.describe('Testing segment overlays', () => {
|
|||||||
await page.getByTestId('constraint-symbol-popover').count()
|
await page.getByTestId('constraint-symbol-popover').count()
|
||||||
).toBeGreaterThan(0)
|
).toBeGreaterThan(0)
|
||||||
await unconstrainedLocator.click()
|
await unconstrainedLocator.click()
|
||||||
await page.getByText('Add variable').click()
|
await expect(
|
||||||
|
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
|
||||||
|
).toBeFocused()
|
||||||
|
await page
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'arrow right Continue',
|
||||||
|
})
|
||||||
|
.click()
|
||||||
await expect(page.locator('.cm-content')).toContainText(
|
await expect(page.locator('.cm-content')).toContainText(
|
||||||
expectAfterUnconstrained
|
expectAfterUnconstrained
|
||||||
)
|
)
|
||||||
|
@ -1,20 +1,9 @@
|
|||||||
import type { ForgeConfig } from '@electron-forge/shared-types'
|
import type { ForgeConfig } from '@electron-forge/shared-types'
|
||||||
import { MakerSquirrel } from '@electron-forge/maker-squirrel'
|
|
||||||
import { MakerZIP } from '@electron-forge/maker-zip'
|
|
||||||
import { MakerDeb } from '@electron-forge/maker-deb'
|
|
||||||
import { MakerRpm } from '@electron-forge/maker-rpm'
|
|
||||||
import { VitePlugin } from '@electron-forge/plugin-vite'
|
import { VitePlugin } from '@electron-forge/plugin-vite'
|
||||||
import { MakerWix, MakerWixConfig } from '@electron-forge/maker-wix'
|
|
||||||
import { FusesPlugin } from '@electron-forge/plugin-fuses'
|
import { FusesPlugin } from '@electron-forge/plugin-fuses'
|
||||||
import { FuseV1Options, FuseVersion } from '@electron/fuses'
|
import { FuseV1Options, FuseVersion } from '@electron/fuses'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
interface ExtendedMakerWixConfig extends MakerWixConfig {
|
|
||||||
// see https://github.com/electron/forge/issues/3673
|
|
||||||
// this is an undocumented property of electron-wix-msi
|
|
||||||
associateExtensions?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const rootDir = process.cwd()
|
const rootDir = process.cwd()
|
||||||
|
|
||||||
const config: ForgeConfig = {
|
const config: ForgeConfig = {
|
||||||
@ -39,26 +28,7 @@ const config: ForgeConfig = {
|
|||||||
extendInfo: 'Info.plist', // Information for file associations.
|
extendInfo: 'Info.plist', // Information for file associations.
|
||||||
},
|
},
|
||||||
rebuildConfig: {},
|
rebuildConfig: {},
|
||||||
makers: [
|
makers: [],
|
||||||
new MakerSquirrel({
|
|
||||||
setupIcon: path.resolve(rootDir, 'assets', 'icon.ico'),
|
|
||||||
}),
|
|
||||||
new MakerWix({
|
|
||||||
icon: path.resolve(rootDir, 'assets', 'icon.ico'),
|
|
||||||
associateExtensions: 'kcl',
|
|
||||||
} as ExtendedMakerWixConfig),
|
|
||||||
new MakerZIP({}, ['darwin']),
|
|
||||||
new MakerRpm({
|
|
||||||
options: {
|
|
||||||
icon: path.resolve(rootDir, 'assets', 'icon.png'),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
new MakerDeb({
|
|
||||||
options: {
|
|
||||||
icon: path.resolve(rootDir, 'assets', 'icon.png'),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
plugins: [
|
plugins: [
|
||||||
new VitePlugin({
|
new VitePlugin({
|
||||||
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
|
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
|
||||||
|
26
package.json
@ -39,7 +39,6 @@
|
|||||||
"chokidar": "^4.0.1",
|
"chokidar": "^4.0.1",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"decamelize": "^6.0.0",
|
"decamelize": "^6.0.0",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
|
||||||
"electron-updater": "6.3.0",
|
"electron-updater": "6.3.0",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"html2canvas-pro": "^1.5.8",
|
"html2canvas-pro": "^1.5.8",
|
||||||
@ -69,7 +68,7 @@
|
|||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite --port=3000 --host=0.0.0.0",
|
||||||
"start:prod": "vite preview --port=3000",
|
"start:prod": "vite preview --port=3000",
|
||||||
"serve": "vite serve --port=3000",
|
"serve": "vite serve --port=3000",
|
||||||
"build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build",
|
"build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build",
|
||||||
@ -104,8 +103,6 @@
|
|||||||
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
|
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
|
||||||
"tron:start": "electron-forge start",
|
"tron:start": "electron-forge start",
|
||||||
"tron:package": "electron-forge package",
|
"tron:package": "electron-forge package",
|
||||||
"tron:make": "electron-forge make",
|
|
||||||
"tron:publish": "electron-forge publish",
|
|
||||||
"tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron",
|
"tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron",
|
||||||
"tronb:vite": "vite build -c vite.main.config.ts && vite build -c vite.preload.config.ts && vite build -c vite.renderer.config.ts",
|
"tronb:vite": "vite build -c vite.main.config.ts && vite build -c vite.preload.config.ts && vite build -c vite.renderer.config.ts",
|
||||||
"tronb:package": "electron-builder --config electron-builder.yml",
|
"tronb:package": "electron-builder --config electron-builder.yml",
|
||||||
@ -148,17 +145,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-env": "^7.25.4",
|
"@babel/preset-env": "^7.25.4",
|
||||||
"@electron-forge/cli": "^7.4.0",
|
"@electron-forge/cli": "7.4.0",
|
||||||
"@electron-forge/maker-deb": "^7.4.0",
|
"@electron-forge/plugin-fuses": "7.4.0",
|
||||||
"@electron-forge/maker-rpm": "^7.4.0",
|
"@electron-forge/plugin-vite": "7.4.0",
|
||||||
"@electron-forge/maker-squirrel": "^7.4.0",
|
"@electron/fuses": "1.8.0",
|
||||||
"@electron-forge/maker-wix": "^7.5.0",
|
|
||||||
"@electron-forge/maker-zip": "^7.5.0",
|
|
||||||
"@electron-forge/plugin-auto-unpack-natives": "^7.4.0",
|
|
||||||
"@electron-forge/plugin-fuses": "^7.4.0",
|
|
||||||
"@electron-forge/plugin-vite": "^7.4.0",
|
|
||||||
"@electron/fuses": "^1.8.0",
|
|
||||||
"@electron/rebuild": "^3.6.0",
|
|
||||||
"@iarna/toml": "^2.2.5",
|
"@iarna/toml": "^2.2.5",
|
||||||
"@lezer/generator": "^1.7.1",
|
"@lezer/generator": "^1.7.1",
|
||||||
"@nabla/vite-plugin-eslint": "^2.0.5",
|
"@nabla/vite-plugin-eslint": "^2.0.5",
|
||||||
@ -188,9 +178,9 @@
|
|||||||
"@xstate/cli": "^0.5.17",
|
"@xstate/cli": "^0.5.17",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"d3-force": "^3.0.0",
|
"d3-force": "^3.0.0",
|
||||||
"electron": "^32.1.2",
|
"electron": "32.1.2",
|
||||||
"electron-builder": "^24.13.3",
|
"electron-builder": "24.13.3",
|
||||||
"electron-notarize": "^1.2.2",
|
"electron-notarize": "1.2.2",
|
||||||
"eslint": "^8.0.1",
|
"eslint": "^8.0.1",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"eslint-plugin-css-modules": "^2.12.0",
|
"eslint-plugin-css-modules": "^2.12.0",
|
||||||
|
@ -119,6 +119,11 @@
|
|||||||
"title": "Pipe and Flange Assembly",
|
"title": "Pipe and Flange Assembly",
|
||||||
"description": "A crucial component in various piping systems, designed to facilitate the connection, disconnection, and access to piping for inspection, cleaning, and modifications. This assembly combines pipes (long cylindrical conduits) with flanges (plate-like fittings) to create a secure yet detachable joint."
|
"description": "A crucial component in various piping systems, designed to facilitate the connection, disconnection, and access to piping for inspection, cleaning, and modifications. This assembly combines pipes (long cylindrical conduits) with flanges (plate-like fittings) to create a secure yet detachable joint."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"file": "pipe-with-bend.kcl",
|
||||||
|
"title": "Pipe with bend",
|
||||||
|
"description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"file": "poopy-shoe.kcl",
|
"file": "poopy-shoe.kcl",
|
||||||
"title": "Poopy Shoe",
|
"title": "Poopy Shoe",
|
||||||
|
@ -105,7 +105,7 @@ export class CameraControls {
|
|||||||
pendingZoom: number | null = null
|
pendingZoom: number | null = null
|
||||||
pendingRotation: Vector2 | null = null
|
pendingRotation: Vector2 | null = null
|
||||||
pendingPan: Vector2 | null = null
|
pendingPan: Vector2 | null = null
|
||||||
interactionGuards: MouseGuard = cameraMouseDragGuards.KittyCAD
|
interactionGuards: MouseGuard = cameraMouseDragGuards.Zoo
|
||||||
isFovAnimationInProgress = false
|
isFovAnimationInProgress = false
|
||||||
perspectiveFovBeforeOrtho = 45
|
perspectiveFovBeforeOrtho = 45
|
||||||
get isPerspective() {
|
get isPerspective() {
|
||||||
|
@ -505,7 +505,8 @@ const ConstraintSymbol = ({
|
|||||||
constrainInfo: ConstrainInfo
|
constrainInfo: ConstrainInfo
|
||||||
verticalPosition: 'top' | 'bottom'
|
verticalPosition: 'top' | 'bottom'
|
||||||
}) => {
|
}) => {
|
||||||
const { context, send } = useModelingContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
|
const { context } = useModelingContext()
|
||||||
const varNameMap: {
|
const varNameMap: {
|
||||||
[key in ConstrainInfo['type']]: {
|
[key in ConstrainInfo['type']]: {
|
||||||
varName: string
|
varName: string
|
||||||
@ -624,11 +625,18 @@ const ConstraintSymbol = ({
|
|||||||
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
|
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
|
||||||
onClick={toSync(async () => {
|
onClick={toSync(async () => {
|
||||||
if (!isConstrained) {
|
if (!isConstrained) {
|
||||||
send({
|
commandBarSend({
|
||||||
type: 'Convert to variable',
|
type: 'Find and select command',
|
||||||
data: {
|
data: {
|
||||||
|
name: 'Constrain with named value',
|
||||||
|
groupId: 'modeling',
|
||||||
|
argDefaultValues: {
|
||||||
|
currentValue: {
|
||||||
pathToNode,
|
pathToNode,
|
||||||
variableName: varName,
|
variableName: varName,
|
||||||
|
valueText: value,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else if (isConstrained) {
|
} else if (isConstrained) {
|
||||||
|
@ -8,11 +8,16 @@ import { getSystemTheme } from 'lib/theme'
|
|||||||
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
|
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
|
||||||
import { roundOff } from 'lib/utils'
|
import { roundOff } from 'lib/utils'
|
||||||
import { varMentions } from 'lib/varCompletionExtension'
|
import { varMentions } from 'lib/varCompletionExtension'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import styles from './CommandBarKclInput.module.css'
|
import styles from './CommandBarKclInput.module.css'
|
||||||
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
|
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
|
||||||
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
|
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
|
||||||
|
import { useSelector } from '@xstate/react'
|
||||||
|
|
||||||
|
const machineContextSelector = (snapshot?: {
|
||||||
|
context: Record<string, unknown>
|
||||||
|
}) => snapshot?.context
|
||||||
|
|
||||||
function CommandBarKclInput({
|
function CommandBarKclInput({
|
||||||
arg,
|
arg,
|
||||||
@ -31,12 +36,44 @@ function CommandBarKclInput({
|
|||||||
arg.name
|
arg.name
|
||||||
] as KclCommandValue | undefined
|
] as KclCommandValue | undefined
|
||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const defaultValue = (arg.defaultValue as string) || ''
|
const argMachineContext = useSelector(
|
||||||
|
arg.machineActor,
|
||||||
|
machineContextSelector
|
||||||
|
)
|
||||||
|
const defaultValue = useMemo(
|
||||||
|
() =>
|
||||||
|
arg.defaultValue
|
||||||
|
? arg.defaultValue instanceof Function
|
||||||
|
? arg.defaultValue(commandBarState.context, argMachineContext)
|
||||||
|
: arg.defaultValue
|
||||||
|
: '',
|
||||||
|
[arg.defaultValue, commandBarState.context, argMachineContext]
|
||||||
|
)
|
||||||
|
const initialVariableName = useMemo(() => {
|
||||||
|
// Use the configured variable name if it exists
|
||||||
|
if (arg.variableName !== undefined) {
|
||||||
|
return arg.variableName instanceof Function
|
||||||
|
? arg.variableName(commandBarState.context, argMachineContext)
|
||||||
|
: arg.variableName
|
||||||
|
}
|
||||||
|
// or derive it from the previously set value or the argument name
|
||||||
|
return previouslySetValue && 'variableName' in previouslySetValue
|
||||||
|
? previouslySetValue.variableName
|
||||||
|
: arg.name
|
||||||
|
}, [
|
||||||
|
arg.variableName,
|
||||||
|
commandBarState.context,
|
||||||
|
argMachineContext,
|
||||||
|
arg.name,
|
||||||
|
previouslySetValue,
|
||||||
|
])
|
||||||
const [value, setValue] = useState(
|
const [value, setValue] = useState(
|
||||||
previouslySetValue?.valueText || defaultValue || ''
|
previouslySetValue?.valueText || defaultValue || ''
|
||||||
)
|
)
|
||||||
const [createNewVariable, setCreateNewVariable] = useState(
|
const [createNewVariable, setCreateNewVariable] = useState(
|
||||||
previouslySetValue && 'variableName' in previouslySetValue
|
(previouslySetValue && 'variableName' in previouslySetValue) ||
|
||||||
|
arg.createVariableByDefault ||
|
||||||
|
false
|
||||||
)
|
)
|
||||||
const [canSubmit, setCanSubmit] = useState(true)
|
const [canSubmit, setCanSubmit] = useState(true)
|
||||||
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
||||||
@ -52,10 +89,7 @@ function CommandBarKclInput({
|
|||||||
isNewVariableNameUnique,
|
isNewVariableNameUnique,
|
||||||
} = useCalculateKclExpression({
|
} = useCalculateKclExpression({
|
||||||
value,
|
value,
|
||||||
initialVariableName:
|
initialVariableName,
|
||||||
previouslySetValue && 'variableName' in previouslySetValue
|
|
||||||
? previouslySetValue.variableName
|
|
||||||
: arg.name,
|
|
||||||
})
|
})
|
||||||
const varMentionData: Completion[] = prevVariables.map((v) => ({
|
const varMentionData: Completion[] = prevVariables.map((v) => ({
|
||||||
label: v.key,
|
label: v.key,
|
||||||
|
@ -1,13 +1,23 @@
|
|||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { ActionIcon, ActionIconProps } from './ActionIcon'
|
import { ActionIcon, ActionIconProps } from './ActionIcon'
|
||||||
import { RefObject, useEffect, useMemo, useRef, useState } from 'react'
|
import {
|
||||||
|
MouseEvent,
|
||||||
|
RefObject,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { Dialog } from '@headlessui/react'
|
import { Dialog } from '@headlessui/react'
|
||||||
|
|
||||||
interface ContextMenuProps
|
export interface ContextMenuProps
|
||||||
extends Omit<React.HTMLAttributes<HTMLUListElement>, 'children'> {
|
extends Omit<React.HTMLAttributes<HTMLUListElement>, 'children'> {
|
||||||
items?: React.ReactElement[]
|
items?: React.ReactElement[]
|
||||||
menuTargetElement?: RefObject<HTMLElement>
|
menuTargetElement?: RefObject<HTMLElement>
|
||||||
|
guard?: (e: globalThis.MouseEvent) => boolean
|
||||||
|
event?: 'contextmenu' | 'mouseup'
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultContextMenuItems = [
|
const DefaultContextMenuItems = [
|
||||||
@ -20,6 +30,8 @@ export function ContextMenu({
|
|||||||
items = DefaultContextMenuItems,
|
items = DefaultContextMenuItems,
|
||||||
menuTargetElement,
|
menuTargetElement,
|
||||||
className,
|
className,
|
||||||
|
guard,
|
||||||
|
event = 'contextmenu',
|
||||||
...props
|
...props
|
||||||
}: ContextMenuProps) {
|
}: ContextMenuProps) {
|
||||||
const dialogRef = useRef<HTMLDivElement>(null)
|
const dialogRef = useRef<HTMLDivElement>(null)
|
||||||
@ -32,6 +44,15 @@ export function ContextMenu({
|
|||||||
useHotkeys('esc', () => setOpen(false), {
|
useHotkeys('esc', () => setOpen(false), {
|
||||||
enabled: open,
|
enabled: open,
|
||||||
})
|
})
|
||||||
|
const handleContextMenu = useCallback(
|
||||||
|
(e: globalThis.MouseEvent) => {
|
||||||
|
if (guard && !guard(e)) return
|
||||||
|
e.preventDefault()
|
||||||
|
setPosition({ x: e.clientX, y: e.clientY })
|
||||||
|
setOpen(true)
|
||||||
|
},
|
||||||
|
[guard, setPosition, setOpen]
|
||||||
|
)
|
||||||
|
|
||||||
const dialogPositionStyle = useMemo(() => {
|
const dialogPositionStyle = useMemo(() => {
|
||||||
if (!dialogRef.current)
|
if (!dialogRef.current)
|
||||||
@ -78,21 +99,9 @@ export function ContextMenu({
|
|||||||
|
|
||||||
// Add context menu listener to target once mounted
|
// Add context menu listener to target once mounted
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleContextMenu = (e: MouseEvent) => {
|
menuTargetElement?.current?.addEventListener(event, handleContextMenu)
|
||||||
console.log('context menu', e)
|
|
||||||
e.preventDefault()
|
|
||||||
setPosition({ x: e.x, y: e.y })
|
|
||||||
setOpen(true)
|
|
||||||
}
|
|
||||||
menuTargetElement?.current?.addEventListener(
|
|
||||||
'contextmenu',
|
|
||||||
handleContextMenu
|
|
||||||
)
|
|
||||||
return () => {
|
return () => {
|
||||||
menuTargetElement?.current?.removeEventListener(
|
menuTargetElement?.current?.removeEventListener(event, handleContextMenu)
|
||||||
'contextmenu',
|
|
||||||
handleContextMenu
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}, [menuTargetElement?.current])
|
}, [menuTargetElement?.current])
|
||||||
|
|
||||||
@ -100,7 +109,10 @@ export function ContextMenu({
|
|||||||
<Dialog open={open} onClose={() => setOpen(false)}>
|
<Dialog open={open} onClose={() => setOpen(false)}>
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-50 w-screen h-screen"
|
className="fixed inset-0 z-50 w-screen h-screen"
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setPosition({ x: e.clientX, y: e.clientY })
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Dialog.Backdrop className="fixed z-10 inset-0" />
|
<Dialog.Backdrop className="fixed z-10 inset-0" />
|
||||||
<Dialog.Panel
|
<Dialog.Panel
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { SceneInfra } from 'clientSideScene/sceneInfra'
|
import { SceneInfra } from 'clientSideScene/sceneInfra'
|
||||||
import { sceneInfra } from 'lib/singletons'
|
import { sceneInfra } from 'lib/singletons'
|
||||||
import { MutableRefObject, useEffect, useMemo, useRef } from 'react'
|
import { MutableRefObject, useEffect, useRef } from 'react'
|
||||||
import {
|
import {
|
||||||
WebGLRenderer,
|
WebGLRenderer,
|
||||||
Scene,
|
Scene,
|
||||||
@ -19,16 +19,14 @@ import {
|
|||||||
Intersection,
|
Intersection,
|
||||||
Object3D,
|
Object3D,
|
||||||
} from 'three'
|
} from 'three'
|
||||||
import {
|
|
||||||
ContextMenu,
|
|
||||||
ContextMenuDivider,
|
|
||||||
ContextMenuItem,
|
|
||||||
ContextMenuItemRefresh,
|
|
||||||
} from './ContextMenu'
|
|
||||||
import { Popover } from '@headlessui/react'
|
import { Popover } from '@headlessui/react'
|
||||||
import { CustomIcon } from './CustomIcon'
|
import { CustomIcon } from './CustomIcon'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import {
|
||||||
|
useViewControlMenuItems,
|
||||||
|
ViewControlContextMenu,
|
||||||
|
} from './ViewControlMenu'
|
||||||
|
import { AxisNames } from 'lib/constants'
|
||||||
|
|
||||||
const CANVAS_SIZE = 80
|
const CANVAS_SIZE = 80
|
||||||
const FRUSTUM_SIZE = 0.5
|
const FRUSTUM_SIZE = 0.5
|
||||||
@ -40,64 +38,14 @@ enum AxisColors {
|
|||||||
Z = '#6689ef',
|
Z = '#6689ef',
|
||||||
Gray = '#c6c7c2',
|
Gray = '#c6c7c2',
|
||||||
}
|
}
|
||||||
enum AxisNames {
|
|
||||||
X = 'x',
|
|
||||||
Y = 'y',
|
|
||||||
Z = 'z',
|
|
||||||
NEG_X = '-x',
|
|
||||||
NEG_Y = '-y',
|
|
||||||
NEG_Z = '-z',
|
|
||||||
}
|
|
||||||
const axisNamesSemantic: Record<AxisNames, string> = {
|
|
||||||
[AxisNames.X]: 'Right',
|
|
||||||
[AxisNames.Y]: 'Back',
|
|
||||||
[AxisNames.Z]: 'Top',
|
|
||||||
[AxisNames.NEG_X]: 'Left',
|
|
||||||
[AxisNames.NEG_Y]: 'Front',
|
|
||||||
[AxisNames.NEG_Z]: 'Bottom',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Gizmo() {
|
export default function Gizmo() {
|
||||||
|
const menuItems = useViewControlMenuItems()
|
||||||
const wrapperRef = useRef<HTMLDivElement | null>(null)
|
const wrapperRef = useRef<HTMLDivElement | null>(null)
|
||||||
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
||||||
const raycasterIntersect = useRef<Intersection<Object3D> | null>(null)
|
const raycasterIntersect = useRef<Intersection<Object3D> | null>(null)
|
||||||
const cameraPassiveUpdateTimer = useRef(0)
|
const cameraPassiveUpdateTimer = useRef(0)
|
||||||
const raycasterPassiveUpdateTimer = useRef(0)
|
const raycasterPassiveUpdateTimer = useRef(0)
|
||||||
const { send: modelingSend } = useModelingContext()
|
|
||||||
const menuItems = useMemo(
|
|
||||||
() => [
|
|
||||||
...Object.entries(axisNamesSemantic).map(([axisName, axisSemantic]) => (
|
|
||||||
<ContextMenuItem
|
|
||||||
key={axisName}
|
|
||||||
onClick={() => {
|
|
||||||
sceneInfra.camControls
|
|
||||||
.updateCameraToAxis(axisName as AxisNames)
|
|
||||||
.catch(reportRejection)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{axisSemantic} view
|
|
||||||
</ContextMenuItem>
|
|
||||||
)),
|
|
||||||
<ContextMenuDivider />,
|
|
||||||
<ContextMenuItem
|
|
||||||
onClick={() => {
|
|
||||||
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Reset view
|
|
||||||
</ContextMenuItem>,
|
|
||||||
<ContextMenuItem
|
|
||||||
onClick={() => {
|
|
||||||
modelingSend({ type: 'Center camera on selection' })
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Center view on selection
|
|
||||||
</ContextMenuItem>,
|
|
||||||
<ContextMenuDivider />,
|
|
||||||
<ContextMenuItemRefresh />,
|
|
||||||
],
|
|
||||||
[axisNamesSemantic]
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canvasRef.current) return
|
if (!canvasRef.current) return
|
||||||
@ -161,7 +109,7 @@ export default function Gizmo() {
|
|||||||
className="grid place-content-center rounded-full overflow-hidden border border-solid border-primary/50 pointer-events-auto bg-chalkboard-10/70 dark:bg-chalkboard-100/80 backdrop-blur-sm"
|
className="grid place-content-center rounded-full overflow-hidden border border-solid border-primary/50 pointer-events-auto bg-chalkboard-10/70 dark:bg-chalkboard-100/80 backdrop-blur-sm"
|
||||||
>
|
>
|
||||||
<canvas ref={canvasRef} />
|
<canvas ref={canvasRef} />
|
||||||
<ContextMenu menuTargetElement={wrapperRef} items={menuItems} />
|
<ViewControlContextMenu menuTargetElement={wrapperRef} />
|
||||||
</div>
|
</div>
|
||||||
<GizmoDropdown items={menuItems} />
|
<GizmoDropdown items={menuItems} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { APP_VERSION } from 'routes/Settings'
|
import { APP_VERSION, getReleaseUrl } from 'routes/Settings'
|
||||||
import { CustomIcon } from 'components/CustomIcon'
|
import { CustomIcon } from 'components/CustomIcon'
|
||||||
import Tooltip from 'components/Tooltip'
|
import Tooltip from 'components/Tooltip'
|
||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
@ -72,10 +72,8 @@ export function LowerRightControls({
|
|||||||
<menu className="flex items-center justify-end gap-3 pointer-events-auto">
|
<menu className="flex items-center justify-end gap-3 pointer-events-auto">
|
||||||
{!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />}
|
{!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />}
|
||||||
<a
|
<a
|
||||||
onClick={openExternalBrowserIfDesktop(
|
onClick={openExternalBrowserIfDesktop(getReleaseUrl())}
|
||||||
`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`
|
href={getReleaseUrl()}
|
||||||
)}
|
|
||||||
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`}
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className={'!no-underline font-mono text-xs ' + linkOverrideClassName}
|
className={'!no-underline font-mono text-xs ' + linkOverrideClassName}
|
||||||
|
@ -69,14 +69,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
const [isKclLspReady, setIsKclLspReady] = useState(false)
|
const [isKclLspReady, setIsKclLspReady] = useState(false)
|
||||||
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
|
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
|
||||||
|
|
||||||
const {
|
const { auth } = useSettingsAuthContext()
|
||||||
auth,
|
|
||||||
settings: {
|
|
||||||
context: {
|
|
||||||
modeling: { defaultUnit },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} = useSettingsAuthContext()
|
|
||||||
const token = auth?.context.token
|
const token = auth?.context.token
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
@ -92,7 +85,6 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
const initEvent: KclWorkerOptions = {
|
const initEvent: KclWorkerOptions = {
|
||||||
wasmUrl: wasmUrl(),
|
wasmUrl: wasmUrl(),
|
||||||
token: token,
|
token: token,
|
||||||
baseUnit: defaultUnit.current,
|
|
||||||
apiBaseUrl: VITE_KC_API_BASE_URL,
|
apiBaseUrl: VITE_KC_API_BASE_URL,
|
||||||
}
|
}
|
||||||
lspWorker.postMessage({
|
lspWorker.postMessage({
|
||||||
|
@ -41,7 +41,10 @@ import {
|
|||||||
angleBetweenInfo,
|
angleBetweenInfo,
|
||||||
applyConstraintAngleBetween,
|
applyConstraintAngleBetween,
|
||||||
} from './Toolbar/SetAngleBetween'
|
} from './Toolbar/SetAngleBetween'
|
||||||
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
|
import {
|
||||||
|
applyConstraintAngleLength,
|
||||||
|
applyConstraintLength,
|
||||||
|
} from './Toolbar/setAngleLength'
|
||||||
import {
|
import {
|
||||||
canSweepSelection,
|
canSweepSelection,
|
||||||
handleSelectionBatch,
|
handleSelectionBatch,
|
||||||
@ -51,6 +54,8 @@ import {
|
|||||||
Selections,
|
Selections,
|
||||||
updateSelections,
|
updateSelections,
|
||||||
canLoftSelection,
|
canLoftSelection,
|
||||||
|
canRevolveSelection,
|
||||||
|
canShellSelection,
|
||||||
} from 'lib/selections'
|
} from 'lib/selections'
|
||||||
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
||||||
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
||||||
@ -62,13 +67,15 @@ import {
|
|||||||
getSketchOrientationDetails,
|
getSketchOrientationDetails,
|
||||||
} from 'clientSideScene/sceneEntities'
|
} from 'clientSideScene/sceneEntities'
|
||||||
import {
|
import {
|
||||||
moveValueIntoNewVariablePath,
|
insertNamedConstant,
|
||||||
|
replaceValueAtNodePath,
|
||||||
sketchOnExtrudedFace,
|
sketchOnExtrudedFace,
|
||||||
sketchOnOffsetPlane,
|
sketchOnOffsetPlane,
|
||||||
startSketchOnDefault,
|
startSketchOnDefault,
|
||||||
} from 'lang/modifyAst'
|
} from 'lang/modifyAst'
|
||||||
import { Program, parse, recast, resultIsOk } from 'lang/wasm'
|
import { PathToNode, Program, parse, recast, resultIsOk } from 'lang/wasm'
|
||||||
import {
|
import {
|
||||||
|
doesSceneHaveExtrudedSketch,
|
||||||
doesSceneHaveSweepableSketch,
|
doesSceneHaveSweepableSketch,
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
isSingleCursorInPipe,
|
isSingleCursorInPipe,
|
||||||
@ -79,7 +86,6 @@ import toast from 'react-hot-toast'
|
|||||||
import { EditorSelection, Transaction } from '@codemirror/state'
|
import { EditorSelection, Transaction } from '@codemirror/state'
|
||||||
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
||||||
import { getVarNameModal } from 'hooks/useToolbarGuards'
|
|
||||||
import { err, reportRejection, trap } from 'lib/trap'
|
import { err, reportRejection, trap } from 'lib/trap'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { modelingMachineEvent } from 'editor/manager'
|
import { modelingMachineEvent } from 'editor/manager'
|
||||||
@ -570,6 +576,26 @@ export const ModelingMachineProvider = ({
|
|||||||
if (err(canSweep)) return false
|
if (err(canSweep)) return false
|
||||||
return canSweep
|
return canSweep
|
||||||
},
|
},
|
||||||
|
'has valid revolve selection': ({ context: { selectionRanges } }) => {
|
||||||
|
// A user can begin extruding if they either have 1+ faces selected or nothing selected
|
||||||
|
// TODO: I believe this guard only allows for extruding a single face at a time
|
||||||
|
const hasNoSelection =
|
||||||
|
selectionRanges.graphSelections.length === 0 ||
|
||||||
|
isRangeBetweenCharacters(selectionRanges) ||
|
||||||
|
isSelectionLastLine(selectionRanges, codeManager.code)
|
||||||
|
|
||||||
|
if (hasNoSelection) {
|
||||||
|
// they have no selection, we should enable the button
|
||||||
|
// so they can select the face through the cmdbar
|
||||||
|
// BUT only if there's extrudable geometry
|
||||||
|
return doesSceneHaveSweepableSketch(kclManager.ast)
|
||||||
|
}
|
||||||
|
if (!isSketchPipe(selectionRanges)) return false
|
||||||
|
|
||||||
|
const canSweep = canRevolveSelection(selectionRanges)
|
||||||
|
if (err(canSweep)) return false
|
||||||
|
return canSweep
|
||||||
|
},
|
||||||
'has valid loft selection': ({ context: { selectionRanges } }) => {
|
'has valid loft selection': ({ context: { selectionRanges } }) => {
|
||||||
const hasNoSelection =
|
const hasNoSelection =
|
||||||
selectionRanges.graphSelections.length === 0 ||
|
selectionRanges.graphSelections.length === 0 ||
|
||||||
@ -585,6 +611,24 @@ export const ModelingMachineProvider = ({
|
|||||||
if (err(canLoft)) return false
|
if (err(canLoft)) return false
|
||||||
return canLoft
|
return canLoft
|
||||||
},
|
},
|
||||||
|
'has valid shell selection': ({
|
||||||
|
context: { selectionRanges },
|
||||||
|
event,
|
||||||
|
}) => {
|
||||||
|
const hasNoSelection =
|
||||||
|
selectionRanges.graphSelections.length === 0 ||
|
||||||
|
isRangeBetweenCharacters(selectionRanges) ||
|
||||||
|
isSelectionLastLine(selectionRanges, codeManager.code)
|
||||||
|
|
||||||
|
if (hasNoSelection) {
|
||||||
|
return doesSceneHaveExtrudedSketch(kclManager.ast)
|
||||||
|
}
|
||||||
|
|
||||||
|
const canShell = canShellSelection(selectionRanges)
|
||||||
|
console.log('canShellSelection', canShellSelection(selectionRanges))
|
||||||
|
if (err(canShell)) return false
|
||||||
|
return canShell
|
||||||
|
},
|
||||||
'has valid selection for deletion': ({
|
'has valid selection for deletion': ({
|
||||||
context: { selectionRanges },
|
context: { selectionRanges },
|
||||||
}) => {
|
}) => {
|
||||||
@ -869,12 +913,18 @@ export const ModelingMachineProvider = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
'Get length info': fromPromise(
|
astConstrainLength: fromPromise(
|
||||||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
async ({
|
||||||
const { modifiedAst, pathToNodeMap } =
|
input: { selectionRanges, sketchDetails, lengthValue },
|
||||||
await applyConstraintAngleLength({
|
}) => {
|
||||||
|
if (!lengthValue)
|
||||||
|
return Promise.reject(new Error('No length value'))
|
||||||
|
const constraintResult = await applyConstraintLength({
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
|
length: lengthValue,
|
||||||
})
|
})
|
||||||
|
if (err(constraintResult)) return Promise.reject(constraintResult)
|
||||||
|
const { modifiedAst, pathToNodeMap } = constraintResult
|
||||||
const pResult = parse(recast(modifiedAst))
|
const pResult = parse(recast(modifiedAst))
|
||||||
if (trap(pResult) || !resultIsOk(pResult))
|
if (trap(pResult) || !resultIsOk(pResult))
|
||||||
return Promise.reject(new Error('Unexpected compilation error'))
|
return Promise.reject(new Error('Unexpected compilation error'))
|
||||||
@ -1043,38 +1093,88 @@ export const ModelingMachineProvider = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
'Get convert to variable info': fromPromise(
|
'Apply named value constraint': fromPromise(
|
||||||
async ({ input: { selectionRanges, sketchDetails, data } }) => {
|
async ({ input: { selectionRanges, sketchDetails, data } }) => {
|
||||||
if (!sketchDetails)
|
if (!sketchDetails) {
|
||||||
return Promise.reject(new Error('No sketch details'))
|
return Promise.reject(new Error('No sketch details'))
|
||||||
const { variableName } = await getVarNameModal({
|
}
|
||||||
valueName: data?.variableName || 'var',
|
if (!data) {
|
||||||
})
|
return Promise.reject(new Error('No data from command flow'))
|
||||||
|
}
|
||||||
let pResult = parse(recast(kclManager.ast))
|
let pResult = parse(recast(kclManager.ast))
|
||||||
if (trap(pResult) || !resultIsOk(pResult))
|
if (trap(pResult) || !resultIsOk(pResult))
|
||||||
return Promise.reject(new Error('Unexpected compilation error'))
|
return Promise.reject(new Error('Unexpected compilation error'))
|
||||||
let parsed = pResult.program
|
let parsed = pResult.program
|
||||||
|
|
||||||
const { modifiedAst: _modifiedAst, pathToReplacedNode } =
|
let result: {
|
||||||
moveValueIntoNewVariablePath(
|
modifiedAst: Node<Program>
|
||||||
parsed,
|
pathToReplaced: PathToNode | null
|
||||||
kclManager.programMemory,
|
} = {
|
||||||
data?.pathToNode || [],
|
modifiedAst: parsed,
|
||||||
variableName
|
pathToReplaced: null,
|
||||||
|
}
|
||||||
|
// If the user provided a constant name,
|
||||||
|
// we need to insert the named constant
|
||||||
|
// and then replace the node with the constant's name.
|
||||||
|
if ('variableName' in data.namedValue) {
|
||||||
|
const astAfterReplacement = replaceValueAtNodePath({
|
||||||
|
ast: parsed,
|
||||||
|
pathToNode: data.currentValue.pathToNode,
|
||||||
|
newExpressionString: data.namedValue.variableName,
|
||||||
|
})
|
||||||
|
if (trap(astAfterReplacement)) {
|
||||||
|
return Promise.reject(astAfterReplacement)
|
||||||
|
}
|
||||||
|
const parseResultAfterInsertion = parse(
|
||||||
|
recast(
|
||||||
|
insertNamedConstant({
|
||||||
|
node: astAfterReplacement.modifiedAst,
|
||||||
|
newExpression: data.namedValue,
|
||||||
|
})
|
||||||
)
|
)
|
||||||
pResult = parse(recast(_modifiedAst))
|
)
|
||||||
|
if (
|
||||||
|
trap(parseResultAfterInsertion) ||
|
||||||
|
!resultIsOk(parseResultAfterInsertion)
|
||||||
|
)
|
||||||
|
return Promise.reject(parseResultAfterInsertion)
|
||||||
|
result = {
|
||||||
|
modifiedAst: parseResultAfterInsertion.program,
|
||||||
|
pathToReplaced: astAfterReplacement.pathToReplaced,
|
||||||
|
}
|
||||||
|
} else if ('valueText' in data.namedValue) {
|
||||||
|
// If they didn't provide a constant name,
|
||||||
|
// just replace the node with the value.
|
||||||
|
const astAfterReplacement = replaceValueAtNodePath({
|
||||||
|
ast: parsed,
|
||||||
|
pathToNode: data.currentValue.pathToNode,
|
||||||
|
newExpressionString: data.namedValue.valueText,
|
||||||
|
})
|
||||||
|
if (trap(astAfterReplacement)) {
|
||||||
|
return Promise.reject(astAfterReplacement)
|
||||||
|
}
|
||||||
|
// The `replacer` function returns a pathToNode that assumes
|
||||||
|
// an identifier is also being inserted into the AST, creating an off-by-one error.
|
||||||
|
// This corrects that error, but TODO we should fix this upstream
|
||||||
|
// to avoid this kind of error in the future.
|
||||||
|
astAfterReplacement.pathToReplaced[1][0] =
|
||||||
|
(astAfterReplacement.pathToReplaced[1][0] as number) - 1
|
||||||
|
result = astAfterReplacement
|
||||||
|
}
|
||||||
|
|
||||||
|
pResult = parse(recast(result.modifiedAst))
|
||||||
if (trap(pResult) || !resultIsOk(pResult))
|
if (trap(pResult) || !resultIsOk(pResult))
|
||||||
return Promise.reject(new Error('Unexpected compilation error'))
|
return Promise.reject(new Error('Unexpected compilation error'))
|
||||||
parsed = pResult.program
|
parsed = pResult.program
|
||||||
|
|
||||||
if (trap(parsed)) return Promise.reject(parsed)
|
if (trap(parsed)) return Promise.reject(parsed)
|
||||||
parsed = parsed as Node<Program>
|
parsed = parsed as Node<Program>
|
||||||
if (!pathToReplacedNode)
|
if (!result.pathToReplaced)
|
||||||
return Promise.reject(new Error('No path to replaced node'))
|
return Promise.reject(new Error('No path to replaced node'))
|
||||||
|
|
||||||
const updatedAst =
|
const updatedAst =
|
||||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||||
pathToReplacedNode || [],
|
result.pathToReplaced || [],
|
||||||
parsed,
|
parsed,
|
||||||
sketchDetails.zAxis,
|
sketchDetails.zAxis,
|
||||||
sketchDetails.yAxis,
|
sketchDetails.yAxis,
|
||||||
@ -1087,7 +1187,7 @@ export const ModelingMachineProvider = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const selection = updateSelections(
|
const selection = updateSelections(
|
||||||
{ 0: pathToReplacedNode },
|
{ 0: result.pathToReplaced },
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
updatedAst.newAst
|
updatedAst.newAst
|
||||||
)
|
)
|
||||||
@ -1095,7 +1195,7 @@ export const ModelingMachineProvider = ({
|
|||||||
return {
|
return {
|
||||||
selectionType: 'completeSelection',
|
selectionType: 'completeSelection',
|
||||||
selection,
|
selection,
|
||||||
updatedPathToNode: pathToReplacedNode,
|
updatedPathToNode: result.pathToReplaced,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
@ -76,7 +76,7 @@ export const ModelingPane = ({
|
|||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
{...props}
|
{...props}
|
||||||
title={title && typeof title === 'string' ? title : ''}
|
aria-label={title && typeof title === 'string' ? title : ''}
|
||||||
data-testid={detailsTestId}
|
data-testid={detailsTestId}
|
||||||
id={id}
|
id={id}
|
||||||
className={
|
className={
|
||||||
|
@ -10,7 +10,7 @@ interface AllKeybindingsFieldsProps {}
|
|||||||
|
|
||||||
export const AllKeybindingsFields = forwardRef(
|
export const AllKeybindingsFields = forwardRef(
|
||||||
(
|
(
|
||||||
props: AllKeybindingsFieldsProps,
|
_props: AllKeybindingsFieldsProps,
|
||||||
scrollRef: ForwardedRef<HTMLDivElement>
|
scrollRef: ForwardedRef<HTMLDivElement>
|
||||||
) => {
|
) => {
|
||||||
// This is how we will get the interaction map from the context
|
// This is how we will get the interaction map from the context
|
||||||
@ -25,7 +25,7 @@ export const AllKeybindingsFields = forwardRef(
|
|||||||
.map(([category, categoryItems]) => (
|
.map(([category, categoryItems]) => (
|
||||||
<div className="flex flex-col gap-4 px-2 pr-4">
|
<div className="flex flex-col gap-4 px-2 pr-4">
|
||||||
<h2
|
<h2
|
||||||
id={`category-${category}`}
|
id={`category-${category.replaceAll(/\s/g, '-')}`}
|
||||||
className="text-xl mt-6 first-of-type:mt-0 capitalize font-bold"
|
className="text-xl mt-6 first-of-type:mt-0 capitalize font-bold"
|
||||||
>
|
>
|
||||||
{category}
|
{category}
|
||||||
|
@ -13,7 +13,7 @@ import { isDesktop } from 'lib/isDesktop'
|
|||||||
import { ActionButton } from 'components/ActionButton'
|
import { ActionButton } from 'components/ActionButton'
|
||||||
import { SettingsFieldInput } from './SettingsFieldInput'
|
import { SettingsFieldInput } from './SettingsFieldInput'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { APP_VERSION, PACKAGE_NAME } from 'routes/Settings'
|
import { APP_VERSION, IS_NIGHTLY, getReleaseUrl } from 'routes/Settings'
|
||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
import {
|
import {
|
||||||
createAndOpenNewTutorialProject,
|
createAndOpenNewTutorialProject,
|
||||||
@ -246,10 +246,8 @@ export const AllSettingsFields = forwardRef(
|
|||||||
to inject the version from package.json */}
|
to inject the version from package.json */}
|
||||||
App version {APP_VERSION}.{' '}
|
App version {APP_VERSION}.{' '}
|
||||||
<a
|
<a
|
||||||
onClick={openExternalBrowserIfDesktop(
|
onClick={openExternalBrowserIfDesktop(getReleaseUrl())}
|
||||||
`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`
|
href={getReleaseUrl()}
|
||||||
)}
|
|
||||||
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`}
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
@ -271,7 +269,7 @@ export const AllSettingsFields = forwardRef(
|
|||||||
, and start a discussion if you don't see it! Your feedback will
|
, and start a discussion if you don't see it! Your feedback will
|
||||||
help us prioritize what to build next.
|
help us prioritize what to build next.
|
||||||
</p>
|
</p>
|
||||||
{PACKAGE_NAME.indexOf('-nightly') === -1 && (
|
{!IS_NIGHTLY && (
|
||||||
<p className="max-w-2xl mt-6">
|
<p className="max-w-2xl mt-6">
|
||||||
Want to experience the latest and (hopefully) greatest from our
|
Want to experience the latest and (hopefully) greatest from our
|
||||||
main development branch?{' '}
|
main development branch?{' '}
|
||||||
|
@ -19,7 +19,7 @@ export function KeybindingsSectionsList({
|
|||||||
key={category}
|
key={category}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
scrollRef.current
|
scrollRef.current
|
||||||
?.querySelector(`#category-${category}`)
|
?.querySelector(`#category-${category.replaceAll(/\s/g, '-')}`)
|
||||||
?.scrollIntoView({
|
?.scrollIntoView({
|
||||||
block: 'center',
|
block: 'center',
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { trap } from 'lib/trap'
|
import { trap } from 'lib/trap'
|
||||||
import { useMachine } from '@xstate/react'
|
import { useMachine, useSelector } from '@xstate/react'
|
||||||
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
|
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
|
||||||
import { PATHS, BROWSER_PATH } from 'lib/paths'
|
import { PATHS, BROWSER_PATH } from 'lib/paths'
|
||||||
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
|
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
|
||||||
@ -23,7 +23,6 @@ import {
|
|||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
sceneEntitiesManager,
|
sceneEntitiesManager,
|
||||||
} from 'lib/singletons'
|
} from 'lib/singletons'
|
||||||
import { uuidv4 } from 'lib/utils'
|
|
||||||
import { IndexLoaderData } from 'lib/types'
|
import { IndexLoaderData } from 'lib/types'
|
||||||
import { settings } from 'lib/settings/initialSettings'
|
import { settings } from 'lib/settings/initialSettings'
|
||||||
import {
|
import {
|
||||||
@ -55,11 +54,15 @@ type SettingsAuthContextType = {
|
|||||||
settings: MachineContext<typeof settingsMachine>
|
settings: MachineContext<typeof settingsMachine>
|
||||||
}
|
}
|
||||||
|
|
||||||
// a little hacky for sure, open to changing it
|
/**
|
||||||
// this implies that we should only even have one instance of this provider mounted at any one time
|
* This variable is used to store the last snapshot of the settings context
|
||||||
// but I think that's a safe assumption
|
* for use outside of React, such as in `wasm.ts`. It is updated every time
|
||||||
let settingsStateRef: ContextFrom<typeof settingsMachine> | undefined
|
* the settings machine changes with `useSelector`.
|
||||||
export const getSettingsState = () => settingsStateRef
|
* TODO: when we decouple XState from React, we can just subscribe to the actor directly from `wasm.ts`
|
||||||
|
*/
|
||||||
|
export let lastSettingsContextSnapshot:
|
||||||
|
| ContextFrom<typeof settingsMachine>
|
||||||
|
| undefined
|
||||||
|
|
||||||
export const SettingsAuthContext = createContext({} as SettingsAuthContextType)
|
export const SettingsAuthContext = createContext({} as SettingsAuthContextType)
|
||||||
|
|
||||||
@ -129,27 +132,11 @@ export const SettingsAuthProviderBase = ({
|
|||||||
.setTheme(context.app.theme.current)
|
.setTheme(context.app.theme.current)
|
||||||
.catch(reportRejection)
|
.catch(reportRejection)
|
||||||
},
|
},
|
||||||
setEngineScaleGridVisibility: ({ context }) => {
|
|
||||||
engineCommandManager.setScaleGridVisibility(
|
|
||||||
context.modeling.showScaleGrid.current
|
|
||||||
)
|
|
||||||
},
|
|
||||||
setClientTheme: ({ context }) => {
|
setClientTheme: ({ context }) => {
|
||||||
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
||||||
sceneInfra.theme = opposingTheme
|
sceneInfra.theme = opposingTheme
|
||||||
sceneEntitiesManager.updateSegmentBaseColor(opposingTheme)
|
sceneEntitiesManager.updateSegmentBaseColor(opposingTheme)
|
||||||
},
|
},
|
||||||
setEngineEdges: ({ context }) => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
engineCommandManager.sendSceneCommand({
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd: {
|
|
||||||
type: 'edge_lines_visible' as any, // TODO update kittycad.ts to get this new command type
|
|
||||||
hidden: !context.modeling.highlightEdges.current,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
toastSuccess: ({ event }) => {
|
toastSuccess: ({ event }) => {
|
||||||
if (!('data' in event)) return
|
if (!('data' in event)) return
|
||||||
const eventParts = event.type.replace(/^set./, '').split('.') as [
|
const eventParts = event.type.replace(/^set./, '').split('.') as [
|
||||||
@ -175,17 +162,27 @@ export const SettingsAuthProviderBase = ({
|
|||||||
},
|
},
|
||||||
'Execute AST': ({ context, event }) => {
|
'Execute AST': ({ context, event }) => {
|
||||||
try {
|
try {
|
||||||
|
const relevantSetting = (s: typeof settings) => {
|
||||||
|
return (
|
||||||
|
s.modeling?.defaultUnit?.current !==
|
||||||
|
context.modeling.defaultUnit.current ||
|
||||||
|
s.modeling.showScaleGrid.current !==
|
||||||
|
context.modeling.showScaleGrid.current ||
|
||||||
|
s.modeling?.highlightEdges.current !==
|
||||||
|
context.modeling.highlightEdges.current
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const allSettingsIncludesUnitChange =
|
const allSettingsIncludesUnitChange =
|
||||||
event.type === 'Set all settings' &&
|
event.type === 'Set all settings' &&
|
||||||
event.settings?.modeling?.defaultUnit?.current !==
|
relevantSetting(event.settings)
|
||||||
context.modeling.defaultUnit.current
|
|
||||||
const resetSettingsIncludesUnitChange =
|
const resetSettingsIncludesUnitChange =
|
||||||
event.type === 'Reset settings' &&
|
event.type === 'Reset settings' && relevantSetting(settings)
|
||||||
context.modeling.defaultUnit.current !==
|
|
||||||
settings?.modeling?.defaultUnit?.default
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
event.type === 'set.modeling.defaultUnit' ||
|
event.type === 'set.modeling.defaultUnit' ||
|
||||||
|
event.type === 'set.modeling.showScaleGrid' ||
|
||||||
|
event.type === 'set.modeling.highlightEdges' ||
|
||||||
allSettingsIncludesUnitChange ||
|
allSettingsIncludesUnitChange ||
|
||||||
resetSettingsIncludesUnitChange
|
resetSettingsIncludesUnitChange
|
||||||
) {
|
) {
|
||||||
@ -214,7 +211,10 @@ export const SettingsAuthProviderBase = ({
|
|||||||
}),
|
}),
|
||||||
{ input: loadedSettings }
|
{ input: loadedSettings }
|
||||||
)
|
)
|
||||||
settingsStateRef = settingsState.context
|
// Any time the actor changes, update the settings state for external use
|
||||||
|
useSelector(settingsActor, (s) => {
|
||||||
|
lastSettingsContextSnapshot = s.context
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDesktop()) return
|
if (!isDesktop()) return
|
||||||
|
@ -20,6 +20,7 @@ import { IndexLoaderData } from 'lib/types'
|
|||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { err, reportRejection } from 'lib/trap'
|
import { err, reportRejection } from 'lib/trap'
|
||||||
import { getArtifactOfTypes } from 'lang/std/artifactGraph'
|
import { getArtifactOfTypes } from 'lang/std/artifactGraph'
|
||||||
|
import { ViewControlContextMenu } from './ViewControlMenu'
|
||||||
|
|
||||||
enum StreamState {
|
enum StreamState {
|
||||||
Playing = 'playing',
|
Playing = 'playing',
|
||||||
@ -30,6 +31,7 @@ enum StreamState {
|
|||||||
|
|
||||||
export const Stream = () => {
|
export const Stream = () => {
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const videoWrapperRef = useRef<HTMLDivElement>(null)
|
||||||
const videoRef = useRef<HTMLVideoElement>(null)
|
const videoRef = useRef<HTMLVideoElement>(null)
|
||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const { state, send } = useModelingContext()
|
const { state, send } = useModelingContext()
|
||||||
@ -258,7 +260,7 @@ export const Stream = () => {
|
|||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}, [mediaStream])
|
}, [mediaStream])
|
||||||
|
|
||||||
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
|
const handleClick: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||||
// If we've got no stream or connection, don't do anything
|
// If we've got no stream or connection, don't do anything
|
||||||
if (!isNetworkOkay) return
|
if (!isNetworkOkay) return
|
||||||
if (!videoRef.current) return
|
if (!videoRef.current) return
|
||||||
@ -320,10 +322,11 @@ export const Stream = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={videoWrapperRef}
|
||||||
className="absolute inset-0 z-0"
|
className="absolute inset-0 z-0"
|
||||||
id="stream"
|
id="stream"
|
||||||
data-testid="stream"
|
data-testid="stream"
|
||||||
onClick={handleMouseUp}
|
onClick={handleClick}
|
||||||
onDoubleClick={enterSketchModeIfSelectingSketch}
|
onDoubleClick={enterSketchModeIfSelectingSketch}
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
onContextMenuCapture={(e) => e.preventDefault()}
|
onContextMenuCapture={(e) => e.preventDefault()}
|
||||||
@ -384,6 +387,14 @@ export const Stream = () => {
|
|||||||
</Loading>
|
</Loading>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<ViewControlContextMenu
|
||||||
|
event="mouseup"
|
||||||
|
guard={(e) =>
|
||||||
|
sceneInfra.camControls.wasDragging === false &&
|
||||||
|
btnName(e).right === true
|
||||||
|
}
|
||||||
|
menuTargetElement={videoWrapperRef}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import toast from 'react-hot-toast'
|
|||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
import { Marked } from '@ts-stack/markdown'
|
import { Marked } from '@ts-stack/markdown'
|
||||||
|
import { getReleaseUrl } from 'routes/Settings'
|
||||||
|
|
||||||
export function ToastUpdate({
|
export function ToastUpdate({
|
||||||
version,
|
version,
|
||||||
@ -32,10 +33,8 @@ export function ToastUpdate({
|
|||||||
A new update has downloaded and will be available next time you
|
A new update has downloaded and will be available next time you
|
||||||
start the app. You can view the release notes{' '}
|
start the app. You can view the release notes{' '}
|
||||||
<a
|
<a
|
||||||
onClick={openExternalBrowserIfDesktop(
|
onClick={openExternalBrowserIfDesktop(getReleaseUrl(version))}
|
||||||
`https://github.com/KittyCAD/modeling-app/releases/tag/v${version}`
|
href={getReleaseUrl(version)}
|
||||||
)}
|
|
||||||
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${version}`}
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
|
@ -22,6 +22,7 @@ import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
|||||||
import { normaliseAngle } from '../../lib/utils'
|
import { normaliseAngle } from '../../lib/utils'
|
||||||
import { kclManager } from 'lib/singletons'
|
import { kclManager } from 'lib/singletons'
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
|
import { KclCommandValue } from 'lib/commandTypes'
|
||||||
|
|
||||||
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
|
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
|
||||||
|
|
||||||
@ -63,6 +64,57 @@ export function angleLengthInfo({
|
|||||||
return { enabled, transforms }
|
return { enabled, transforms }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function applyConstraintLength({
|
||||||
|
length,
|
||||||
|
selectionRanges,
|
||||||
|
}: {
|
||||||
|
length: KclCommandValue
|
||||||
|
selectionRanges: Selections
|
||||||
|
}) {
|
||||||
|
const ast = kclManager.ast
|
||||||
|
const angleLength = angleLengthInfo({ selectionRanges })
|
||||||
|
if (err(angleLength)) return angleLength
|
||||||
|
const { transforms } = angleLength
|
||||||
|
|
||||||
|
let distanceExpression: Expr = length.valueAst
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To be "constrained", the value must be a binary expression, a named value, or a function call.
|
||||||
|
* If it has a variable name, we need to insert a variable declaration at the correct index.
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
'variableName' in length &&
|
||||||
|
length.variableName &&
|
||||||
|
length.insertIndex !== undefined
|
||||||
|
) {
|
||||||
|
const newBody = [...ast.body]
|
||||||
|
newBody.splice(length.insertIndex, 0, length.variableDeclarationAst)
|
||||||
|
ast.body = newBody
|
||||||
|
distanceExpression = createIdentifier(length.variableName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isExprBinaryPart(distanceExpression)) {
|
||||||
|
return new Error('Invalid valueNode, is not a BinaryPart')
|
||||||
|
}
|
||||||
|
|
||||||
|
const retval = transformAstSketchLines({
|
||||||
|
ast,
|
||||||
|
selectionRanges,
|
||||||
|
transformInfos: transforms,
|
||||||
|
programMemory: kclManager.programMemory,
|
||||||
|
referenceSegName: '',
|
||||||
|
forceValueUsedInTransform: distanceExpression,
|
||||||
|
})
|
||||||
|
if (err(retval)) return Promise.reject(retval)
|
||||||
|
|
||||||
|
const { modifiedAst: _modifiedAst, pathToNodeMap } = retval
|
||||||
|
|
||||||
|
return {
|
||||||
|
modifiedAst: _modifiedAst,
|
||||||
|
pathToNodeMap,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function applyConstraintAngleLength({
|
export async function applyConstraintAngleLength({
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
angleOrLength = 'setLength',
|
angleOrLength = 'setLength',
|
||||||
|
@ -41,7 +41,10 @@ export function UnitsMenu() {
|
|||||||
close()
|
close()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{baseUnitLabels[unit]}
|
<span className="flex-1">{baseUnitLabels[unit]}</span>
|
||||||
|
{unit === settings.context.modeling.defaultUnit.current && (
|
||||||
|
<span className="text-chalkboard-60">current</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
66
src/components/ViewControlMenu.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuDivider,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuItemRefresh,
|
||||||
|
ContextMenuProps,
|
||||||
|
} from './ContextMenu'
|
||||||
|
import { AxisNames, VIEW_NAMES_SEMANTIC } from 'lib/constants'
|
||||||
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { sceneInfra } from 'lib/singletons'
|
||||||
|
|
||||||
|
export function useViewControlMenuItems() {
|
||||||
|
const { send: modelingSend } = useModelingContext()
|
||||||
|
const menuItems = useMemo(
|
||||||
|
() => [
|
||||||
|
...Object.entries(VIEW_NAMES_SEMANTIC).map(([axisName, axisSemantic]) => (
|
||||||
|
<ContextMenuItem
|
||||||
|
key={axisName}
|
||||||
|
onClick={() => {
|
||||||
|
sceneInfra.camControls
|
||||||
|
.updateCameraToAxis(axisName as AxisNames)
|
||||||
|
.catch(reportRejection)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{axisSemantic} view
|
||||||
|
</ContextMenuItem>
|
||||||
|
)),
|
||||||
|
<ContextMenuDivider />,
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset view
|
||||||
|
</ContextMenuItem>,
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
modelingSend({ type: 'Center camera on selection' })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Center view on selection
|
||||||
|
</ContextMenuItem>,
|
||||||
|
<ContextMenuDivider />,
|
||||||
|
<ContextMenuItemRefresh />,
|
||||||
|
],
|
||||||
|
[VIEW_NAMES_SEMANTIC]
|
||||||
|
)
|
||||||
|
return menuItems
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ViewControlContextMenu({
|
||||||
|
menuTargetElement: wrapperRef,
|
||||||
|
...props
|
||||||
|
}: ContextMenuProps) {
|
||||||
|
const menuItems = useViewControlMenuItems()
|
||||||
|
return (
|
||||||
|
<ContextMenu
|
||||||
|
data-testid="view-controls-menu"
|
||||||
|
menuTargetElement={wrapperRef}
|
||||||
|
items={menuItems}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
327
src/editor/plugins/lsp/kcl/colors.ts
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
import {
|
||||||
|
EditorView,
|
||||||
|
WidgetType,
|
||||||
|
ViewUpdate,
|
||||||
|
ViewPlugin,
|
||||||
|
DecorationSet,
|
||||||
|
Decoration,
|
||||||
|
} from '@codemirror/view'
|
||||||
|
import { Range, Extension, Text } from '@codemirror/state'
|
||||||
|
import { NodeProp, Tree } from '@lezer/common'
|
||||||
|
import { language, syntaxTree } from '@codemirror/language'
|
||||||
|
|
||||||
|
interface PickerState {
|
||||||
|
from: number
|
||||||
|
to: number
|
||||||
|
alpha: string
|
||||||
|
colorType: ColorType
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WidgetOptions extends PickerState {
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ColorData = Omit<WidgetOptions, 'from' | 'to'>
|
||||||
|
|
||||||
|
const pickerState = new WeakMap<HTMLInputElement, PickerState>()
|
||||||
|
|
||||||
|
export enum ColorType {
|
||||||
|
hex = 'HEX',
|
||||||
|
}
|
||||||
|
|
||||||
|
const hexRegex = /(^|\b)(#[0-9a-f]{3,9})(\b|$)/i
|
||||||
|
|
||||||
|
function discoverColorsInKCL(
|
||||||
|
syntaxTree: Tree,
|
||||||
|
from: number,
|
||||||
|
to: number,
|
||||||
|
typeName: string,
|
||||||
|
doc: Text,
|
||||||
|
language?: string
|
||||||
|
): WidgetOptions | Array<WidgetOptions> | null {
|
||||||
|
switch (typeName) {
|
||||||
|
case 'Program':
|
||||||
|
case 'VariableDeclaration':
|
||||||
|
case 'CallExpression':
|
||||||
|
case 'ObjectExpression':
|
||||||
|
case 'ObjectProperty':
|
||||||
|
case 'ArgumentList':
|
||||||
|
case 'PipeExpression': {
|
||||||
|
let innerTree = syntaxTree.resolveInner(from, 0).tree
|
||||||
|
|
||||||
|
if (!innerTree) {
|
||||||
|
innerTree = syntaxTree.resolveInner(from, 1).tree
|
||||||
|
if (!innerTree) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlayTree = innerTree.prop(NodeProp.mounted)?.tree
|
||||||
|
|
||||||
|
if (overlayTree?.type.name !== 'Styles') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const ret: Array<WidgetOptions> = []
|
||||||
|
overlayTree.iterate({
|
||||||
|
from: 0,
|
||||||
|
to: overlayTree.length,
|
||||||
|
enter: ({ type, from: overlayFrom, to: overlayTo }) => {
|
||||||
|
const maybeWidgetOptions = discoverColorsInKCL(
|
||||||
|
syntaxTree,
|
||||||
|
// We add one because the tree doesn't include the
|
||||||
|
// quotation mark from the style tag
|
||||||
|
from + 1 + overlayFrom,
|
||||||
|
from + 1 + overlayTo,
|
||||||
|
type.name,
|
||||||
|
doc,
|
||||||
|
language
|
||||||
|
)
|
||||||
|
|
||||||
|
if (maybeWidgetOptions) {
|
||||||
|
if (Array.isArray(maybeWidgetOptions)) {
|
||||||
|
console.error('Unexpected nested overlays')
|
||||||
|
ret.push(...maybeWidgetOptions)
|
||||||
|
} else {
|
||||||
|
ret.push(maybeWidgetOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'String': {
|
||||||
|
const result = parseColorLiteral(doc.sliceString(from, to))
|
||||||
|
if (!result) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseColorLiteral(colorLiteral: string): ColorData | null {
|
||||||
|
const literal = colorLiteral.replace(/"/g, '')
|
||||||
|
const match = hexRegex.exec(literal)
|
||||||
|
if (!match) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const [color, alpha] = toFullHex(literal)
|
||||||
|
|
||||||
|
return {
|
||||||
|
colorType: ColorType.hex,
|
||||||
|
color,
|
||||||
|
alpha,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function colorPickersDecorations(
|
||||||
|
view: EditorView,
|
||||||
|
discoverColors: typeof discoverColorsInKCL
|
||||||
|
) {
|
||||||
|
const widgets: Array<Range<Decoration>> = []
|
||||||
|
|
||||||
|
const st = syntaxTree(view.state)
|
||||||
|
|
||||||
|
for (const range of view.visibleRanges) {
|
||||||
|
st.iterate({
|
||||||
|
from: range.from,
|
||||||
|
to: range.to,
|
||||||
|
enter: ({ type, from, to }) => {
|
||||||
|
const maybeWidgetOptions = discoverColors(
|
||||||
|
st,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
type.name,
|
||||||
|
view.state.doc,
|
||||||
|
view.state.facet(language)?.name
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!maybeWidgetOptions) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(maybeWidgetOptions)) {
|
||||||
|
widgets.push(
|
||||||
|
Decoration.widget({
|
||||||
|
widget: new ColorPickerWidget(maybeWidgetOptions),
|
||||||
|
side: 1,
|
||||||
|
}).range(maybeWidgetOptions.from)
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const wo of maybeWidgetOptions) {
|
||||||
|
widgets.push(
|
||||||
|
Decoration.widget({
|
||||||
|
widget: new ColorPickerWidget(wo),
|
||||||
|
side: 1,
|
||||||
|
}).range(wo.from)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return Decoration.set(widgets)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toFullHex(color: string): string[] {
|
||||||
|
if (color.length === 4) {
|
||||||
|
// 3-char hex
|
||||||
|
return [
|
||||||
|
`#${color[1].repeat(2)}${color[2].repeat(2)}${color[3].repeat(2)}`,
|
||||||
|
'',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (color.length === 5) {
|
||||||
|
// 4-char hex (alpha)
|
||||||
|
return [
|
||||||
|
`#${color[1].repeat(2)}${color[2].repeat(2)}${color[3].repeat(2)}`,
|
||||||
|
color[4].repeat(2),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (color.length === 9) {
|
||||||
|
// 8-char hex (alpha)
|
||||||
|
return [`#${color.slice(1, -2)}`, color.slice(-2)]
|
||||||
|
}
|
||||||
|
|
||||||
|
return [color, '']
|
||||||
|
}
|
||||||
|
|
||||||
|
export const wrapperClassName = 'cm-css-color-picker-wrapper'
|
||||||
|
|
||||||
|
class ColorPickerWidget extends WidgetType {
|
||||||
|
private readonly state: PickerState
|
||||||
|
private readonly color: string
|
||||||
|
|
||||||
|
constructor({ color, ...state }: WidgetOptions) {
|
||||||
|
super()
|
||||||
|
this.state = state
|
||||||
|
this.color = color
|
||||||
|
}
|
||||||
|
|
||||||
|
eq(other: ColorPickerWidget) {
|
||||||
|
return (
|
||||||
|
other.state.colorType === this.state.colorType &&
|
||||||
|
other.color === this.color &&
|
||||||
|
other.state.from === this.state.from &&
|
||||||
|
other.state.to === this.state.to &&
|
||||||
|
other.state.alpha === this.state.alpha
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
toDOM() {
|
||||||
|
const picker = document.createElement('input')
|
||||||
|
pickerState.set(picker, this.state)
|
||||||
|
picker.type = 'color'
|
||||||
|
picker.value = this.color
|
||||||
|
|
||||||
|
const wrapper = document.createElement('span')
|
||||||
|
wrapper.appendChild(picker)
|
||||||
|
wrapper.className = wrapperClassName
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoreEvent() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const colorPickerTheme = EditorView.baseTheme({
|
||||||
|
[`.${wrapperClassName}`]: {
|
||||||
|
display: 'inline-block',
|
||||||
|
outline: '1px solid #eee',
|
||||||
|
marginRight: '0.6ch',
|
||||||
|
height: '1em',
|
||||||
|
width: '1em',
|
||||||
|
transform: 'translateY(1px)',
|
||||||
|
},
|
||||||
|
[`.${wrapperClassName} input[type="color"]`]: {
|
||||||
|
cursor: 'pointer',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
padding: 0,
|
||||||
|
border: 'none',
|
||||||
|
'&::-webkit-color-swatch-wrapper': {
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
'&::-webkit-color-swatch': {
|
||||||
|
border: 'none',
|
||||||
|
},
|
||||||
|
'&::-moz-color-swatch': {
|
||||||
|
border: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
interface IFactoryOptions {
|
||||||
|
discoverColors: typeof discoverColorsInKCL
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeColorPicker = (options: IFactoryOptions) =>
|
||||||
|
ViewPlugin.fromClass(
|
||||||
|
class ColorPickerViewPlugin {
|
||||||
|
decorations: DecorationSet
|
||||||
|
|
||||||
|
constructor(view: EditorView) {
|
||||||
|
this.decorations = colorPickersDecorations(view, options.discoverColors)
|
||||||
|
}
|
||||||
|
|
||||||
|
update(update: ViewUpdate) {
|
||||||
|
if (update.docChanged || update.viewportChanged) {
|
||||||
|
this.decorations = colorPickersDecorations(
|
||||||
|
update.view,
|
||||||
|
options.discoverColors
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
decorations: (v) => v.decorations,
|
||||||
|
eventHandlers: {
|
||||||
|
change: (e, view) => {
|
||||||
|
const target = e.target as HTMLInputElement
|
||||||
|
if (
|
||||||
|
target.nodeName !== 'INPUT' ||
|
||||||
|
!target.parentElement ||
|
||||||
|
!target.parentElement.classList.contains(wrapperClassName)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = pickerState.get(target)!
|
||||||
|
|
||||||
|
let converted = '"' + target.value + data.alpha + '"'
|
||||||
|
|
||||||
|
view.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: data.from,
|
||||||
|
to: data.to,
|
||||||
|
insert: converted,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const colorPicker: Extension = [
|
||||||
|
makeColorPicker({ discoverColors: discoverColorsInKCL }),
|
||||||
|
colorPickerTheme,
|
||||||
|
]
|
@ -17,6 +17,7 @@ import { kclPlugin } from '.'
|
|||||||
import type * as LSP from 'vscode-languageserver-protocol'
|
import type * as LSP from 'vscode-languageserver-protocol'
|
||||||
// @ts-ignore: No types available
|
// @ts-ignore: No types available
|
||||||
import { parser } from './kcl.grammar'
|
import { parser } from './kcl.grammar'
|
||||||
|
import { colorPicker } from './colors'
|
||||||
|
|
||||||
export interface LanguageOptions {
|
export interface LanguageOptions {
|
||||||
workspaceFolders: LSP.WorkspaceFolder[]
|
workspaceFolders: LSP.WorkspaceFolder[]
|
||||||
@ -54,14 +55,14 @@ export const KclLanguage = LRLanguage.define({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export function kcl(options: LanguageOptions) {
|
export function kcl(options: LanguageOptions) {
|
||||||
return new LanguageSupport(
|
return new LanguageSupport(KclLanguage, [
|
||||||
KclLanguage,
|
colorPicker,
|
||||||
kclPlugin({
|
kclPlugin({
|
||||||
documentUri: options.documentUri,
|
documentUri: options.documentUri,
|
||||||
workspaceFolders: options.workspaceFolders,
|
workspaceFolders: options.workspaceFolders,
|
||||||
allowHTMLContent: true,
|
allowHTMLContent: true,
|
||||||
client: options.client,
|
client: options.client,
|
||||||
processLspNotification: options.processLspNotification,
|
processLspNotification: options.processLspNotification,
|
||||||
})
|
}),
|
||||||
)
|
])
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { LspWorkerEventType } from '@kittycad/codemirror-lsp-client'
|
import { LspWorkerEventType } from '@kittycad/codemirror-lsp-client'
|
||||||
|
|
||||||
import { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
|
|
||||||
|
|
||||||
export enum LspWorker {
|
export enum LspWorker {
|
||||||
Kcl = 'kcl',
|
Kcl = 'kcl',
|
||||||
Copilot = 'copilot',
|
Copilot = 'copilot',
|
||||||
@ -9,7 +7,6 @@ export enum LspWorker {
|
|||||||
export interface KclWorkerOptions {
|
export interface KclWorkerOptions {
|
||||||
wasmUrl: string
|
wasmUrl: string
|
||||||
token: string
|
token: string
|
||||||
baseUnit: UnitLength
|
|
||||||
apiBaseUrl: string
|
apiBaseUrl: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,6 @@ import {
|
|||||||
KclWorkerOptions,
|
KclWorkerOptions,
|
||||||
CopilotWorkerOptions,
|
CopilotWorkerOptions,
|
||||||
} from 'editor/plugins/lsp/types'
|
} from 'editor/plugins/lsp/types'
|
||||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
|
||||||
import { err, reportRejection } from 'lib/trap'
|
import { err, reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
const intoServer: IntoServer = new IntoServer()
|
const intoServer: IntoServer = new IntoServer()
|
||||||
@ -46,14 +45,12 @@ export async function copilotLspRun(
|
|||||||
|
|
||||||
export async function kclLspRun(
|
export async function kclLspRun(
|
||||||
config: ServerConfig,
|
config: ServerConfig,
|
||||||
engineCommandManager: EngineCommandManager | null,
|
|
||||||
token: string,
|
token: string,
|
||||||
baseUnit: string,
|
|
||||||
baseUrl: string
|
baseUrl: string
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
console.log('start kcl lsp')
|
console.log('start kcl lsp')
|
||||||
await kcl_lsp_run(config, engineCommandManager, baseUnit, token, baseUrl)
|
await kcl_lsp_run(config, null, undefined, token, baseUrl)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.log('kcl lsp failed', e)
|
console.log('kcl lsp failed', e)
|
||||||
// We can't restart here because a moved value, we should do this another way.
|
// We can't restart here because a moved value, we should do this another way.
|
||||||
@ -82,13 +79,7 @@ onmessage = function (event: MessageEvent) {
|
|||||||
switch (worker) {
|
switch (worker) {
|
||||||
case LspWorker.Kcl:
|
case LspWorker.Kcl:
|
||||||
const kclData = eventData as KclWorkerOptions
|
const kclData = eventData as KclWorkerOptions
|
||||||
await kclLspRun(
|
await kclLspRun(config, kclData.token, kclData.apiBaseUrl)
|
||||||
config,
|
|
||||||
null,
|
|
||||||
kclData.token,
|
|
||||||
kclData.baseUnit,
|
|
||||||
kclData.apiBaseUrl
|
|
||||||
)
|
|
||||||
break
|
break
|
||||||
case LspWorker.Copilot:
|
case LspWorker.Copilot:
|
||||||
let copilotData = eventData as CopilotWorkerOptions
|
let copilotData = eventData as CopilotWorkerOptions
|
||||||
|
@ -2,7 +2,7 @@ import { useLayoutEffect, useEffect, useRef } from 'react'
|
|||||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||||
import { deferExecution } from 'lib/utils'
|
import { deferExecution } from 'lib/utils'
|
||||||
import { Themes } from 'lib/theme'
|
import { Themes } from 'lib/theme'
|
||||||
import { makeDefaultPlanes, modifyGrid } from 'lang/wasm'
|
import { makeDefaultPlanes } from 'lang/wasm'
|
||||||
import { useModelingContext } from './useModelingContext'
|
import { useModelingContext } from './useModelingContext'
|
||||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
import { useAppState, useAppStream } from 'AppState'
|
import { useAppState, useAppStream } from 'AppState'
|
||||||
@ -56,9 +56,6 @@ export function useSetupEngineManager(
|
|||||||
makeDefaultPlanes: () => {
|
makeDefaultPlanes: () => {
|
||||||
return makeDefaultPlanes(kclManager.engineCommandManager)
|
return makeDefaultPlanes(kclManager.engineCommandManager)
|
||||||
},
|
},
|
||||||
modifyGrid: (hidden: boolean) => {
|
|
||||||
return modifyGrid(kclManager.engineCommandManager, hidden)
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
hasSetNonZeroDimensions.current = true
|
hasSetNonZeroDimensions.current = true
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,8 @@ export function useConvertToVariable(range?: SourceRange) {
|
|||||||
}, [enable])
|
}, [enable])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Return early if there are no selection ranges for whatever reason
|
||||||
|
if (!context.selectionRanges) return
|
||||||
const parsed = ast
|
const parsed = ast
|
||||||
|
|
||||||
const meta = isNodeSafeToReplace(
|
const meta = isNodeSafeToReplace(
|
||||||
|
@ -317,3 +317,8 @@ code {
|
|||||||
#code-mirror-override .cm-editor {
|
#code-mirror-override .cm-editor {
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Can't use #code-mirror-override here as we're outside of this div */
|
||||||
|
.body-bg .cm-diagnosticAction {
|
||||||
|
@apply bg-primary;
|
||||||
|
}
|
||||||
|
@ -45,6 +45,7 @@ import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
|
|||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import { ExtrudeFacePlane } from 'machines/modelingMachine'
|
import { ExtrudeFacePlane } from 'machines/modelingMachine'
|
||||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
|
import { KclExpressionWithVariable } from 'lib/commandTypes'
|
||||||
|
|
||||||
export function startSketchOnDefault(
|
export function startSketchOnDefault(
|
||||||
node: Node<Program>,
|
node: Node<Program>,
|
||||||
@ -590,6 +591,25 @@ export function addOffsetPlane({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a modified clone of an AST with a named constant inserted into the body
|
||||||
|
*/
|
||||||
|
export function insertNamedConstant({
|
||||||
|
node,
|
||||||
|
newExpression,
|
||||||
|
}: {
|
||||||
|
node: Node<Program>
|
||||||
|
newExpression: KclExpressionWithVariable
|
||||||
|
}): Node<Program> {
|
||||||
|
const ast = structuredClone(node)
|
||||||
|
ast.body.splice(
|
||||||
|
newExpression.insertIndex,
|
||||||
|
0,
|
||||||
|
newExpression.variableDeclarationAst
|
||||||
|
)
|
||||||
|
return ast
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modify the AST to create a new sketch using the variable declaration
|
* Modify the AST to create a new sketch using the variable declaration
|
||||||
* of an offset plane. The new sketch just has to come after the offset
|
* of an offset plane. The new sketch just has to come after the offset
|
||||||
@ -933,6 +953,31 @@ export function giveSketchFnCallTag(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace a
|
||||||
|
*/
|
||||||
|
export function replaceValueAtNodePath({
|
||||||
|
ast,
|
||||||
|
pathToNode,
|
||||||
|
newExpressionString,
|
||||||
|
}: {
|
||||||
|
ast: Node<Program>
|
||||||
|
pathToNode: PathToNode
|
||||||
|
newExpressionString: string
|
||||||
|
}) {
|
||||||
|
const replaceCheckResult = isNodeSafeToReplacePath(ast, pathToNode)
|
||||||
|
if (err(replaceCheckResult)) {
|
||||||
|
return replaceCheckResult
|
||||||
|
}
|
||||||
|
const { isSafe, value, replacer } = replaceCheckResult
|
||||||
|
|
||||||
|
if (!isSafe || value.type === 'Identifier') {
|
||||||
|
return new Error('Not safe to replace')
|
||||||
|
}
|
||||||
|
|
||||||
|
return replacer(ast, newExpressionString)
|
||||||
|
}
|
||||||
|
|
||||||
export function moveValueIntoNewVariablePath(
|
export function moveValueIntoNewVariablePath(
|
||||||
ast: Node<Program>,
|
ast: Node<Program>,
|
||||||
programMemory: ProgramMemory,
|
programMemory: ProgramMemory,
|
||||||
|
@ -22,7 +22,7 @@ import {
|
|||||||
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
|
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
|
||||||
import { createLiteral } from 'lang/modifyAst'
|
import { createLiteral } from 'lang/modifyAst'
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selection, Selections } from 'lib/selections'
|
||||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||||
import { VITE_KC_DEV_TOKEN } from 'env'
|
import { VITE_KC_DEV_TOKEN } from 'env'
|
||||||
import { isOverlap } from 'lib/utils'
|
import { isOverlap } from 'lib/utils'
|
||||||
@ -40,7 +40,6 @@ beforeAll(async () => {
|
|||||||
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
|
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
|
||||||
setMediaStream: () => {},
|
setMediaStream: () => {},
|
||||||
setIsStreamReady: () => {},
|
setIsStreamReady: () => {},
|
||||||
modifyGrid: async () => {},
|
|
||||||
callbackOnEngineLiteConnect: () => {
|
callbackOnEngineLiteConnect: () => {
|
||||||
resolve(true)
|
resolve(true)
|
||||||
},
|
},
|
||||||
@ -118,13 +117,8 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
|
|||||||
code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length,
|
code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length,
|
||||||
true,
|
true,
|
||||||
]
|
]
|
||||||
const selection: Selections = {
|
const selection: Selection = {
|
||||||
graphSelections: [
|
|
||||||
{
|
|
||||||
codeRef: codeRefFromRange(segmentRange, ast),
|
codeRef: codeRefFromRange(segmentRange, ast),
|
||||||
},
|
|
||||||
],
|
|
||||||
otherSelections: [],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// executeAst and artifactGraph
|
// executeAst and artifactGraph
|
||||||
|
@ -29,7 +29,7 @@ import {
|
|||||||
sketchLineHelperMap,
|
sketchLineHelperMap,
|
||||||
} from '../std/sketch'
|
} from '../std/sketch'
|
||||||
import { err, trap } from 'lib/trap'
|
import { err, trap } from 'lib/trap'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selection, Selections } from 'lib/selections'
|
||||||
import { KclCommandValue } from 'lib/commandTypes'
|
import { KclCommandValue } from 'lib/commandTypes'
|
||||||
import {
|
import {
|
||||||
Artifact,
|
Artifact,
|
||||||
@ -99,14 +99,9 @@ export function modifyAstWithEdgeTreatmentAndTag(
|
|||||||
const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison
|
const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison
|
||||||
|
|
||||||
for (const selection of selections.graphSelections) {
|
for (const selection of selections.graphSelections) {
|
||||||
const singleSelection = {
|
|
||||||
graphSelections: [selection],
|
|
||||||
otherSelections: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = getPathToExtrudeForSegmentSelection(
|
const result = getPathToExtrudeForSegmentSelection(
|
||||||
clonedAstForGetExtrude,
|
clonedAstForGetExtrude,
|
||||||
singleSelection,
|
selection,
|
||||||
artifactGraph
|
artifactGraph
|
||||||
)
|
)
|
||||||
if (err(result)) return result
|
if (err(result)) return result
|
||||||
@ -259,12 +254,12 @@ function insertParametersIntoAst(
|
|||||||
|
|
||||||
export function getPathToExtrudeForSegmentSelection(
|
export function getPathToExtrudeForSegmentSelection(
|
||||||
ast: Program,
|
ast: Program,
|
||||||
selection: Selections,
|
selection: Selection,
|
||||||
artifactGraph: ArtifactGraph
|
artifactGraph: ArtifactGraph
|
||||||
): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error {
|
): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error {
|
||||||
const pathToSegmentNode = getNodePathFromSourceRange(
|
const pathToSegmentNode = getNodePathFromSourceRange(
|
||||||
ast,
|
ast,
|
||||||
selection.graphSelections[0]?.codeRef?.range
|
selection.codeRef?.range
|
||||||
)
|
)
|
||||||
|
|
||||||
const varDecNode = getNodeFromPath<VariableDeclaration>(
|
const varDecNode = getNodeFromPath<VariableDeclaration>(
|
||||||
@ -308,7 +303,7 @@ async function updateAstAndFocus(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mutateAstWithTagForSketchSegment(
|
export function mutateAstWithTagForSketchSegment(
|
||||||
astClone: Node<Program>,
|
astClone: Node<Program>,
|
||||||
pathToSegmentNode: PathToNode
|
pathToSegmentNode: PathToNode
|
||||||
): { modifiedAst: Program; tag: string } | Error {
|
): { modifiedAst: Program; tag: string } | Error {
|
||||||
@ -340,7 +335,7 @@ function mutateAstWithTagForSketchSegment(
|
|||||||
return { modifiedAst: astClone, tag }
|
return { modifiedAst: astClone, tag }
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEdgeTagCall(
|
export function getEdgeTagCall(
|
||||||
tag: string,
|
tag: string,
|
||||||
artifact: Artifact
|
artifact: Artifact
|
||||||
): Node<Identifier | CallExpression> {
|
): Node<Identifier | CallExpression> {
|
||||||
|
154
src/lang/modifyAst/addRevolve.ts
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import { err } from 'lib/trap'
|
||||||
|
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
|
||||||
|
import {
|
||||||
|
Program,
|
||||||
|
PathToNode,
|
||||||
|
Expr,
|
||||||
|
CallExpression,
|
||||||
|
PipeExpression,
|
||||||
|
VariableDeclarator,
|
||||||
|
} from 'lang/wasm'
|
||||||
|
import { Selections } from 'lib/selections'
|
||||||
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
|
import {
|
||||||
|
createLiteral,
|
||||||
|
createCallExpressionStdLib,
|
||||||
|
createObjectExpression,
|
||||||
|
createIdentifier,
|
||||||
|
createPipeExpression,
|
||||||
|
findUniqueName,
|
||||||
|
createVariableDeclaration,
|
||||||
|
} from 'lang/modifyAst'
|
||||||
|
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||||
|
import {
|
||||||
|
mutateAstWithTagForSketchSegment,
|
||||||
|
getEdgeTagCall,
|
||||||
|
} from 'lang/modifyAst/addEdgeTreatment'
|
||||||
|
export function revolveSketch(
|
||||||
|
ast: Node<Program>,
|
||||||
|
pathToSketchNode: PathToNode,
|
||||||
|
shouldPipe = false,
|
||||||
|
angle: Expr = createLiteral(4),
|
||||||
|
axis: Selections
|
||||||
|
):
|
||||||
|
| {
|
||||||
|
modifiedAst: Node<Program>
|
||||||
|
pathToSketchNode: PathToNode
|
||||||
|
pathToRevolveArg: PathToNode
|
||||||
|
}
|
||||||
|
| Error {
|
||||||
|
const clonedAst = structuredClone(ast)
|
||||||
|
const sketchNode = getNodeFromPath(clonedAst, pathToSketchNode)
|
||||||
|
if (err(sketchNode)) return sketchNode
|
||||||
|
|
||||||
|
// testing code
|
||||||
|
const pathToAxisSelection = getNodePathFromSourceRange(
|
||||||
|
clonedAst,
|
||||||
|
axis.graphSelections[0]?.codeRef.range
|
||||||
|
)
|
||||||
|
|
||||||
|
const lineNode = getNodeFromPath<CallExpression>(
|
||||||
|
clonedAst,
|
||||||
|
pathToAxisSelection,
|
||||||
|
'CallExpression'
|
||||||
|
)
|
||||||
|
if (err(lineNode)) return lineNode
|
||||||
|
|
||||||
|
// TODO Kevin: What if |> close(%)?
|
||||||
|
// TODO Kevin: What if opposite edge
|
||||||
|
// TODO Kevin: What if the edge isn't planar to the sketch?
|
||||||
|
// TODO Kevin: add a tag.
|
||||||
|
const tagResult = mutateAstWithTagForSketchSegment(
|
||||||
|
clonedAst,
|
||||||
|
pathToAxisSelection
|
||||||
|
)
|
||||||
|
|
||||||
|
// Have the tag whether it is already created or a new one is generated
|
||||||
|
if (err(tagResult)) return tagResult
|
||||||
|
const { tag } = tagResult
|
||||||
|
|
||||||
|
/* Original Code */
|
||||||
|
const { node: sketchExpression } = sketchNode
|
||||||
|
|
||||||
|
// determine if sketchExpression is in a pipeExpression or not
|
||||||
|
const sketchPipeExpressionNode = getNodeFromPath<PipeExpression>(
|
||||||
|
clonedAst,
|
||||||
|
pathToSketchNode,
|
||||||
|
'PipeExpression'
|
||||||
|
)
|
||||||
|
if (err(sketchPipeExpressionNode)) return sketchPipeExpressionNode
|
||||||
|
const { node: sketchPipeExpression } = sketchPipeExpressionNode
|
||||||
|
const isInPipeExpression = sketchPipeExpression.type === 'PipeExpression'
|
||||||
|
|
||||||
|
const sketchVariableDeclaratorNode = getNodeFromPath<VariableDeclarator>(
|
||||||
|
clonedAst,
|
||||||
|
pathToSketchNode,
|
||||||
|
'VariableDeclarator'
|
||||||
|
)
|
||||||
|
if (err(sketchVariableDeclaratorNode)) return sketchVariableDeclaratorNode
|
||||||
|
const {
|
||||||
|
node: sketchVariableDeclarator,
|
||||||
|
shallowPath: sketchPathToDecleration,
|
||||||
|
} = sketchVariableDeclaratorNode
|
||||||
|
|
||||||
|
const axisSelection = axis?.graphSelections[0]?.artifact
|
||||||
|
|
||||||
|
if (!axisSelection) return new Error('Axis selection is missing.')
|
||||||
|
|
||||||
|
const revolveCall = createCallExpressionStdLib('revolve', [
|
||||||
|
createObjectExpression({
|
||||||
|
angle: angle,
|
||||||
|
axis: getEdgeTagCall(tag, axisSelection),
|
||||||
|
}),
|
||||||
|
createIdentifier(sketchVariableDeclarator.id.name),
|
||||||
|
])
|
||||||
|
|
||||||
|
if (shouldPipe) {
|
||||||
|
const pipeChain = createPipeExpression(
|
||||||
|
isInPipeExpression
|
||||||
|
? [...sketchPipeExpression.body, revolveCall]
|
||||||
|
: [sketchExpression as any, revolveCall]
|
||||||
|
)
|
||||||
|
|
||||||
|
sketchVariableDeclarator.init = pipeChain
|
||||||
|
const pathToRevolveArg: PathToNode = [
|
||||||
|
...sketchPathToDecleration,
|
||||||
|
['init', 'VariableDeclarator'],
|
||||||
|
['body', ''],
|
||||||
|
[pipeChain.body.length - 1, 'index'],
|
||||||
|
['arguments', 'CallExpression'],
|
||||||
|
[0, 'index'],
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
modifiedAst: clonedAst,
|
||||||
|
pathToSketchNode,
|
||||||
|
pathToRevolveArg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're not creating a pipe expression,
|
||||||
|
// but rather a separate constant for the extrusion
|
||||||
|
const name = findUniqueName(clonedAst, KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE)
|
||||||
|
const VariableDeclaration = createVariableDeclaration(name, revolveCall)
|
||||||
|
const sketchIndexInPathToNode =
|
||||||
|
sketchPathToDecleration.findIndex((a) => a[0] === 'body') + 1
|
||||||
|
const sketchIndexInBody = sketchPathToDecleration[sketchIndexInPathToNode][0]
|
||||||
|
if (typeof sketchIndexInBody !== 'number')
|
||||||
|
return new Error('expected sketchIndexInBody to be a number')
|
||||||
|
clonedAst.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)
|
||||||
|
|
||||||
|
const pathToRevolveArg: PathToNode = [
|
||||||
|
['body', ''],
|
||||||
|
[sketchIndexInBody + 1, 'index'],
|
||||||
|
['declaration', 'VariableDeclaration'],
|
||||||
|
['init', 'VariableDeclarator'],
|
||||||
|
['arguments', 'CallExpression'],
|
||||||
|
[0, 'index'],
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
modifiedAst: clonedAst,
|
||||||
|
pathToSketchNode: [...pathToSketchNode.slice(0, -1), [-1, 'index']],
|
||||||
|
pathToRevolveArg,
|
||||||
|
}
|
||||||
|
}
|
123
src/lang/modifyAst/addShell.ts
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import { ArtifactGraph } from 'lang/std/artifactGraph'
|
||||||
|
import { Selections } from 'lib/selections'
|
||||||
|
import { Expr } from 'wasm-lib/kcl/bindings/Expr'
|
||||||
|
import { Program } from 'wasm-lib/kcl/bindings/Program'
|
||||||
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
|
import { PathToNode, VariableDeclarator } from 'lang/wasm'
|
||||||
|
import {
|
||||||
|
getPathToExtrudeForSegmentSelection,
|
||||||
|
mutateAstWithTagForSketchSegment,
|
||||||
|
} from './addEdgeTreatment'
|
||||||
|
import { getNodeFromPath } from 'lang/queryAst'
|
||||||
|
import { err } from 'lib/trap'
|
||||||
|
import {
|
||||||
|
createLiteral,
|
||||||
|
createIdentifier,
|
||||||
|
findUniqueName,
|
||||||
|
createCallExpressionStdLib,
|
||||||
|
createObjectExpression,
|
||||||
|
createArrayExpression,
|
||||||
|
createVariableDeclaration,
|
||||||
|
} from 'lang/modifyAst'
|
||||||
|
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
|
||||||
|
|
||||||
|
export function addShell({
|
||||||
|
node,
|
||||||
|
selection,
|
||||||
|
artifactGraph,
|
||||||
|
thickness,
|
||||||
|
}: {
|
||||||
|
node: Node<Program>
|
||||||
|
selection: Selections
|
||||||
|
artifactGraph: ArtifactGraph
|
||||||
|
thickness: Expr
|
||||||
|
}): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } {
|
||||||
|
const modifiedAst = structuredClone(node)
|
||||||
|
|
||||||
|
// Look up the corresponding extrude
|
||||||
|
const clonedAstForGetExtrude = structuredClone(modifiedAst)
|
||||||
|
|
||||||
|
const expressions: Expr[] = []
|
||||||
|
let pathToExtrudeNode: PathToNode | undefined = undefined
|
||||||
|
for (const graphSelection of selection.graphSelections) {
|
||||||
|
const extrudeLookupResult = getPathToExtrudeForSegmentSelection(
|
||||||
|
clonedAstForGetExtrude,
|
||||||
|
graphSelection,
|
||||||
|
artifactGraph
|
||||||
|
)
|
||||||
|
if (err(extrudeLookupResult)) {
|
||||||
|
return new Error("Couldn't find extrude")
|
||||||
|
}
|
||||||
|
|
||||||
|
pathToExtrudeNode = extrudeLookupResult.pathToExtrudeNode
|
||||||
|
// Get the sketch ref from the selection
|
||||||
|
// TODO: this assumes the segment is piped directly from the sketch, with no intermediate `VariableDeclarator` between.
|
||||||
|
// We must find a technique for these situations that is robust to intermediate declarations
|
||||||
|
const sketchNode = getNodeFromPath<VariableDeclarator>(
|
||||||
|
modifiedAst,
|
||||||
|
graphSelection.codeRef.pathToNode,
|
||||||
|
'VariableDeclarator'
|
||||||
|
)
|
||||||
|
if (err(sketchNode)) {
|
||||||
|
return sketchNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedArtifact = graphSelection.artifact
|
||||||
|
if (!selectedArtifact) {
|
||||||
|
return new Error('Bad artifact')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check on the selection, and handle the wall vs cap casees
|
||||||
|
let expr: Expr
|
||||||
|
if (selectedArtifact.type === 'cap') {
|
||||||
|
expr = createLiteral(selectedArtifact.subType)
|
||||||
|
} else if (selectedArtifact.type === 'wall') {
|
||||||
|
const tagResult = mutateAstWithTagForSketchSegment(
|
||||||
|
modifiedAst,
|
||||||
|
extrudeLookupResult.pathToSegmentNode
|
||||||
|
)
|
||||||
|
if (err(tagResult)) return tagResult
|
||||||
|
const { tag } = tagResult
|
||||||
|
expr = createIdentifier(tag)
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
expressions.push(expr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pathToExtrudeNode) return new Error('No extrude found')
|
||||||
|
|
||||||
|
const extrudeNode = getNodeFromPath<VariableDeclarator>(
|
||||||
|
modifiedAst,
|
||||||
|
pathToExtrudeNode,
|
||||||
|
'VariableDeclarator'
|
||||||
|
)
|
||||||
|
if (err(extrudeNode)) {
|
||||||
|
return extrudeNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SHELL)
|
||||||
|
const shell = createCallExpressionStdLib('shell', [
|
||||||
|
createObjectExpression({
|
||||||
|
faces: createArrayExpression(expressions),
|
||||||
|
thickness,
|
||||||
|
}),
|
||||||
|
createIdentifier(extrudeNode.node.id.name),
|
||||||
|
])
|
||||||
|
const declaration = createVariableDeclaration(name, shell)
|
||||||
|
|
||||||
|
// TODO: check if we should append at the end like here or right after the extrude
|
||||||
|
modifiedAst.body.push(declaration)
|
||||||
|
const pathToNode: PathToNode = [
|
||||||
|
['body', ''],
|
||||||
|
[modifiedAst.body.length - 1, 'index'],
|
||||||
|
['declaration', 'VariableDeclaration'],
|
||||||
|
['init', 'VariableDeclarator'],
|
||||||
|
['arguments', 'CallExpression'],
|
||||||
|
[0, 'index'],
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
modifiedAst,
|
||||||
|
pathToNode,
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,7 @@ import {
|
|||||||
doesSceneHaveSweepableSketch,
|
doesSceneHaveSweepableSketch,
|
||||||
traverse,
|
traverse,
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
|
doesSceneHaveExtrudedSketch,
|
||||||
} from './queryAst'
|
} from './queryAst'
|
||||||
import { enginelessExecutor } from '../lib/testHelpers'
|
import { enginelessExecutor } from '../lib/testHelpers'
|
||||||
import {
|
import {
|
||||||
@ -654,6 +655,38 @@ extrude001 = extrude(10, sketch001)
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Testing doesSceneHaveExtrudedSketch', () => {
|
||||||
|
it('finds extruded sketch as variable', async () => {
|
||||||
|
const exampleCode = `sketch001 = startSketchOn('XZ')
|
||||||
|
|> circle({ center = [0, 0], radius = 1 }, %)
|
||||||
|
extrude001 = extrude(1, sketch001)
|
||||||
|
`
|
||||||
|
const ast = assertParse(exampleCode)
|
||||||
|
if (err(ast)) throw ast
|
||||||
|
const extrudable = doesSceneHaveExtrudedSketch(ast)
|
||||||
|
expect(extrudable).toBeTruthy()
|
||||||
|
})
|
||||||
|
it('finds extruded sketch in pipe', async () => {
|
||||||
|
const exampleCode = `extrude001 = startSketchOn('XZ')
|
||||||
|
|> circle({ center = [0, 0], radius = 1 }, %)
|
||||||
|
|> extrude(1, %)
|
||||||
|
`
|
||||||
|
const ast = assertParse(exampleCode)
|
||||||
|
if (err(ast)) throw ast
|
||||||
|
const extrudable = doesSceneHaveExtrudedSketch(ast)
|
||||||
|
expect(extrudable).toBeTruthy()
|
||||||
|
})
|
||||||
|
it('finds no extrusion with sketch only', async () => {
|
||||||
|
const exampleCode = `extrude001 = startSketchOn('XZ')
|
||||||
|
|> circle({ center = [0, 0], radius = 1 }, %)
|
||||||
|
`
|
||||||
|
const ast = assertParse(exampleCode)
|
||||||
|
if (err(ast)) throw ast
|
||||||
|
const extrudable = doesSceneHaveExtrudedSketch(ast)
|
||||||
|
expect(extrudable).toBeFalsy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('Testing traverse and pathToNode', () => {
|
describe('Testing traverse and pathToNode', () => {
|
||||||
it.each([
|
it.each([
|
||||||
['basic', '2.73'],
|
['basic', '2.73'],
|
||||||
|
@ -1064,6 +1064,35 @@ export function doesSceneHaveSweepableSketch(ast: Node<Program>, count = 1) {
|
|||||||
return Object.keys(theMap).length >= count
|
return Object.keys(theMap).length >= count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function doesSceneHaveExtrudedSketch(ast: Node<Program>) {
|
||||||
|
const theMap: any = {}
|
||||||
|
traverse(ast as any, {
|
||||||
|
enter(node) {
|
||||||
|
if (
|
||||||
|
node.type === 'VariableDeclarator' &&
|
||||||
|
node.init?.type === 'PipeExpression'
|
||||||
|
) {
|
||||||
|
for (const pipe of node.init.body) {
|
||||||
|
if (
|
||||||
|
pipe.type === 'CallExpression' &&
|
||||||
|
pipe.callee.name === 'extrude'
|
||||||
|
) {
|
||||||
|
theMap[node.id.name] = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
node.type === 'CallExpression' &&
|
||||||
|
node.callee.name === 'extrude' &&
|
||||||
|
node.arguments[1]?.type === 'Identifier'
|
||||||
|
) {
|
||||||
|
theMap[node.moduleId] = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return Object.keys(theMap).length > 0
|
||||||
|
}
|
||||||
|
|
||||||
export function getObjExprProperty(
|
export function getObjExprProperty(
|
||||||
node: ObjectExpression,
|
node: ObjectExpression,
|
||||||
propName: string
|
propName: string
|
||||||
|
@ -139,7 +139,6 @@ beforeAll(async () => {
|
|||||||
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
|
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
|
||||||
setMediaStream: () => {},
|
setMediaStream: () => {},
|
||||||
setIsStreamReady: () => {},
|
setIsStreamReady: () => {},
|
||||||
modifyGrid: async () => {},
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
callbackOnEngineLiteConnect: async () => {
|
callbackOnEngineLiteConnect: async () => {
|
||||||
const cacheEntries = Object.entries(codeToWriteCacheFor) as [
|
const cacheEntries = Object.entries(codeToWriteCacheFor) as [
|
||||||
|
@ -1399,7 +1399,6 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null
|
private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null
|
||||||
private modifyGrid: (hidden: boolean) => Promise<void> | null = () => null
|
|
||||||
|
|
||||||
private onEngineConnectionOpened = () => {}
|
private onEngineConnectionOpened = () => {}
|
||||||
private onEngineConnectionClosed = () => {}
|
private onEngineConnectionClosed = () => {}
|
||||||
@ -1432,7 +1431,6 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
height,
|
height,
|
||||||
token,
|
token,
|
||||||
makeDefaultPlanes,
|
makeDefaultPlanes,
|
||||||
modifyGrid,
|
|
||||||
settings = {
|
settings = {
|
||||||
pool: null,
|
pool: null,
|
||||||
theme: Themes.Dark,
|
theme: Themes.Dark,
|
||||||
@ -1452,14 +1450,12 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
height: number
|
height: number
|
||||||
token?: string
|
token?: string
|
||||||
makeDefaultPlanes: () => Promise<DefaultPlanes>
|
makeDefaultPlanes: () => Promise<DefaultPlanes>
|
||||||
modifyGrid: (hidden: boolean) => Promise<void>
|
|
||||||
settings?: SettingsViaQueryString
|
settings?: SettingsViaQueryString
|
||||||
}) {
|
}) {
|
||||||
if (settings) {
|
if (settings) {
|
||||||
this.settings = settings
|
this.settings = settings
|
||||||
}
|
}
|
||||||
this.makeDefaultPlanes = makeDefaultPlanes
|
this.makeDefaultPlanes = makeDefaultPlanes
|
||||||
this.modifyGrid = modifyGrid
|
|
||||||
if (width === 0 || height === 0) {
|
if (width === 0 || height === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1539,11 +1535,6 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
type: 'default_camera_get_settings',
|
type: 'default_camera_get_settings',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
// We want modify the grid first because we don't want it to flash.
|
|
||||||
// Ideally these would already be default hidden in engine (TODO do
|
|
||||||
// that) https://github.com/KittyCAD/engine/issues/2282
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.modifyGrid(!this.settings.showScaleGrid)?.then(async () => {
|
|
||||||
await this.initPlanes()
|
await this.initPlanes()
|
||||||
setIsStreamReady(true)
|
setIsStreamReady(true)
|
||||||
|
|
||||||
@ -1553,7 +1544,6 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
detail: this.engineConnection,
|
detail: this.engineConnection,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.engineConnection.addEventListener(
|
this.engineConnection.addEventListener(
|
||||||
@ -2212,15 +2202,6 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
}).catch(reportRejection)
|
}).catch(reportRejection)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the visibility of the scale grid in the engine scene.
|
|
||||||
* @param visible - whether to show or hide the scale grid
|
|
||||||
*/
|
|
||||||
setScaleGridVisibility(visible: boolean) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.modifyGrid(!visible)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some "objects" have the same source range, such as sketch_mode_start and start_path.
|
// Some "objects" have the same source range, such as sketch_mode_start and start_path.
|
||||||
// So when passing a range, we need to also specify the command type
|
// So when passing a range, we need to also specify the command type
|
||||||
mapRangeToObjectId(
|
mapRangeToObjectId(
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
import { parse, ParseResult } from './wasm'
|
import { initPromise, parse, ParseResult } from './wasm'
|
||||||
import { enginelessExecutor } from 'lib/testHelpers'
|
import { enginelessExecutor } from 'lib/testHelpers'
|
||||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
import { Program } from '../wasm-lib/kcl/bindings/Program'
|
import { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await initPromise
|
||||||
|
})
|
||||||
|
|
||||||
it('can execute parsed AST', async () => {
|
it('can execute parsed AST', async () => {
|
||||||
const code = `x = 1
|
const code = `x = 1
|
||||||
// A comment.`
|
// A comment.`
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import init, {
|
import init, {
|
||||||
parse_wasm,
|
parse_wasm,
|
||||||
recast_wasm,
|
recast_wasm,
|
||||||
execute_wasm,
|
execute,
|
||||||
kcl_lint,
|
kcl_lint,
|
||||||
modify_ast_for_sketch_wasm,
|
modify_ast_for_sketch_wasm,
|
||||||
is_points_ccw,
|
is_points_ccw,
|
||||||
get_tangential_arc_to_info,
|
get_tangential_arc_to_info,
|
||||||
program_memory_init,
|
program_memory_init,
|
||||||
make_default_planes,
|
make_default_planes,
|
||||||
modify_grid,
|
|
||||||
coredump,
|
coredump,
|
||||||
toml_stringify,
|
toml_stringify,
|
||||||
default_app_settings,
|
default_app_settings,
|
||||||
@ -43,7 +42,9 @@ import { Environment } from '../wasm-lib/kcl/bindings/Environment'
|
|||||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError'
|
import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError'
|
||||||
import { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
|
import { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
|
||||||
|
import { getAllCurrentSettings } from 'lib/settings/settingsUtils'
|
||||||
|
|
||||||
|
export type { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||||
export type { Program } from '../wasm-lib/kcl/bindings/Program'
|
export type { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||||
export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
|
export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
|
||||||
export type { ObjectExpression } from '../wasm-lib/kcl/bindings/ObjectExpression'
|
export type { ObjectExpression } from '../wasm-lib/kcl/bindings/ObjectExpression'
|
||||||
@ -92,12 +93,26 @@ export type { Solid } from '../wasm-lib/kcl/bindings/Solid'
|
|||||||
export type { KclValue } from '../wasm-lib/kcl/bindings/KclValue'
|
export type { KclValue } from '../wasm-lib/kcl/bindings/KclValue'
|
||||||
export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'
|
export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The first two items are the start and end points (byte offsets from the start of the file).
|
||||||
|
* The third item is whether the source range belongs to the 'main' file, i.e., the file currently
|
||||||
|
* being rendered/displayed in the editor (TODO we need to handle modules better in the frontend).
|
||||||
|
*/
|
||||||
export type SourceRange = [number, number, boolean]
|
export type SourceRange = [number, number, boolean]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a SourceRange as used inside the KCL interpreter into the above one for use in the
|
||||||
|
* frontend (essentially we're eagerly checking whether the frontend should care about the SourceRange
|
||||||
|
* so as not to expose details of the interpreter's current representation of module ids throughout
|
||||||
|
* the frontend).
|
||||||
|
*/
|
||||||
export function sourceRangeFromRust(s: RustSourceRange): SourceRange {
|
export function sourceRangeFromRust(s: RustSourceRange): SourceRange {
|
||||||
return [s[0], s[1], s[2] === 0]
|
return [s[0], s[1], s[2] === 0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a default SourceRange for testing or as a placeholder.
|
||||||
|
*/
|
||||||
export function defaultSourceRange(): SourceRange {
|
export function defaultSourceRange(): SourceRange {
|
||||||
return [0, 0, true]
|
return [0, 0, true]
|
||||||
}
|
}
|
||||||
@ -122,7 +137,7 @@ const initialise = async () => {
|
|||||||
const fullUrl = wasmUrl()
|
const fullUrl = wasmUrl()
|
||||||
const input = await fetch(fullUrl)
|
const input = await fetch(fullUrl)
|
||||||
const buffer = await input.arrayBuffer()
|
const buffer = await input.arrayBuffer()
|
||||||
return await init(buffer)
|
return await init({ module_or_path: buffer })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Error initialising WASM', e)
|
console.log('Error initialising WASM', e)
|
||||||
return Promise.reject(e)
|
return Promise.reject(e)
|
||||||
@ -163,6 +178,10 @@ export class ParseResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parsing was successful. There is guaranteed to be an AST and no fatal errors. There may or may
|
||||||
|
* not be warnings or non-fatal errors.
|
||||||
|
*/
|
||||||
class SuccessParseResult extends ParseResult {
|
class SuccessParseResult extends ParseResult {
|
||||||
program: Node<Program>
|
program: Node<Program>
|
||||||
|
|
||||||
@ -493,18 +512,19 @@ export const _executor = async (
|
|||||||
return Promise.reject(programMemoryOverride)
|
return Promise.reject(programMemoryOverride)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let baseUnit = 'mm'
|
let jsAppSettings = default_app_settings()
|
||||||
if (!TEST) {
|
if (!TEST) {
|
||||||
const getSettingsState = import('components/SettingsAuthProvider').then(
|
const lastSettingsSnapshot = await import(
|
||||||
(module) => module.getSettingsState
|
'components/SettingsAuthProvider'
|
||||||
)
|
).then((module) => module.lastSettingsContextSnapshot)
|
||||||
baseUnit =
|
if (lastSettingsSnapshot) {
|
||||||
(await getSettingsState)()?.modeling.defaultUnit.current || 'mm'
|
jsAppSettings = getAllCurrentSettings(lastSettingsSnapshot)
|
||||||
}
|
}
|
||||||
const execState: RawExecState = await execute_wasm(
|
}
|
||||||
|
const execState: RawExecState = await execute(
|
||||||
JSON.stringify(node),
|
JSON.stringify(node),
|
||||||
JSON.stringify(programMemoryOverride?.toRaw() || null),
|
JSON.stringify(programMemoryOverride?.toRaw() || null),
|
||||||
baseUnit,
|
JSON.stringify({ settings: jsAppSettings }),
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
fileSystemManager
|
fileSystemManager
|
||||||
)
|
)
|
||||||
@ -552,20 +572,6 @@ export const makeDefaultPlanes = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const modifyGrid = async (
|
|
||||||
engineCommandManager: EngineCommandManager,
|
|
||||||
hidden: boolean
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
|
||||||
await modify_grid(engineCommandManager, hidden)
|
|
||||||
return
|
|
||||||
} catch (e) {
|
|
||||||
// TODO: do something real with the error.
|
|
||||||
console.log('modify grid error', e)
|
|
||||||
return Promise.reject(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const modifyAstForSketch = async (
|
export const modifyAstForSketch = async (
|
||||||
engineCommandManager: EngineCommandManager,
|
engineCommandManager: EngineCommandManager,
|
||||||
ast: Node<Program>,
|
ast: Node<Program>,
|
||||||
|
@ -10,7 +10,7 @@ const noModifiersPressed = (e: MouseEvent) =>
|
|||||||
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
|
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
|
||||||
|
|
||||||
export type CameraSystem =
|
export type CameraSystem =
|
||||||
| 'KittyCAD'
|
| 'Zoo'
|
||||||
| 'OnShape'
|
| 'OnShape'
|
||||||
| 'Trackpad Friendly'
|
| 'Trackpad Friendly'
|
||||||
| 'Solidworks'
|
| 'Solidworks'
|
||||||
@ -19,7 +19,7 @@ export type CameraSystem =
|
|||||||
| 'AutoCAD'
|
| 'AutoCAD'
|
||||||
|
|
||||||
export const cameraSystems: CameraSystem[] = [
|
export const cameraSystems: CameraSystem[] = [
|
||||||
'KittyCAD',
|
'Zoo',
|
||||||
'OnShape',
|
'OnShape',
|
||||||
'Trackpad Friendly',
|
'Trackpad Friendly',
|
||||||
'Solidworks',
|
'Solidworks',
|
||||||
@ -32,8 +32,13 @@ export function mouseControlsToCameraSystem(
|
|||||||
mouseControl: MouseControlType | undefined
|
mouseControl: MouseControlType | undefined
|
||||||
): CameraSystem | undefined {
|
): CameraSystem | undefined {
|
||||||
switch (mouseControl) {
|
switch (mouseControl) {
|
||||||
case 'kitty_cad':
|
// TODO: understand why the values come back without underscores and fix the root cause
|
||||||
return 'KittyCAD'
|
// @ts-ignore: TS2678
|
||||||
|
case 'zoo':
|
||||||
|
return 'Zoo'
|
||||||
|
// TODO: understand why the values come back without underscores and fix the root cause
|
||||||
|
// @ts-ignore: TS2678
|
||||||
|
case 'onshape':
|
||||||
case 'on_shape':
|
case 'on_shape':
|
||||||
return 'OnShape'
|
return 'OnShape'
|
||||||
case 'trackpad_friendly':
|
case 'trackpad_friendly':
|
||||||
@ -44,6 +49,9 @@ export function mouseControlsToCameraSystem(
|
|||||||
return 'NX'
|
return 'NX'
|
||||||
case 'creo':
|
case 'creo':
|
||||||
return 'Creo'
|
return 'Creo'
|
||||||
|
// TODO: understand why the values come back without underscores and fix the root cause
|
||||||
|
// @ts-ignore: TS2678
|
||||||
|
case 'autocad':
|
||||||
case 'auto_cad':
|
case 'auto_cad':
|
||||||
return 'AutoCAD'
|
return 'AutoCAD'
|
||||||
default:
|
default:
|
||||||
@ -77,7 +85,7 @@ export const btnName = (e: MouseEvent) => ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
|
export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
|
||||||
KittyCAD: {
|
Zoo: {
|
||||||
pan: {
|
pan: {
|
||||||
description: 'Shift + Right click drag or middle click drag',
|
description: 'Shift + Right click drag or middle click drag',
|
||||||
callback: (e) =>
|
callback: (e) =>
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
|
import { angleLengthInfo } from 'components/Toolbar/setAngleLength'
|
||||||
|
import { transformAstSketchLines } from 'lang/std/sketchcombos'
|
||||||
|
import { PathToNode } from 'lang/wasm'
|
||||||
import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes'
|
import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes'
|
||||||
import { KCL_DEFAULT_LENGTH, KCL_DEFAULT_DEGREE } from 'lib/constants'
|
import { KCL_DEFAULT_LENGTH, KCL_DEFAULT_DEGREE } from 'lib/constants'
|
||||||
import { components } from 'lib/machine-api'
|
import { components } from 'lib/machine-api'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
|
import { kclManager } from 'lib/singletons'
|
||||||
|
import { err } from 'lib/trap'
|
||||||
import { modelingMachine, SketchTool } from 'machines/modelingMachine'
|
import { modelingMachine, SketchTool } from 'machines/modelingMachine'
|
||||||
|
import { revolveAxisValidator } from './validators'
|
||||||
|
|
||||||
type OutputFormat = Models['OutputFormat_type']
|
type OutputFormat = Models['OutputFormat_type']
|
||||||
type OutputTypeKey = OutputFormat['type']
|
type OutputTypeKey = OutputFormat['type']
|
||||||
@ -34,9 +40,14 @@ export type ModelingCommandSchema = {
|
|||||||
Loft: {
|
Loft: {
|
||||||
selection: Selections
|
selection: Selections
|
||||||
}
|
}
|
||||||
|
Shell: {
|
||||||
|
selection: Selections
|
||||||
|
thickness: KclCommandValue
|
||||||
|
}
|
||||||
Revolve: {
|
Revolve: {
|
||||||
selection: Selections
|
selection: Selections
|
||||||
angle: KclCommandValue
|
angle: KclCommandValue
|
||||||
|
axis: Selections
|
||||||
}
|
}
|
||||||
Fillet: {
|
Fillet: {
|
||||||
// todo
|
// todo
|
||||||
@ -50,6 +61,18 @@ export type ModelingCommandSchema = {
|
|||||||
'change tool': {
|
'change tool': {
|
||||||
tool: SketchTool
|
tool: SketchTool
|
||||||
}
|
}
|
||||||
|
'Constrain length': {
|
||||||
|
selection: Selections
|
||||||
|
length: KclCommandValue
|
||||||
|
}
|
||||||
|
'Constrain with named value': {
|
||||||
|
currentValue: {
|
||||||
|
valueText: string
|
||||||
|
pathToNode: PathToNode
|
||||||
|
variableName: string
|
||||||
|
}
|
||||||
|
namedValue: KclCommandValue
|
||||||
|
}
|
||||||
'Text-to-CAD': {
|
'Text-to-CAD': {
|
||||||
prompt: string
|
prompt: string
|
||||||
}
|
}
|
||||||
@ -277,6 +300,25 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Shell: {
|
||||||
|
description: 'Hollow out a 3D solid.',
|
||||||
|
icon: 'shell',
|
||||||
|
needsReview: true,
|
||||||
|
args: {
|
||||||
|
selection: {
|
||||||
|
inputType: 'selection',
|
||||||
|
selectionTypes: ['cap', 'wall'],
|
||||||
|
multiple: true,
|
||||||
|
required: true,
|
||||||
|
skip: false,
|
||||||
|
},
|
||||||
|
thickness: {
|
||||||
|
inputType: 'kcl',
|
||||||
|
defaultValue: KCL_DEFAULT_LENGTH,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
// TODO: Update this configuration, copied from extrude for MVP of revolve, specifically the args.selection
|
// TODO: Update this configuration, copied from extrude for MVP of revolve, specifically the args.selection
|
||||||
Revolve: {
|
Revolve: {
|
||||||
description: 'Create a 3D body by rotating a sketch region about an axis.',
|
description: 'Create a 3D body by rotating a sketch region about an axis.',
|
||||||
@ -290,6 +332,13 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
|||||||
required: true,
|
required: true,
|
||||||
skip: true,
|
skip: true,
|
||||||
},
|
},
|
||||||
|
axis: {
|
||||||
|
required: true,
|
||||||
|
inputType: 'selection',
|
||||||
|
selectionTypes: ['segment', 'sweepEdge', 'edgeCutEdge'],
|
||||||
|
multiple: false,
|
||||||
|
validation: revolveAxisValidator,
|
||||||
|
},
|
||||||
angle: {
|
angle: {
|
||||||
inputType: 'kcl',
|
inputType: 'kcl',
|
||||||
defaultValue: KCL_DEFAULT_DEGREE,
|
defaultValue: KCL_DEFAULT_DEGREE,
|
||||||
@ -337,6 +386,88 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'Constrain length': {
|
||||||
|
description: 'Constrain the length of one or more segments.',
|
||||||
|
icon: 'dimension',
|
||||||
|
args: {
|
||||||
|
selection: {
|
||||||
|
inputType: 'selection',
|
||||||
|
selectionTypes: ['segment'],
|
||||||
|
multiple: false,
|
||||||
|
required: true,
|
||||||
|
skip: true,
|
||||||
|
},
|
||||||
|
length: {
|
||||||
|
inputType: 'kcl',
|
||||||
|
required: true,
|
||||||
|
createVariableByDefault: true,
|
||||||
|
defaultValue(_, machineContext) {
|
||||||
|
const selectionRanges = machineContext?.selectionRanges
|
||||||
|
if (!selectionRanges) return KCL_DEFAULT_LENGTH
|
||||||
|
const angleLength = angleLengthInfo({
|
||||||
|
selectionRanges,
|
||||||
|
angleOrLength: 'setLength',
|
||||||
|
})
|
||||||
|
if (err(angleLength)) return KCL_DEFAULT_LENGTH
|
||||||
|
const { transforms } = angleLength
|
||||||
|
|
||||||
|
// QUESTION: is it okay to reference kclManager here? will its state be up to date?
|
||||||
|
const sketched = transformAstSketchLines({
|
||||||
|
ast: structuredClone(kclManager.ast),
|
||||||
|
selectionRanges,
|
||||||
|
transformInfos: transforms,
|
||||||
|
programMemory: kclManager.programMemory,
|
||||||
|
referenceSegName: '',
|
||||||
|
})
|
||||||
|
if (err(sketched)) return KCL_DEFAULT_LENGTH
|
||||||
|
const { valueUsedInTransform } = sketched
|
||||||
|
return valueUsedInTransform?.toString() || KCL_DEFAULT_LENGTH
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Constrain with named value': {
|
||||||
|
description: 'Constrain a value by making it a named constant.',
|
||||||
|
icon: 'make-variable',
|
||||||
|
args: {
|
||||||
|
currentValue: {
|
||||||
|
description:
|
||||||
|
'Path to the node in the AST to constrain. This is never shown to the user.',
|
||||||
|
inputType: 'text',
|
||||||
|
required: false,
|
||||||
|
skip: true,
|
||||||
|
},
|
||||||
|
namedValue: {
|
||||||
|
inputType: 'kcl',
|
||||||
|
required: true,
|
||||||
|
createVariableByDefault: true,
|
||||||
|
variableName(commandBarContext, machineContext) {
|
||||||
|
const { currentValue } = commandBarContext.argumentsToSubmit
|
||||||
|
if (
|
||||||
|
!currentValue ||
|
||||||
|
!(currentValue instanceof Object) ||
|
||||||
|
!('variableName' in currentValue) ||
|
||||||
|
typeof currentValue.variableName !== 'string'
|
||||||
|
) {
|
||||||
|
return 'value'
|
||||||
|
}
|
||||||
|
return currentValue.variableName
|
||||||
|
},
|
||||||
|
defaultValue: (commandBarContext) => {
|
||||||
|
const { currentValue } = commandBarContext.argumentsToSubmit
|
||||||
|
if (
|
||||||
|
!currentValue ||
|
||||||
|
!(currentValue instanceof Object) ||
|
||||||
|
!('valueText' in currentValue) ||
|
||||||
|
typeof currentValue.valueText !== 'string'
|
||||||
|
) {
|
||||||
|
return KCL_DEFAULT_LENGTH
|
||||||
|
}
|
||||||
|
return currentValue.valueText
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
'Text-to-CAD': {
|
'Text-to-CAD': {
|
||||||
description: 'Use the Zoo Text-to-CAD API to generate part starters.',
|
description: 'Use the Zoo Text-to-CAD API to generate part starters.',
|
||||||
icon: 'chat',
|
icon: 'chat',
|
||||||
|
106
src/lib/commandBarConfigs/validators.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { Models } from '@kittycad/lib'
|
||||||
|
import { engineCommandManager } from 'lib/singletons'
|
||||||
|
import { uuidv4 } from 'lib/utils'
|
||||||
|
import { CommandBarContext } from 'machines/commandBarMachine'
|
||||||
|
import { Selections } from 'lib/selections'
|
||||||
|
|
||||||
|
export const disableDryRunWithRetry = async (numberOfRetries = 3) => {
|
||||||
|
for (let tries = 0; tries < numberOfRetries; tries++) {
|
||||||
|
try {
|
||||||
|
await engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: { type: 'disable_dry_run' },
|
||||||
|
})
|
||||||
|
// Exit out since the command was successful
|
||||||
|
return
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
console.error('disable_dry_run failed. This is bad!')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Takes a callback function and wraps it around enable_dry_run and disable_dry_run
|
||||||
|
export const dryRunWrapper = async (callback: () => Promise<any>) => {
|
||||||
|
// Gotcha: What about race conditions?
|
||||||
|
try {
|
||||||
|
await engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: { type: 'enable_dry_run' },
|
||||||
|
})
|
||||||
|
const result = await callback()
|
||||||
|
return result
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
await disableDryRunWithRetry(5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSelections(selections: unknown): selections is Selections {
|
||||||
|
return (
|
||||||
|
(selections as Selections).graphSelections !== undefined &&
|
||||||
|
(selections as Selections).otherSelections !== undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const revolveAxisValidator = async ({
|
||||||
|
data,
|
||||||
|
context,
|
||||||
|
}: {
|
||||||
|
data: { [key: string]: Selections }
|
||||||
|
context: CommandBarContext
|
||||||
|
}): Promise<boolean | string> => {
|
||||||
|
if (!isSelections(context.argumentsToSubmit.selection)) {
|
||||||
|
return 'Unable to revolve, selections are missing'
|
||||||
|
}
|
||||||
|
const artifact =
|
||||||
|
context.argumentsToSubmit.selection.graphSelections[0].artifact
|
||||||
|
|
||||||
|
if (!artifact) {
|
||||||
|
return 'Unable to revolve, sketch not found'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('pathId' in artifact)) {
|
||||||
|
return 'Unable to revolve, sketch has no path'
|
||||||
|
}
|
||||||
|
|
||||||
|
const sketchSelection = artifact.pathId
|
||||||
|
let edgeSelection = data.axis.graphSelections[0].artifact?.id
|
||||||
|
|
||||||
|
if (!sketchSelection) {
|
||||||
|
return 'Unable to revolve, sketch is missing'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!edgeSelection) {
|
||||||
|
return 'Unable to revolve, edge is missing'
|
||||||
|
}
|
||||||
|
|
||||||
|
const angleInDegrees: Models['Angle_type'] = {
|
||||||
|
unit: 'degrees',
|
||||||
|
value: 360,
|
||||||
|
}
|
||||||
|
|
||||||
|
const revolveAboutEdgeCommand = async () => {
|
||||||
|
return await engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'revolve_about_edge',
|
||||||
|
angle: angleInDegrees,
|
||||||
|
edge_id: edgeSelection,
|
||||||
|
target: sketchSelection,
|
||||||
|
tolerance: 0.0001,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const attemptRevolve = await dryRunWrapper(revolveAboutEdgeCommand)
|
||||||
|
if (attemptRevolve?.success) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
// return error message for the toast
|
||||||
|
return 'Unable to revolve with selected axis'
|
||||||
|
}
|
||||||
|
}
|
@ -7,7 +7,7 @@ import { ReactNode } from 'react'
|
|||||||
import { MachineManager } from 'components/MachineManagerProvider'
|
import { MachineManager } from 'components/MachineManagerProvider'
|
||||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
import { Artifact } from 'lang/std/artifactGraph'
|
import { Artifact } from 'lang/std/artifactGraph'
|
||||||
|
import { CommandBarContext } from 'machines/commandBarMachine'
|
||||||
type Icon = CustomIconName
|
type Icon = CustomIconName
|
||||||
const PLATFORMS = ['both', 'web', 'desktop'] as const
|
const PLATFORMS = ['both', 'web', 'desktop'] as const
|
||||||
const INPUT_TYPES = [
|
const INPUT_TYPES = [
|
||||||
@ -147,8 +147,30 @@ export type CommandArgumentConfig<
|
|||||||
inputType: 'selection'
|
inputType: 'selection'
|
||||||
selectionTypes: Artifact['type'][]
|
selectionTypes: Artifact['type'][]
|
||||||
multiple: boolean
|
multiple: boolean
|
||||||
|
validation?: ({
|
||||||
|
data,
|
||||||
|
context,
|
||||||
|
}: {
|
||||||
|
data: any
|
||||||
|
context: CommandBarContext
|
||||||
|
}) => Promise<boolean | string>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
inputType: 'kcl'
|
||||||
|
createVariableByDefault?: boolean
|
||||||
|
variableName?:
|
||||||
|
| string
|
||||||
|
| ((
|
||||||
|
commandBarContext: ContextFrom<typeof commandBarMachine>,
|
||||||
|
machineContext?: C
|
||||||
|
) => string)
|
||||||
|
defaultValue?:
|
||||||
|
| string
|
||||||
|
| ((
|
||||||
|
commandBarContext: ContextFrom<typeof commandBarMachine>,
|
||||||
|
machineContext?: C
|
||||||
|
) => string)
|
||||||
}
|
}
|
||||||
| { inputType: 'kcl'; defaultValue?: string } // KCL expression inputs have simple strings as default values
|
|
||||||
| {
|
| {
|
||||||
inputType: 'string'
|
inputType: 'string'
|
||||||
defaultValue?:
|
defaultValue?:
|
||||||
@ -221,8 +243,30 @@ export type CommandArgument<
|
|||||||
inputType: 'selection'
|
inputType: 'selection'
|
||||||
selectionTypes: Artifact['type'][]
|
selectionTypes: Artifact['type'][]
|
||||||
multiple: boolean
|
multiple: boolean
|
||||||
|
validation?: ({
|
||||||
|
data,
|
||||||
|
context,
|
||||||
|
}: {
|
||||||
|
data: any
|
||||||
|
context: CommandBarContext
|
||||||
|
}) => Promise<boolean | string>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
inputType: 'kcl'
|
||||||
|
createVariableByDefault?: boolean
|
||||||
|
variableName?:
|
||||||
|
| string
|
||||||
|
| ((
|
||||||
|
commandBarContext: ContextFrom<typeof commandBarMachine>,
|
||||||
|
machineContext?: ContextFrom<T>
|
||||||
|
) => string)
|
||||||
|
defaultValue?:
|
||||||
|
| string
|
||||||
|
| ((
|
||||||
|
commandBarContext: ContextFrom<typeof commandBarMachine>,
|
||||||
|
machineContext?: ContextFrom<T>
|
||||||
|
) => string)
|
||||||
}
|
}
|
||||||
| { inputType: 'kcl'; defaultValue?: string } // KCL expression inputs have simple strings as default value
|
|
||||||
| {
|
| {
|
||||||
inputType: 'string'
|
inputType: 'string'
|
||||||
defaultValue?:
|
defaultValue?:
|
||||||
|
@ -53,6 +53,7 @@ export const KCL_DEFAULT_CONSTANT_PREFIXES = {
|
|||||||
SKETCH: 'sketch',
|
SKETCH: 'sketch',
|
||||||
EXTRUDE: 'extrude',
|
EXTRUDE: 'extrude',
|
||||||
LOFT: 'loft',
|
LOFT: 'loft',
|
||||||
|
SHELL: 'shell',
|
||||||
SEGMENT: 'seg',
|
SEGMENT: 'seg',
|
||||||
REVOLVE: 'revolve',
|
REVOLVE: 'revolve',
|
||||||
PLANE: 'plane',
|
PLANE: 'plane',
|
||||||
@ -110,3 +111,28 @@ export const KCL_SAMPLES_MANIFEST_URLS = {
|
|||||||
|
|
||||||
/** Toast id for the app auto-updater toast */
|
/** Toast id for the app auto-updater toast */
|
||||||
export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast'
|
export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast'
|
||||||
|
|
||||||
|
/** Local sketch axis values in KCL for operations, it could either be 'X' or 'Y' */
|
||||||
|
export const KCL_AXIS_X = 'X'
|
||||||
|
export const KCL_AXIS_Y = 'Y'
|
||||||
|
export const KCL_AXIS_NEG_X = '-X'
|
||||||
|
export const KCL_AXIS_NEG_Y = '-Y'
|
||||||
|
export const KCL_DEFAULT_AXIS = 'X'
|
||||||
|
|
||||||
|
export enum AxisNames {
|
||||||
|
X = 'x',
|
||||||
|
Y = 'y',
|
||||||
|
Z = 'z',
|
||||||
|
NEG_X = '-x',
|
||||||
|
NEG_Y = '-y',
|
||||||
|
NEG_Z = '-z',
|
||||||
|
}
|
||||||
|
/** Semantic names of views from AxisNames */
|
||||||
|
export const VIEW_NAMES_SEMANTIC = {
|
||||||
|
[AxisNames.X]: 'Right',
|
||||||
|
[AxisNames.Y]: 'Back',
|
||||||
|
[AxisNames.Z]: 'Top',
|
||||||
|
[AxisNames.NEG_X]: 'Left',
|
||||||
|
[AxisNames.NEG_Y]: 'Front',
|
||||||
|
[AxisNames.NEG_Z]: 'Bottom',
|
||||||
|
} as const
|
||||||
|
@ -155,6 +155,8 @@ export function buildCommandArgument<
|
|||||||
context: ContextFrom<T>,
|
context: ContextFrom<T>,
|
||||||
machineActor: Actor<T>
|
machineActor: Actor<T>
|
||||||
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
|
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
|
||||||
|
// GOTCHA: modelingCommandConfig is not a 1:1 mapping to this baseCommandArgument
|
||||||
|
// You need to manually add key/value pairs here.
|
||||||
const baseCommandArgument = {
|
const baseCommandArgument = {
|
||||||
description: arg.description,
|
description: arg.description,
|
||||||
required: arg.required,
|
required: arg.required,
|
||||||
@ -181,10 +183,13 @@ export function buildCommandArgument<
|
|||||||
...baseCommandArgument,
|
...baseCommandArgument,
|
||||||
multiple: arg.multiple,
|
multiple: arg.multiple,
|
||||||
selectionTypes: arg.selectionTypes,
|
selectionTypes: arg.selectionTypes,
|
||||||
|
validation: arg.validation,
|
||||||
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
|
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
|
||||||
} else if (arg.inputType === 'kcl') {
|
} else if (arg.inputType === 'kcl') {
|
||||||
return {
|
return {
|
||||||
inputType: arg.inputType,
|
inputType: arg.inputType,
|
||||||
|
createVariableByDefault: arg.createVariableByDefault,
|
||||||
|
variableName: arg.variableName,
|
||||||
defaultValue: arg.defaultValue,
|
defaultValue: arg.defaultValue,
|
||||||
...baseCommandArgument,
|
...baseCommandArgument,
|
||||||
} satisfies CommandArgument<O, T> & { inputType: 'kcl' }
|
} satisfies CommandArgument<O, T> & { inputType: 'kcl' }
|
||||||
|
@ -569,6 +569,17 @@ export function canSweepSelection(selection: Selections) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canRevolveSelection(selection: Selections) {
|
||||||
|
const commonNodes = selection.graphSelections.map((_, i) =>
|
||||||
|
buildCommonNodeFromSelection(selection, i)
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
!!isSketchPipe(selection) &&
|
||||||
|
(commonNodes.every((n) => nodeHasClose(n)) ||
|
||||||
|
commonNodes.every((n) => nodeHasCircle(n)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function canLoftSelection(selection: Selections) {
|
export function canLoftSelection(selection: Selections) {
|
||||||
const commonNodes = selection.graphSelections.map((_, i) =>
|
const commonNodes = selection.graphSelections.map((_, i) =>
|
||||||
buildCommonNodeFromSelection(selection, i)
|
buildCommonNodeFromSelection(selection, i)
|
||||||
@ -585,6 +596,17 @@ export function canLoftSelection(selection: Selections) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canShellSelection(selection: Selections) {
|
||||||
|
const commonNodes = selection.graphSelections.map((_, i) =>
|
||||||
|
buildCommonNodeFromSelection(selection, i)
|
||||||
|
)
|
||||||
|
return commonNodes.every(
|
||||||
|
(n) =>
|
||||||
|
n.selection.artifact?.type === 'cap' ||
|
||||||
|
n.selection.artifact?.type === 'wall'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// This accounts for non-geometry selections under "other"
|
// This accounts for non-geometry selections under "other"
|
||||||
export type ResolvedSelectionType = Artifact['type'] | 'other'
|
export type ResolvedSelectionType = Artifact['type'] | 'other'
|
||||||
export type SelectionCountsByType = Map<ResolvedSelectionType, number>
|
export type SelectionCountsByType = Map<ResolvedSelectionType, number>
|
||||||
@ -619,12 +641,29 @@ export function getSelectionCountByType(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
selection.graphSelections.forEach((selection) => {
|
selection.graphSelections.forEach((graphSelection) => {
|
||||||
if (!selection.artifact) {
|
if (!graphSelection.artifact) {
|
||||||
|
/**
|
||||||
|
* TODO: remove this heuristic-based selection type detection.
|
||||||
|
* Currently, if you've created a sketch and have not left sketch mode,
|
||||||
|
* the selection will be a segment selection with no artifact.
|
||||||
|
* This is because the mock execution does not update the artifact graph.
|
||||||
|
* Once we move the artifactGraph creation to WASM, we can remove this,
|
||||||
|
* as the artifactGraph will always be up-to-date.
|
||||||
|
*/
|
||||||
|
if (isSingleCursorInPipe(selection, kclManager.ast)) {
|
||||||
|
incrementOrInitializeSelectionType('segment')
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
'Selection is outside of a sketch but has no artifact. Sketch segment selections are the only kind that can have a valid selection with no artifact.',
|
||||||
|
JSON.stringify(graphSelection)
|
||||||
|
)
|
||||||
incrementOrInitializeSelectionType('other')
|
incrementOrInitializeSelectionType('other')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
incrementOrInitializeSelectionType(selection.artifact.type)
|
}
|
||||||
|
incrementOrInitializeSelectionType(graphSelection.artifact.type)
|
||||||
})
|
})
|
||||||
|
|
||||||
return selectionsByType
|
return selectionsByType
|
||||||
|
@ -12,7 +12,7 @@ export type InteractionMapItem = {
|
|||||||
* Controls both the available names for interaction map categories
|
* Controls both the available names for interaction map categories
|
||||||
* and the order in which they are displayed.
|
* and the order in which they are displayed.
|
||||||
*/
|
*/
|
||||||
export const interactionMapCategories = [
|
const interactionMapCategories = [
|
||||||
'Sketching',
|
'Sketching',
|
||||||
'Modeling',
|
'Modeling',
|
||||||
'Command Palette',
|
'Command Palette',
|
||||||
|
@ -283,7 +283,7 @@ export function createSettings() {
|
|||||||
* The controls for how to navigate the 3D view
|
* The controls for how to navigate the 3D view
|
||||||
*/
|
*/
|
||||||
mouseControls: new Setting<CameraSystem>({
|
mouseControls: new Setting<CameraSystem>({
|
||||||
defaultValue: 'KittyCAD',
|
defaultValue: 'Zoo',
|
||||||
description: 'The controls for how to navigate the 3D view',
|
description: 'The controls for how to navigate the 3D view',
|
||||||
validate: (v) => cameraSystems.includes(v as CameraSystem),
|
validate: (v) => cameraSystems.includes(v as CameraSystem),
|
||||||
hideOnLevel: 'project',
|
hideOnLevel: 'project',
|
||||||
|
@ -2,6 +2,7 @@ import { DeepPartial } from 'lib/types'
|
|||||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||||
import {
|
import {
|
||||||
configurationToSettingsPayload,
|
configurationToSettingsPayload,
|
||||||
|
getAllCurrentSettings,
|
||||||
projectConfigurationToSettingsPayload,
|
projectConfigurationToSettingsPayload,
|
||||||
setSettingsAtLevel,
|
setSettingsAtLevel,
|
||||||
} from './settingsUtils'
|
} from './settingsUtils'
|
||||||
@ -65,3 +66,48 @@ describe(`testing settings initialization`, () => {
|
|||||||
expect(settings.app.themeColor.current).toBe('200')
|
expect(settings.app.themeColor.current).toBe('200')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe(`testing getAllCurrentSettings`, () => {
|
||||||
|
it(`returns the correct settings`, () => {
|
||||||
|
// Set up the settings
|
||||||
|
let settings = createSettings()
|
||||||
|
const appConfiguration: DeepPartial<Configuration> = {
|
||||||
|
settings: {
|
||||||
|
app: {
|
||||||
|
appearance: {
|
||||||
|
theme: 'dark',
|
||||||
|
color: 190,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const projectConfiguration: DeepPartial<Configuration> = {
|
||||||
|
settings: {
|
||||||
|
app: {
|
||||||
|
appearance: {
|
||||||
|
theme: 'light',
|
||||||
|
color: 200,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modeling: {
|
||||||
|
base_unit: 'ft',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const appSettingsPayload = configurationToSettingsPayload(appConfiguration)
|
||||||
|
const projectSettingsPayload =
|
||||||
|
projectConfigurationToSettingsPayload(projectConfiguration)
|
||||||
|
|
||||||
|
setSettingsAtLevel(settings, 'user', appSettingsPayload)
|
||||||
|
setSettingsAtLevel(settings, 'project', projectSettingsPayload)
|
||||||
|
|
||||||
|
// Now the test: get all the settings' current resolved values
|
||||||
|
const allCurrentSettings = getAllCurrentSettings(settings)
|
||||||
|
// This one gets the 'user'-level theme because it's ignored at the project level
|
||||||
|
// (see the test "doesn't read theme from project settings")
|
||||||
|
expect(allCurrentSettings.app.theme).toBe('dark')
|
||||||
|
expect(allCurrentSettings.app.themeColor).toBe('200')
|
||||||
|
expect(allCurrentSettings.modeling.defaultUnit).toBe('ft')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
@ -286,6 +286,27 @@ export function getChangedSettingsAtLevel(
|
|||||||
return changedSettings
|
return changedSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAllCurrentSettings(
|
||||||
|
allSettings: typeof settings
|
||||||
|
): SaveSettingsPayload {
|
||||||
|
const currentSettings = {} as SaveSettingsPayload
|
||||||
|
Object.entries(allSettings).forEach(([category, settingsCategory]) => {
|
||||||
|
const categoryKey = category as keyof typeof settings
|
||||||
|
Object.entries(settingsCategory).forEach(
|
||||||
|
([setting, settingValue]: [string, Setting]) => {
|
||||||
|
const settingKey =
|
||||||
|
setting as keyof (typeof settings)[typeof categoryKey]
|
||||||
|
currentSettings[categoryKey] = {
|
||||||
|
...currentSettings[categoryKey],
|
||||||
|
[settingKey]: settingValue.current,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return currentSettings
|
||||||
|
}
|
||||||
|
|
||||||
export function setSettingsAtLevel(
|
export function setSettingsAtLevel(
|
||||||
allSettings: typeof settings,
|
allSettings: typeof settings,
|
||||||
level: SettingsLevel,
|
level: SettingsLevel,
|
||||||
|
@ -112,9 +112,6 @@ export async function executor(
|
|||||||
makeDefaultPlanes: () => {
|
makeDefaultPlanes: () => {
|
||||||
return new Promise((resolve) => resolve(defaultPlanes))
|
return new Promise((resolve) => resolve(defaultPlanes))
|
||||||
},
|
},
|
||||||
modifyGrid: (hidden: boolean) => {
|
|
||||||
return new Promise((resolve) => resolve())
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
@ -190,9 +190,15 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'shell',
|
id: 'shell',
|
||||||
onClick: () => console.error('Shell not yet implemented'),
|
onClick: ({ commandBarSend }) => {
|
||||||
|
commandBarSend({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: { name: 'Shell', groupId: 'modeling' },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
disabled: (state) => !state.can({ type: 'Shell' }),
|
||||||
icon: 'shell',
|
icon: 'shell',
|
||||||
status: 'kcl-only',
|
status: 'available',
|
||||||
title: 'Shell',
|
title: 'Shell',
|
||||||
description: 'Hollow out a 3D solid.',
|
description: 'Hollow out a 3D solid.',
|
||||||
links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/shell' }],
|
links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/shell' }],
|
||||||
@ -534,13 +540,15 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
id: 'constraint-length',
|
id: 'constraint-length',
|
||||||
disabled: (state) =>
|
disabled: (state) => !state.matches({ Sketch: 'SketchIdle' }),
|
||||||
!(
|
onClick: ({ commandBarSend }) =>
|
||||||
state.matches({ Sketch: 'SketchIdle' }) &&
|
commandBarSend({
|
||||||
state.can({ type: 'Constrain length' })
|
type: 'Find and select command',
|
||||||
),
|
data: {
|
||||||
onClick: ({ modelingSend }) =>
|
name: 'Constrain length',
|
||||||
modelingSend({ type: 'Constrain length' }),
|
groupId: 'modeling',
|
||||||
|
},
|
||||||
|
}),
|
||||||
icon: 'dimension',
|
icon: 'dimension',
|
||||||
status: 'available',
|
status: 'available',
|
||||||
title: 'Length',
|
title: 'Length',
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
import { Selections__old } from 'lib/selections'
|
import { Selections__old } from 'lib/selections'
|
||||||
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
|
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
|
||||||
import { MachineManager } from 'components/MachineManagerProvider'
|
import { MachineManager } from 'components/MachineManagerProvider'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
export type CommandBarContext = {
|
export type CommandBarContext = {
|
||||||
commands: Command[]
|
commands: Command[]
|
||||||
@ -247,14 +248,69 @@ export const commandBarMachine = setup({
|
|||||||
'All arguments are skippable': () => false,
|
'All arguments are skippable': () => false,
|
||||||
},
|
},
|
||||||
actors: {
|
actors: {
|
||||||
'Validate argument': fromPromise(({ input }) => {
|
'Validate argument': fromPromise(
|
||||||
|
({
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
input: {
|
||||||
|
context: CommandBarContext | undefined
|
||||||
|
event: CommandBarMachineEvent | undefined
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// TODO: figure out if we should validate argument data here or in the form itself,
|
if (!input || input?.event?.type !== 'Submit argument') {
|
||||||
// and if we should support people configuring a argument's validation function
|
toast.error(`Unable to validate, wrong event type.`)
|
||||||
|
return reject(`Unable to validate, wrong event type`)
|
||||||
|
}
|
||||||
|
|
||||||
resolve(input)
|
const context = input?.context
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
toast.error(`Unable to validate, wrong argument.`)
|
||||||
|
return reject(`Unable to validate, wrong argument`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = input.event.data
|
||||||
|
const argName = context.currentArgument?.name
|
||||||
|
const args = context?.selectedCommand?.args
|
||||||
|
const argConfig = args && argName ? args[argName] : undefined
|
||||||
|
// Only do a validation check if the argument, selectedCommand, and the validation function are defined
|
||||||
|
if (
|
||||||
|
context.currentArgument &&
|
||||||
|
context.selectedCommand &&
|
||||||
|
argConfig?.inputType === 'selection' &&
|
||||||
|
argConfig?.validation
|
||||||
|
) {
|
||||||
|
argConfig
|
||||||
|
.validation({ context, data })
|
||||||
|
.then((result) => {
|
||||||
|
if (typeof result === 'boolean' && result === true) {
|
||||||
|
return resolve(data)
|
||||||
|
} else {
|
||||||
|
// validation failed
|
||||||
|
if (typeof result === 'string') {
|
||||||
|
// The result of the validation is the error message
|
||||||
|
toast.error(result)
|
||||||
|
return reject(
|
||||||
|
`unable to validate ${argName}, Message: ${result}`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Default message if there is not a custom one sent
|
||||||
|
toast.error(`Unable to validate ${argName}`)
|
||||||
|
return reject(`unable to validate ${argName}}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}),
|
.catch(() => {
|
||||||
|
return reject(`unable to validate ${argName}}`)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Missing several requirements for validate argument, just bypass
|
||||||
|
return resolve(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
),
|
||||||
'Validate all arguments': fromPromise(
|
'Validate all arguments': fromPromise(
|
||||||
({ input }: { input: CommandBarContext }) => {
|
({ input }: { input: CommandBarContext }) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -449,9 +505,10 @@ export const commandBarMachine = setup({
|
|||||||
invoke: {
|
invoke: {
|
||||||
src: 'Validate argument',
|
src: 'Validate argument',
|
||||||
id: 'validateSingleArgument',
|
id: 'validateSingleArgument',
|
||||||
input: ({ event }) => {
|
input: ({ event, context }) => {
|
||||||
if (event.type !== 'Submit argument') return {}
|
if (event.type !== 'Submit argument')
|
||||||
return event.data
|
return { event: undefined, context: undefined }
|
||||||
|
return { event, context }
|
||||||
},
|
},
|
||||||
onDone: {
|
onDone: {
|
||||||
target: '#Command Bar.Checking Arguments',
|
target: '#Command Bar.Checking Arguments',
|
||||||
|
@ -42,8 +42,6 @@ export const settingsMachine = setup({
|
|||||||
setClientTheme: () => {},
|
setClientTheme: () => {},
|
||||||
'Execute AST': () => {},
|
'Execute AST': () => {},
|
||||||
toastSuccess: () => {},
|
toastSuccess: () => {},
|
||||||
setEngineEdges: () => {},
|
|
||||||
setEngineScaleGridVisibility: () => {},
|
|
||||||
setClientSideSceneUnits: () => {},
|
setClientSideSceneUnits: () => {},
|
||||||
persistSettings: () => {},
|
persistSettings: () => {},
|
||||||
resetSettings: assign(({ context, event }) => {
|
resetSettings: assign(({ context, event }) => {
|
||||||
@ -172,7 +170,7 @@ export const settingsMachine = setup({
|
|||||||
'set.modeling.highlightEdges': {
|
'set.modeling.highlightEdges': {
|
||||||
target: 'persisting settings',
|
target: 'persisting settings',
|
||||||
|
|
||||||
actions: ['setSettingAtLevel', 'toastSuccess', 'setEngineEdges'],
|
actions: ['setSettingAtLevel', 'toastSuccess', 'Execute AST'],
|
||||||
},
|
},
|
||||||
|
|
||||||
'Reset settings': {
|
'Reset settings': {
|
||||||
@ -201,11 +199,7 @@ export const settingsMachine = setup({
|
|||||||
|
|
||||||
'set.modeling.showScaleGrid': {
|
'set.modeling.showScaleGrid': {
|
||||||
target: 'persisting settings',
|
target: 'persisting settings',
|
||||||
actions: [
|
actions: ['setSettingAtLevel', 'toastSuccess', 'Execute AST'],
|
||||||
'setSettingAtLevel',
|
|
||||||
'toastSuccess',
|
|
||||||
'setEngineScaleGridVisibility',
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -44,11 +44,6 @@ process.env.VITE_KC_SITE_BASE_URL ??= 'https://zoo.dev'
|
|||||||
process.env.VITE_KC_SKIP_AUTH ??= 'false'
|
process.env.VITE_KC_SKIP_AUTH ??= 'false'
|
||||||
process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??= '15000'
|
process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??= '15000'
|
||||||
|
|
||||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
|
||||||
if (require('electron-squirrel-startup')) {
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
|
|
||||||
const ZOO_STUDIO_PROTOCOL = 'zoo-studio'
|
const ZOO_STUDIO_PROTOCOL = 'zoo-studio'
|
||||||
|
|
||||||
/// Register our application to handle all "electron-fiddle://" protocols.
|
/// Register our application to handle all "electron-fiddle://" protocols.
|
||||||
@ -256,6 +251,9 @@ export function getAutoUpdater(): AppUpdater {
|
|||||||
|
|
||||||
app.on('ready', () => {
|
app.on('ready', () => {
|
||||||
const autoUpdater = getAutoUpdater()
|
const autoUpdater = getAutoUpdater()
|
||||||
|
// TODO: we're getting `Error: Response ends without calling any handlers` with our setup,
|
||||||
|
// so at the moment this isn't worth enabling
|
||||||
|
autoUpdater.disableDifferentialDownload = true
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
autoUpdater.checkForUpdates().catch(reportRejection)
|
autoUpdater.checkForUpdates().catch(reportRejection)
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
@ -30,6 +30,14 @@ export const PACKAGE_NAME = isDesktop()
|
|||||||
? window.electron.packageJson.name
|
? window.electron.packageJson.name
|
||||||
: 'zoo-modeling-app'
|
: 'zoo-modeling-app'
|
||||||
|
|
||||||
|
export const IS_NIGHTLY = PACKAGE_NAME.indexOf('-nightly') > -1
|
||||||
|
|
||||||
|
export function getReleaseUrl(version: string = APP_VERSION) {
|
||||||
|
return `https://github.com/KittyCAD/modeling-app/releases/tag/${
|
||||||
|
IS_NIGHTLY ? 'nightly-' : ''
|
||||||
|
}v${version}`
|
||||||
|
}
|
||||||
|
|
||||||
export const Settings = () => {
|
export const Settings = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
|
167
src/wasm-lib/Cargo.lock
generated
@ -121,9 +121,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.93"
|
version = "1.0.94"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
|
checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
]
|
]
|
||||||
@ -176,7 +176,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -187,7 +187,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -204,7 +204,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -401,9 +401,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.38"
|
version = "0.4.39"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
|
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-tzdata",
|
"android-tzdata",
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
@ -474,7 +474,7 @@ dependencies = [
|
|||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -665,7 +665,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"strsim",
|
"strsim",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -676,7 +676,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -737,7 +737,7 @@ dependencies = [
|
|||||||
"rustfmt-wrapper",
|
"rustfmt-wrapper",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_tokenstream",
|
"serde_tokenstream",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -748,7 +748,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -791,7 +791,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -829,7 +829,7 @@ checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -990,7 +990,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1086,7 +1086,7 @@ dependencies = [
|
|||||||
"inflections",
|
"inflections",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1112,7 +1112,7 @@ dependencies = [
|
|||||||
"fnv",
|
"fnv",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"http 1.1.0",
|
"http 1.2.0",
|
||||||
"indexmap 2.7.0",
|
"indexmap 2.7.0",
|
||||||
"slab",
|
"slab",
|
||||||
"tokio",
|
"tokio",
|
||||||
@ -1215,9 +1215,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
|
checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
@ -1242,7 +1242,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"http 1.1.0",
|
"http 1.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1253,7 +1253,7 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 1.1.0",
|
"http 1.2.0",
|
||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
@ -1303,7 +1303,7 @@ dependencies = [
|
|||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"h2",
|
"h2",
|
||||||
"http 1.1.0",
|
"http 1.2.0",
|
||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
"httparse",
|
"httparse",
|
||||||
"itoa",
|
"itoa",
|
||||||
@ -1320,7 +1320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
|
checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 1.1.0",
|
"http 1.2.0",
|
||||||
"hyper 1.4.1",
|
"hyper 1.4.1",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"rustls",
|
"rustls",
|
||||||
@ -1340,7 +1340,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 1.1.0",
|
"http 1.2.0",
|
||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
"hyper 1.4.1",
|
"hyper 1.4.1",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@ -1494,7 +1494,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1705,7 +1705,7 @@ dependencies = [
|
|||||||
"git_rev",
|
"git_rev",
|
||||||
"gltf-json",
|
"gltf-json",
|
||||||
"handlebars",
|
"handlebars",
|
||||||
"http 1.1.0",
|
"http 1.2.0",
|
||||||
"iai",
|
"iai",
|
||||||
"image",
|
"image",
|
||||||
"indexmap 2.7.0",
|
"indexmap 2.7.0",
|
||||||
@ -1721,7 +1721,9 @@ dependencies = [
|
|||||||
"parse-display 0.9.1",
|
"parse-display 0.9.1",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"pyo3",
|
"pyo3",
|
||||||
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"rgba_simple",
|
||||||
"ropey",
|
"ropey",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
@ -1790,7 +1792,7 @@ dependencies = [
|
|||||||
"data-encoding",
|
"data-encoding",
|
||||||
"format_serde_error",
|
"format_serde_error",
|
||||||
"futures",
|
"futures",
|
||||||
"http 1.1.0",
|
"http 1.2.0",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"log",
|
"log",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
@ -1816,9 +1818,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kittycad-modeling-cmds"
|
name = "kittycad-modeling-cmds"
|
||||||
version = "0.2.77"
|
version = "0.2.80"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/KittyCAD/modeling-api.git?branch=mike/multi-extrude-experimentation2#e2dd07d25f76b87f69eccdbd9f6b526da02f226c"
|
||||||
checksum = "3b77259b37acafa360d98af27431ac394bc8899eeed7037513832ddbee856811"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -1826,7 +1827,7 @@ dependencies = [
|
|||||||
"enum-iterator",
|
"enum-iterator",
|
||||||
"enum-iterator-derive",
|
"enum-iterator-derive",
|
||||||
"euler",
|
"euler",
|
||||||
"http 1.1.0",
|
"http 1.2.0",
|
||||||
"kittycad-modeling-cmds-macros",
|
"kittycad-modeling-cmds-macros",
|
||||||
"kittycad-unit-conversion-derive",
|
"kittycad-unit-conversion-derive",
|
||||||
"measurements",
|
"measurements",
|
||||||
@ -1842,24 +1843,22 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "kittycad-modeling-cmds-macros"
|
name = "kittycad-modeling-cmds-macros"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/KittyCAD/modeling-api.git?branch=mike/multi-extrude-experimentation2#e2dd07d25f76b87f69eccdbd9f6b526da02f226c"
|
||||||
checksum = "fb9bb1a594541b878adc1c8dcb821328774bf7aa09b65b104a206b1291a5235c"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"kittycad-modeling-cmds-macros-impl",
|
"kittycad-modeling-cmds-macros-impl",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kittycad-modeling-cmds-macros-impl"
|
name = "kittycad-modeling-cmds-macros-impl"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/KittyCAD/modeling-api.git?branch=mike/multi-extrude-experimentation2#e2dd07d25f76b87f69eccdbd9f6b526da02f226c"
|
||||||
checksum = "6607507a8a0e4273b943179f0a3ef8e90712308d1d3095246040c29cfdbf985b"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2009,7 +2008,7 @@ checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2308,7 +2307,7 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"regex-syntax 0.8.5",
|
"regex-syntax 0.8.5",
|
||||||
"structmeta",
|
"structmeta",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2322,7 +2321,7 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"regex-syntax 0.8.5",
|
"regex-syntax 0.8.5",
|
||||||
"structmeta",
|
"structmeta",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2362,7 +2361,7 @@ dependencies = [
|
|||||||
"pest_meta",
|
"pest_meta",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2420,7 +2419,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2550,14 +2549,14 @@ dependencies = [
|
|||||||
"proc-macro-error-attr2",
|
"proc-macro-error-attr2",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.89"
|
version = "1.0.92"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
|
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
@ -2609,7 +2608,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"pyo3-macros-backend",
|
"pyo3-macros-backend",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2622,7 +2621,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"pyo3-build-config",
|
"pyo3-build-config",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2860,7 +2859,7 @@ dependencies = [
|
|||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"h2",
|
"h2",
|
||||||
"http 1.1.0",
|
"http 1.2.0",
|
||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper 1.4.1",
|
"hyper 1.4.1",
|
||||||
@ -2902,7 +2901,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "f67ad7fdf5c0a015763fcd164bee294b13fb7b6f89f1b55961d40f00c3e32d6b"
|
checksum = "f67ad7fdf5c0a015763fcd164bee294b13fb7b6f89f1b55961d40f00c3e32d6b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"http 1.1.0",
|
"http 1.2.0",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"reqwest-middleware",
|
"reqwest-middleware",
|
||||||
]
|
]
|
||||||
@ -2915,7 +2914,7 @@ checksum = "d1ccd3b55e711f91a9885a2fa6fbbb2e39db1776420b062efc058c6410f7e5e3"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"http 1.1.0",
|
"http 1.2.0",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 1.0.68",
|
"thiserror 1.0.68",
|
||||||
@ -2932,7 +2931,7 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"futures",
|
"futures",
|
||||||
"getrandom",
|
"getrandom",
|
||||||
"http 1.1.0",
|
"http 1.2.0",
|
||||||
"hyper 1.4.1",
|
"hyper 1.4.1",
|
||||||
"parking_lot 0.11.2",
|
"parking_lot 0.11.2",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
@ -2953,7 +2952,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"getrandom",
|
"getrandom",
|
||||||
"http 1.1.0",
|
"http 1.2.0",
|
||||||
"matchit",
|
"matchit",
|
||||||
"opentelemetry",
|
"opentelemetry",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
@ -2971,6 +2970,12 @@ dependencies = [
|
|||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rgba_simple"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6cd655523701785087f69900df39892fb7b9b0721aa67682f571c38c32ac58a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.8"
|
version = "0.17.8"
|
||||||
@ -3151,7 +3156,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"serde_derive_internals",
|
"serde_derive_internals",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3191,9 +3196,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.215"
|
version = "1.0.216"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
|
checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
@ -3209,13 +3214,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.215"
|
version = "1.0.216"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
|
checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3226,7 +3231,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3250,7 +3255,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3271,7 +3276,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"serde",
|
"serde",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3420,7 +3425,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"structmeta-derive",
|
"structmeta-derive",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3431,7 +3436,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3453,7 +3458,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3496,9 +3501,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.87"
|
version = "2.0.90"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
|
checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -3522,7 +3527,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3630,7 +3635,7 @@ checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3641,7 +3646,7 @@ checksum = "22efd00f33f93fa62848a7cab956c3d38c8d43095efda1decfc2b3a5dc0b8972"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3753,7 +3758,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3895,7 +3900,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3923,7 +3928,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -4006,7 +4011,7 @@ checksum = "0ea0b99e8ec44abd6f94a18f28f7934437809dd062820797c52401298116f70e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
"termcolor",
|
"termcolor",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -4019,7 +4024,7 @@ dependencies = [
|
|||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
"data-encoding",
|
"data-encoding",
|
||||||
"http 1.1.0",
|
"http 1.2.0",
|
||||||
"httparse",
|
"httparse",
|
||||||
"log",
|
"log",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
@ -4185,7 +4190,7 @@ dependencies = [
|
|||||||
"proc-macro-error2",
|
"proc-macro-error2",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -4247,7 +4252,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -4282,7 +4287,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
"wasm-bindgen-backend",
|
"wasm-bindgen-backend",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
@ -4663,7 +4668,7 @@ checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -4685,7 +4690,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -4705,7 +4710,7 @@ checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -4734,7 +4739,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.87",
|
"syn 2.0.90",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -76,10 +76,13 @@ members = [
|
|||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
http = "1"
|
http = "1"
|
||||||
kittycad = { version = "0.3.28", default-features = false, features = ["js", "requests"] }
|
kittycad = { version = "0.3.28", default-features = false, features = ["js", "requests"] }
|
||||||
kittycad-modeling-cmds = { version = "0.2.77", features = ["websocket"] }
|
kittycad-modeling-cmds = { version = "0.2.80", features = ["websocket"] }
|
||||||
|
|
||||||
[workspace.lints.clippy]
|
[workspace.lints.clippy]
|
||||||
|
assertions_on_result_states = "warn"
|
||||||
|
dbg_macro = "warn"
|
||||||
iter_over_hash_type = "warn"
|
iter_over_hash_type = "warn"
|
||||||
|
lossy_float_literal = "warn"
|
||||||
|
|
||||||
[[test]]
|
[[test]]
|
||||||
name = "executor"
|
name = "executor"
|
||||||
@ -90,6 +93,7 @@ name = "modify"
|
|||||||
path = "tests/modify/main.rs"
|
path = "tests/modify/main.rs"
|
||||||
|
|
||||||
# Example: how to point modeling-api at a different repo (e.g. a branch or a local clone)
|
# Example: how to point modeling-api at a different repo (e.g. a branch or a local clone)
|
||||||
#[patch.crates-io]
|
[patch.crates-io]
|
||||||
|
kittycad-modeling-cmds = { git = "https://github.com/KittyCAD/modeling-api.git", branch = "mike/multi-extrude-experimentation2" }
|
||||||
#kittycad-modeling-cmds = { path = "../../../modeling-api/modeling-cmds" }
|
#kittycad-modeling-cmds = { path = "../../../modeling-api/modeling-cmds" }
|
||||||
#kittycad-modeling-session = { path = "../../../modeling-api/modeling-session" }
|
#kittycad-modeling-session = { path = "../../../modeling-api/modeling-session" }
|
||||||
|
@ -12,8 +12,8 @@ redo-kcl-stdlib-docs-no-imgs:
|
|||||||
# Generate the stdlib image artifacts
|
# Generate the stdlib image artifacts
|
||||||
# Then run the stdlib docs generation
|
# Then run the stdlib docs generation
|
||||||
redo-kcl-stdlib-docs:
|
redo-kcl-stdlib-docs:
|
||||||
TWENTY_TWENTY=overwrite {{cnr}} -p kcl-lib kcl_test_example
|
TWENTY_TWENTY=overwrite {{cnr}} -p kcl-lib --no-fail-fast -- kcl_test_example
|
||||||
EXPECTORATE=overwrite {{cnr}} -p kcl-lib docs::gen_std_tests::test_generate_stdlib
|
EXPECTORATE=overwrite {{cnr}} -p kcl-lib --no-fail-fast -- docs::gen_std_tests::test_generate_stdlib
|
||||||
|
|
||||||
# Copy a test KCL file from executor tests into a new simulation test.
|
# Copy a test KCL file from executor tests into a new simulation test.
|
||||||
copy-exec-test-into-sim-test test_name:
|
copy-exec-test-into-sim-test test_name:
|
||||||
|
@ -15,5 +15,5 @@ async fn kcl_to_core_test() {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert!(result.is_ok());
|
result.unwrap();
|
||||||
}
|
}
|
||||||
|
@ -40,10 +40,12 @@ miette = "7.2.0"
|
|||||||
mime_guess = "2.0.5"
|
mime_guess = "2.0.5"
|
||||||
parse-display = "0.9.1"
|
parse-display = "0.9.1"
|
||||||
pyo3 = { version = "0.22.6", optional = true }
|
pyo3 = { version = "0.22.6", optional = true }
|
||||||
|
regex = "1.11.1"
|
||||||
reqwest = { version = "0.12", default-features = false, features = [
|
reqwest = { version = "0.12", default-features = false, features = [
|
||||||
"stream",
|
"stream",
|
||||||
"rustls-tls",
|
"rustls-tls",
|
||||||
] }
|
] }
|
||||||
|
rgba_simple = "0.10.0"
|
||||||
ropey = "1.6.1"
|
ropey = "1.6.1"
|
||||||
schemars = { version = "0.8.17", features = [
|
schemars = { version = "0.8.17", features = [
|
||||||
"impl_json_schema",
|
"impl_json_schema",
|
||||||
|
@ -597,6 +597,8 @@ fn clean_function_name(name: &str) -> String {
|
|||||||
fn_name = fn_name.replace("seg_", "segment_");
|
fn_name = fn_name.replace("seg_", "segment_");
|
||||||
} else if fn_name.starts_with("log_") {
|
} else if fn_name.starts_with("log_") {
|
||||||
fn_name = fn_name.replace("log_", "log");
|
fn_name = fn_name.replace("log_", "log");
|
||||||
|
} else if fn_name.ends_with("tan_2") {
|
||||||
|
fn_name = fn_name.replace("tan_2", "tan2");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn_name
|
fn_name
|
||||||
|
@ -13,6 +13,8 @@ use tower_lsp::lsp_types::{
|
|||||||
MarkupKind, ParameterInformation, ParameterLabel, SignatureHelp, SignatureInformation,
|
MarkupKind, ParameterInformation, ParameterLabel, SignatureHelp, SignatureInformation,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::execution::Sketch;
|
||||||
|
|
||||||
use crate::std::Primitive;
|
use crate::std::Primitive;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
|
||||||
@ -232,6 +234,11 @@ pub trait StdLibFn: std::fmt::Debug + Send + Sync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn to_autocomplete_snippet(&self) -> Result<String> {
|
fn to_autocomplete_snippet(&self) -> Result<String> {
|
||||||
|
if self.name() == "loft" {
|
||||||
|
return Ok("loft([${0:sketch000}, ${1:sketch001}])${}".to_string());
|
||||||
|
} else if self.name() == "hole" {
|
||||||
|
return Ok("hole(${0:holeSketch}, ${1:%})${}".to_string());
|
||||||
|
}
|
||||||
let mut args = Vec::new();
|
let mut args = Vec::new();
|
||||||
let mut index = 0;
|
let mut index = 0;
|
||||||
for arg in self.args(true).iter() {
|
for arg in self.args(true).iter() {
|
||||||
@ -451,6 +458,16 @@ fn get_autocomplete_snippet_from_schema(
|
|||||||
) -> Result<Option<(usize, String)>> {
|
) -> Result<Option<(usize, String)>> {
|
||||||
match schema {
|
match schema {
|
||||||
schemars::schema::Schema::Object(o) => {
|
schemars::schema::Schema::Object(o) => {
|
||||||
|
// Check if the schema is the same as a Sketch.
|
||||||
|
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||||
|
// We set this so we can recurse them later.
|
||||||
|
settings.inline_subschemas = true;
|
||||||
|
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||||
|
let sketch_schema = generator.root_schema_for::<Sketch>().schema;
|
||||||
|
if sketch_schema.object == o.object {
|
||||||
|
return Ok(Some((index, format!("${{{}:sketch{}}}", index, "000"))));
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(serde_json::Value::Bool(nullable)) = o.extensions.get("nullable") {
|
if let Some(serde_json::Value::Bool(nullable)) = o.extensions.get("nullable") {
|
||||||
if *nullable {
|
if *nullable {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@ -489,6 +506,12 @@ fn get_autocomplete_snippet_from_schema(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if prop_name == "color" {
|
||||||
|
fn_docs.push_str(&format!("\t{}: ${{{}:\"#ff0000\"}},\n", prop_name, i));
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some((new_index, snippet)) = get_autocomplete_snippet_from_schema(prop, i)? {
|
if let Some((new_index, snippet)) = get_autocomplete_snippet_from_schema(prop, i)? {
|
||||||
fn_docs.push_str(&format!("\t{}: {},\n", prop_name, snippet));
|
fn_docs.push_str(&format!("\t{}: {},\n", prop_name, snippet));
|
||||||
i = new_index + 1;
|
i = new_index + 1;
|
||||||
@ -946,6 +969,47 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_autocomplete_snippet_appearance() {
|
||||||
|
let appearance_fn: Box<dyn StdLibFn> = Box::new(crate::std::appearance::Appearance);
|
||||||
|
let snippet = appearance_fn.to_autocomplete_snippet().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
snippet,
|
||||||
|
r#"appearance({
|
||||||
|
color: ${0:"#
|
||||||
|
.to_owned()
|
||||||
|
+ "\"#"
|
||||||
|
+ r#"ff0000"},
|
||||||
|
}, ${1:%})${}"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_autocomplete_snippet_loft() {
|
||||||
|
let loft_fn: Box<dyn StdLibFn> = Box::new(crate::std::loft::Loft);
|
||||||
|
let snippet = loft_fn.to_autocomplete_snippet().unwrap();
|
||||||
|
assert_eq!(snippet, r#"loft([${0:sketch000}, ${1:sketch001}])${}"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_autocomplete_snippet_sweep() {
|
||||||
|
let sweep_fn: Box<dyn StdLibFn> = Box::new(crate::std::sweep::Sweep);
|
||||||
|
let snippet = sweep_fn.to_autocomplete_snippet().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
snippet,
|
||||||
|
r#"sweep({
|
||||||
|
path: ${0:sketch000},
|
||||||
|
}, ${1:%})${}"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_autocomplete_snippet_hole() {
|
||||||
|
let hole_fn: Box<dyn StdLibFn> = Box::new(crate::std::sketch::Hole);
|
||||||
|
let snippet = hole_fn.to_autocomplete_snippet().unwrap();
|
||||||
|
assert_eq!(snippet, r#"hole(${0:holeSketch}, ${1:%})${}"#);
|
||||||
|
}
|
||||||
|
|
||||||
// We want to test the snippets we compile at lsp start.
|
// We want to test the snippets we compile at lsp start.
|
||||||
#[test]
|
#[test]
|
||||||
fn get_all_stdlib_autocomplete_snippets() {
|
fn get_all_stdlib_autocomplete_snippets() {
|
||||||
|
@ -120,6 +120,61 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the visibility of edges.
|
||||||
|
async fn set_edge_visibility(
|
||||||
|
&self,
|
||||||
|
visible: bool,
|
||||||
|
source_range: SourceRange,
|
||||||
|
) -> Result<(), crate::errors::KclError> {
|
||||||
|
self.batch_modeling_cmd(
|
||||||
|
uuid::Uuid::new_v4(),
|
||||||
|
source_range,
|
||||||
|
&ModelingCmd::from(mcmd::EdgeLinesVisible { hidden: !visible }),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_units(
|
||||||
|
&self,
|
||||||
|
units: crate::UnitLength,
|
||||||
|
source_range: SourceRange,
|
||||||
|
) -> Result<(), crate::errors::KclError> {
|
||||||
|
// Before we even start executing the program, set the units.
|
||||||
|
self.batch_modeling_cmd(
|
||||||
|
uuid::Uuid::new_v4(),
|
||||||
|
source_range,
|
||||||
|
&ModelingCmd::from(mcmd::SetSceneUnits { unit: units.into() }),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Re-run the command to apply the settings.
|
||||||
|
async fn reapply_settings(
|
||||||
|
&self,
|
||||||
|
settings: &crate::ExecutorSettings,
|
||||||
|
source_range: SourceRange,
|
||||||
|
) -> Result<(), crate::errors::KclError> {
|
||||||
|
// Set the edge visibility.
|
||||||
|
self.set_edge_visibility(settings.highlight_edges, source_range).await?;
|
||||||
|
|
||||||
|
// Change the units.
|
||||||
|
self.set_units(settings.units, source_range).await?;
|
||||||
|
|
||||||
|
// Send the command to show the grid.
|
||||||
|
self.modify_grid(!settings.show_grid, source_range).await?;
|
||||||
|
|
||||||
|
// We do not have commands for changing ssao on the fly.
|
||||||
|
|
||||||
|
// Flush the batch queue, so the settings are applied right away.
|
||||||
|
self.flush_batch(false, source_range).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// Add a modeling command to the batch but don't fire it right away.
|
// Add a modeling command to the batch but don't fire it right away.
|
||||||
async fn batch_modeling_cmd(
|
async fn batch_modeling_cmd(
|
||||||
&self,
|
&self,
|
||||||
@ -504,11 +559,11 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn modify_grid(&self, hidden: bool) -> Result<(), KclError> {
|
async fn modify_grid(&self, hidden: bool, source_range: SourceRange) -> Result<(), KclError> {
|
||||||
// Hide/show the grid.
|
// Hide/show the grid.
|
||||||
self.batch_modeling_cmd(
|
self.batch_modeling_cmd(
|
||||||
uuid::Uuid::new_v4(),
|
uuid::Uuid::new_v4(),
|
||||||
Default::default(),
|
source_range,
|
||||||
&ModelingCmd::from(mcmd::ObjectVisible {
|
&ModelingCmd::from(mcmd::ObjectVisible {
|
||||||
hidden,
|
hidden,
|
||||||
object_id: *GRID_OBJECT_ID,
|
object_id: *GRID_OBJECT_ID,
|
||||||
@ -519,7 +574,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
// Hide/show the grid scale text.
|
// Hide/show the grid scale text.
|
||||||
self.batch_modeling_cmd(
|
self.batch_modeling_cmd(
|
||||||
uuid::Uuid::new_v4(),
|
uuid::Uuid::new_v4(),
|
||||||
Default::default(),
|
source_range,
|
||||||
&ModelingCmd::from(mcmd::ObjectVisible {
|
&ModelingCmd::from(mcmd::ObjectVisible {
|
||||||
hidden,
|
hidden,
|
||||||
object_id: *GRID_SCALE_TEXT_OBJECT_ID,
|
object_id: *GRID_SCALE_TEXT_OBJECT_ID,
|
||||||
@ -527,8 +582,6 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
self.flush_batch(false, Default::default()).await?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
50
src/wasm-lib/kcl/src/execution/cache.rs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
//! Functions for helping with caching an ast and finding the parts the changed.
|
||||||
|
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
execution::ExecState,
|
||||||
|
parsing::ast::types::{Node, Program},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Information for the caching an AST and smartly re-executing it if we can.
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct CacheInformation {
|
||||||
|
/// The old information.
|
||||||
|
pub old: Option<OldAstState>,
|
||||||
|
/// The new ast to executed.
|
||||||
|
pub new_ast: Node<Program>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The old ast and program memory.
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct OldAstState {
|
||||||
|
/// The ast.
|
||||||
|
pub ast: Node<Program>,
|
||||||
|
/// The exec state.
|
||||||
|
pub exec_state: ExecState,
|
||||||
|
/// The last settings used for execution.
|
||||||
|
pub settings: crate::execution::ExecutorSettings,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::Program> for CacheInformation {
|
||||||
|
fn from(program: crate::Program) -> Self {
|
||||||
|
CacheInformation {
|
||||||
|
old: None,
|
||||||
|
new_ast: program.ast,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The result of a cache check.
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct CacheResult {
|
||||||
|
/// Should we clear the scene and start over?
|
||||||
|
pub clear_scene: bool,
|
||||||
|
/// The program that needs to be executed.
|
||||||
|
pub program: Node<Program>,
|
||||||
|
}
|
@ -326,29 +326,12 @@ async fn inner_execute_pipe_body(
|
|||||||
ctx: &ExecutorContext,
|
ctx: &ExecutorContext,
|
||||||
) -> Result<KclValue, KclError> {
|
) -> Result<KclValue, KclError> {
|
||||||
for expression in body {
|
for expression in body {
|
||||||
match expression {
|
if let Expr::TagDeclarator(_) = expression {
|
||||||
Expr::TagDeclarator(_) => {
|
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
message: format!("This cannot be in a PipeExpression: {:?}", expression),
|
message: format!("This cannot be in a PipeExpression: {:?}", expression),
|
||||||
source_ranges: vec![expression.into()],
|
source_ranges: vec![expression.into()],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
Expr::Literal(_)
|
|
||||||
| Expr::Identifier(_)
|
|
||||||
| Expr::BinaryExpression(_)
|
|
||||||
| Expr::FunctionExpression(_)
|
|
||||||
| Expr::CallExpression(_)
|
|
||||||
| Expr::CallExpressionKw(_)
|
|
||||||
| Expr::PipeExpression(_)
|
|
||||||
| Expr::PipeSubstitution(_)
|
|
||||||
| Expr::ArrayExpression(_)
|
|
||||||
| Expr::ArrayRangeExpression(_)
|
|
||||||
| Expr::ObjectExpression(_)
|
|
||||||
| Expr::MemberExpression(_)
|
|
||||||
| Expr::UnaryExpression(_)
|
|
||||||
| Expr::IfExpression(_)
|
|
||||||
| Expr::None(_) => {}
|
|
||||||
};
|
|
||||||
let metadata = Metadata {
|
let metadata = Metadata {
|
||||||
source_range: SourceRange::from(expression),
|
source_range: SourceRange::from(expression),
|
||||||
};
|
};
|
||||||
@ -366,9 +349,11 @@ impl Node<CallExpressionKw> {
|
|||||||
#[async_recursion]
|
#[async_recursion]
|
||||||
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
||||||
let fn_name = &self.callee.name;
|
let fn_name = &self.callee.name;
|
||||||
|
let callsite: SourceRange = self.into();
|
||||||
|
|
||||||
// Build a hashmap from argument labels to the final evaluated values.
|
// Build a hashmap from argument labels to the final evaluated values.
|
||||||
let mut fn_args = HashMap::with_capacity(self.arguments.len());
|
let mut fn_args = HashMap::with_capacity(self.arguments.len());
|
||||||
|
let mut tag_declarator_args = Vec::new();
|
||||||
for arg_expr in &self.arguments {
|
for arg_expr in &self.arguments {
|
||||||
let source_range = SourceRange::from(arg_expr.arg.clone());
|
let source_range = SourceRange::from(arg_expr.arg.clone());
|
||||||
let metadata = Metadata { source_range };
|
let metadata = Metadata { source_range };
|
||||||
@ -376,8 +361,12 @@ impl Node<CallExpressionKw> {
|
|||||||
.execute_expr(&arg_expr.arg, exec_state, &metadata, StatementKind::Expression)
|
.execute_expr(&arg_expr.arg, exec_state, &metadata, StatementKind::Expression)
|
||||||
.await?;
|
.await?;
|
||||||
fn_args.insert(arg_expr.label.name.clone(), Arg::new(value, source_range));
|
fn_args.insert(arg_expr.label.name.clone(), Arg::new(value, source_range));
|
||||||
|
if let Expr::TagDeclarator(td) = &arg_expr.arg {
|
||||||
|
tag_declarator_args.push((td.inner.clone(), source_range));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let fn_args = fn_args; // remove mutability
|
let fn_args = fn_args; // remove mutability
|
||||||
|
let tag_declarator_args = tag_declarator_args; // remove mutability
|
||||||
|
|
||||||
// Evaluate the unlabeled first param, if any exists.
|
// Evaluate the unlabeled first param, if any exists.
|
||||||
let unlabeled = if let Some(ref arg_expr) = self.unlabeled {
|
let unlabeled = if let Some(ref arg_expr) = self.unlabeled {
|
||||||
@ -403,11 +392,43 @@ impl Node<CallExpressionKw> {
|
|||||||
FunctionKind::Core(func) => {
|
FunctionKind::Core(func) => {
|
||||||
// Attempt to call the function.
|
// Attempt to call the function.
|
||||||
let mut result = func.std_lib_fn()(exec_state, args).await?;
|
let mut result = func.std_lib_fn()(exec_state, args).await?;
|
||||||
update_memory_for_tags_of_geometry(&mut result, exec_state)?;
|
update_memory_for_tags_of_geometry(&mut result, &tag_declarator_args, exec_state)?;
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
FunctionKind::UserDefined => {
|
FunctionKind::UserDefined => {
|
||||||
todo!("Part of modeling-app#4600: Support keyword arguments for user-defined functions")
|
let source_range = SourceRange::from(self);
|
||||||
|
// Clone the function so that we can use a mutable reference to
|
||||||
|
// exec_state.
|
||||||
|
let func = exec_state.memory.get(fn_name, source_range)?.clone();
|
||||||
|
let fn_dynamic_state = exec_state.dynamic_state.merge(&exec_state.memory);
|
||||||
|
|
||||||
|
let return_value = {
|
||||||
|
let previous_dynamic_state = std::mem::replace(&mut exec_state.dynamic_state, fn_dynamic_state);
|
||||||
|
let result = func
|
||||||
|
.call_fn_kw(args, exec_state, ctx.clone(), callsite)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
// Add the call expression to the source ranges.
|
||||||
|
// TODO currently ignored by the frontend
|
||||||
|
e.add_source_ranges(vec![source_range])
|
||||||
|
});
|
||||||
|
exec_state.dynamic_state = previous_dynamic_state;
|
||||||
|
result?
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = return_value.ok_or_else(move || {
|
||||||
|
let mut source_ranges: Vec<SourceRange> = vec![source_range];
|
||||||
|
// We want to send the source range of the original function.
|
||||||
|
if let KclValue::Function { meta, .. } = func {
|
||||||
|
source_ranges = meta.iter().map(|m| m.source_range).collect();
|
||||||
|
};
|
||||||
|
KclError::UndefinedValue(KclErrorDetails {
|
||||||
|
message: format!("Result of user-defined function {} is undefined", fn_name),
|
||||||
|
source_ranges,
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -419,6 +440,7 @@ impl Node<CallExpression> {
|
|||||||
let fn_name = &self.callee.name;
|
let fn_name = &self.callee.name;
|
||||||
|
|
||||||
let mut fn_args: Vec<Arg> = Vec::with_capacity(self.arguments.len());
|
let mut fn_args: Vec<Arg> = Vec::with_capacity(self.arguments.len());
|
||||||
|
let mut tag_declarator_args = Vec::new();
|
||||||
|
|
||||||
for arg_expr in &self.arguments {
|
for arg_expr in &self.arguments {
|
||||||
let metadata = Metadata {
|
let metadata = Metadata {
|
||||||
@ -428,15 +450,19 @@ impl Node<CallExpression> {
|
|||||||
.execute_expr(arg_expr, exec_state, &metadata, StatementKind::Expression)
|
.execute_expr(arg_expr, exec_state, &metadata, StatementKind::Expression)
|
||||||
.await?;
|
.await?;
|
||||||
let arg = Arg::new(value, SourceRange::from(arg_expr));
|
let arg = Arg::new(value, SourceRange::from(arg_expr));
|
||||||
|
if let Expr::TagDeclarator(td) = arg_expr {
|
||||||
|
tag_declarator_args.push((td.inner.clone(), arg.source_range));
|
||||||
|
}
|
||||||
fn_args.push(arg);
|
fn_args.push(arg);
|
||||||
}
|
}
|
||||||
|
let tag_declarator_args = tag_declarator_args; // remove mutability
|
||||||
|
|
||||||
match ctx.stdlib.get_either(fn_name) {
|
match ctx.stdlib.get_either(fn_name) {
|
||||||
FunctionKind::Core(func) => {
|
FunctionKind::Core(func) => {
|
||||||
// Attempt to call the function.
|
// Attempt to call the function.
|
||||||
let args = crate::std::Args::new(fn_args, self.into(), ctx.clone());
|
let args = crate::std::Args::new(fn_args, self.into(), ctx.clone());
|
||||||
let mut result = func.std_lib_fn()(exec_state, args).await?;
|
let mut result = func.std_lib_fn()(exec_state, args).await?;
|
||||||
update_memory_for_tags_of_geometry(&mut result, exec_state)?;
|
update_memory_for_tags_of_geometry(&mut result, &tag_declarator_args, exec_state)?;
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
FunctionKind::UserDefined => {
|
FunctionKind::UserDefined => {
|
||||||
@ -475,7 +501,24 @@ impl Node<CallExpression> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut ExecState) -> Result<(), KclError> {
|
/// `tag_declarator_args` should only contain tag declarator literals, which
|
||||||
|
/// will be defined as local variables. Non-literals that evaluate to tag
|
||||||
|
/// declarators should not be defined.
|
||||||
|
fn update_memory_for_tags_of_geometry(
|
||||||
|
result: &mut KclValue,
|
||||||
|
tag_declarator_args: &[(TagDeclarator, SourceRange)],
|
||||||
|
exec_state: &mut ExecState,
|
||||||
|
) -> Result<(), KclError> {
|
||||||
|
// Define all the tags in the memory.
|
||||||
|
for (tag_declarator, arg_sr) in tag_declarator_args {
|
||||||
|
let tag = TagIdentifier {
|
||||||
|
value: tag_declarator.name.clone(),
|
||||||
|
info: None,
|
||||||
|
meta: vec![Metadata { source_range: *arg_sr }],
|
||||||
|
};
|
||||||
|
|
||||||
|
exec_state.memory.add_tag(&tag.value, tag.clone(), *arg_sr)?;
|
||||||
|
}
|
||||||
// If the return result is a sketch or solid, we want to update the
|
// If the return result is a sketch or solid, we want to update the
|
||||||
// memory for the tags of the group.
|
// memory for the tags of the group.
|
||||||
// TODO: This could probably be done in a better way, but as of now this was my only idea
|
// TODO: This could probably be done in a better way, but as of now this was my only idea
|
||||||
@ -483,7 +526,7 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
|
|||||||
match result {
|
match result {
|
||||||
KclValue::Sketch { value: ref mut sketch } => {
|
KclValue::Sketch { value: ref mut sketch } => {
|
||||||
for (_, tag) in sketch.tags.iter() {
|
for (_, tag) in sketch.tags.iter() {
|
||||||
exec_state.memory.update_tag(&tag.value, tag.clone())?;
|
exec_state.memory.update_tag_if_defined(&tag.value, tag.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KclValue::Solid(ref mut solid) => {
|
KclValue::Solid(ref mut solid) => {
|
||||||
@ -521,7 +564,7 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
|
|||||||
info.sketch = solid.id;
|
info.sketch = solid.id;
|
||||||
t.info = Some(info);
|
t.info = Some(info);
|
||||||
|
|
||||||
exec_state.memory.update_tag(&tag.name, t.clone())?;
|
exec_state.memory.update_tag_if_defined(&tag.name, t.clone());
|
||||||
|
|
||||||
// update the sketch tags.
|
// update the sketch tags.
|
||||||
solid.sketch.tags.insert(tag.name.clone(), t);
|
solid.sketch.tags.insert(tag.name.clone(), t);
|
||||||
@ -542,22 +585,6 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Node<TagDeclarator> {
|
|
||||||
pub async fn execute(&self, exec_state: &mut ExecState) -> Result<KclValue, KclError> {
|
|
||||||
let memory_item = KclValue::TagIdentifier(Box::new(TagIdentifier {
|
|
||||||
value: self.name.clone(),
|
|
||||||
info: None,
|
|
||||||
meta: vec![Metadata {
|
|
||||||
source_range: self.into(),
|
|
||||||
}],
|
|
||||||
}));
|
|
||||||
|
|
||||||
exec_state.memory.add(&self.name, memory_item.clone(), self.into())?;
|
|
||||||
|
|
||||||
Ok(self.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Node<ArrayExpression> {
|
impl Node<ArrayExpression> {
|
||||||
#[async_recursion]
|
#[async_recursion]
|
||||||
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
||||||
|
@ -72,6 +72,10 @@ pub enum KclValue {
|
|||||||
ImportedGeometry(ImportedGeometry),
|
ImportedGeometry(ImportedGeometry),
|
||||||
#[ts(skip)]
|
#[ts(skip)]
|
||||||
Function {
|
Function {
|
||||||
|
/// Adam Chalmers speculation:
|
||||||
|
/// Reference to a KCL stdlib function (written in Rust).
|
||||||
|
/// Some if the KCL value is an alias of a stdlib function,
|
||||||
|
/// None if it's a KCL function written/declared in KCL.
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
func: Option<MemoryFunction>,
|
func: Option<MemoryFunction>,
|
||||||
#[schemars(skip)]
|
#[schemars(skip)]
|
||||||
@ -503,4 +507,39 @@ impl KclValue {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If this is a function, call it by applying keyword arguments.
|
||||||
|
/// If it's not a function, returns an error.
|
||||||
|
pub async fn call_fn_kw(
|
||||||
|
&self,
|
||||||
|
args: crate::std::Args,
|
||||||
|
exec_state: &mut ExecState,
|
||||||
|
ctx: ExecutorContext,
|
||||||
|
callsite: SourceRange,
|
||||||
|
) -> Result<Option<KclValue>, KclError> {
|
||||||
|
let KclValue::Function {
|
||||||
|
func,
|
||||||
|
expression,
|
||||||
|
memory: closure_memory,
|
||||||
|
meta: _,
|
||||||
|
} = &self
|
||||||
|
else {
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: "cannot call this because it isn't a function".to_string(),
|
||||||
|
source_ranges: vec![callsite],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
if let Some(_func) = func {
|
||||||
|
todo!("Implement calling KCL stdlib fns that are aliased. Part of https://github.com/KittyCAD/modeling-app/issues/4600");
|
||||||
|
} else {
|
||||||
|
crate::execution::call_user_defined_function_kw(
|
||||||
|
args.kw_args,
|
||||||
|
closure_memory.as_ref(),
|
||||||
|
expression.as_ref(),
|
||||||
|
exec_state,
|
||||||
|
&ctx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,26 +23,25 @@ type Point3D = kcmc::shared::Point3d<f64>;
|
|||||||
pub use function_param::FunctionParam;
|
pub use function_param::FunctionParam;
|
||||||
pub use kcl_value::{KclObjectFields, KclValue};
|
pub use kcl_value::{KclObjectFields, KclValue};
|
||||||
|
|
||||||
|
pub(crate) mod cache;
|
||||||
|
mod exec_ast;
|
||||||
|
mod function_param;
|
||||||
|
mod kcl_value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
engine::{EngineManager, ExecutionKind},
|
engine::{EngineManager, ExecutionKind},
|
||||||
errors::{KclError, KclErrorDetails},
|
errors::{KclError, KclErrorDetails},
|
||||||
|
execution::cache::{CacheInformation, CacheResult},
|
||||||
fs::{FileManager, FileSystem},
|
fs::{FileManager, FileSystem},
|
||||||
parsing::ast::{
|
parsing::ast::types::{
|
||||||
cache::{get_changed_program, CacheInformation},
|
|
||||||
types::{
|
|
||||||
BodyItem, Expr, FunctionExpression, ImportSelector, ItemVisibility, Node, NodeRef, TagDeclarator, TagNode,
|
BodyItem, Expr, FunctionExpression, ImportSelector, ItemVisibility, Node, NodeRef, TagDeclarator, TagNode,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
settings::types::UnitLength,
|
settings::types::UnitLength,
|
||||||
source_range::{ModuleId, SourceRange},
|
source_range::{ModuleId, SourceRange},
|
||||||
std::{args::Arg, StdLib},
|
std::{args::Arg, StdLib},
|
||||||
ExecError, Program,
|
ExecError, Program,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod exec_ast;
|
|
||||||
mod function_param;
|
|
||||||
mod kcl_value;
|
|
||||||
|
|
||||||
/// State for executing a program.
|
/// State for executing a program.
|
||||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
@ -125,10 +124,16 @@ impl ProgramMemory {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_tag(&mut self, tag: &str, value: TagIdentifier) -> Result<(), KclError> {
|
pub fn add_tag(&mut self, tag: &str, value: TagIdentifier, source_range: SourceRange) -> Result<(), KclError> {
|
||||||
self.environments[self.current_env.index()].insert(tag.to_string(), KclValue::TagIdentifier(Box::new(value)));
|
self.add(tag, KclValue::TagIdentifier(Box::new(value)), source_range)
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
pub fn update_tag_if_defined(&mut self, tag: &str, value: TagIdentifier) {
|
||||||
|
if !self.environments[self.current_env.index()].contains_key(tag) {
|
||||||
|
// Do nothing if the tag isn't defined.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.environments[self.current_env.index()].insert(tag.to_string(), KclValue::TagIdentifier(Box::new(value)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a value from the program memory.
|
/// Get a value from the program memory.
|
||||||
@ -845,7 +850,7 @@ impl GetTangentialInfoFromPathsResult {
|
|||||||
|
|
||||||
impl Sketch {
|
impl Sketch {
|
||||||
pub(crate) fn add_tag(&mut self, tag: NodeRef<'_, TagDeclarator>, current_path: &Path) {
|
pub(crate) fn add_tag(&mut self, tag: NodeRef<'_, TagDeclarator>, current_path: &Path) {
|
||||||
let mut tag_identifier: TagIdentifier = tag.into();
|
let mut tag_identifier = TagIdentifier::from(tag);
|
||||||
let base = current_path.get_base();
|
let base = current_path.get_base();
|
||||||
tag_identifier.info = Some(TagEngineInfo {
|
tag_identifier.info = Some(TagEngineInfo {
|
||||||
id: base.geo_meta.id,
|
id: base.geo_meta.id,
|
||||||
@ -1654,17 +1659,6 @@ impl ExecutorContext {
|
|||||||
let engine: Arc<Box<dyn EngineManager>> =
|
let engine: Arc<Box<dyn EngineManager>> =
|
||||||
Arc::new(Box::new(crate::engine::conn::EngineConnection::new(ws).await?));
|
Arc::new(Box::new(crate::engine::conn::EngineConnection::new(ws).await?));
|
||||||
|
|
||||||
// Set the edge visibility.
|
|
||||||
engine
|
|
||||||
.batch_modeling_cmd(
|
|
||||||
uuid::Uuid::new_v4(),
|
|
||||||
SourceRange::default(),
|
|
||||||
&ModelingCmd::from(mcmd::EdgeLinesVisible {
|
|
||||||
hidden: !settings.highlight_edges,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
engine,
|
engine,
|
||||||
fs: Arc::new(FileManager::new()),
|
fs: Arc::new(FileManager::new()),
|
||||||
@ -1691,7 +1685,7 @@ impl ExecutorContext {
|
|||||||
pub async fn new(
|
pub async fn new(
|
||||||
engine_manager: crate::engine::conn_wasm::EngineCommandManager,
|
engine_manager: crate::engine::conn_wasm::EngineCommandManager,
|
||||||
fs_manager: crate::fs::wasm::FileSystemManager,
|
fs_manager: crate::fs::wasm::FileSystemManager,
|
||||||
units: UnitLength,
|
settings: ExecutorSettings,
|
||||||
) -> Result<Self, String> {
|
) -> Result<Self, String> {
|
||||||
Ok(ExecutorContext {
|
Ok(ExecutorContext {
|
||||||
engine: Arc::new(Box::new(
|
engine: Arc::new(Box::new(
|
||||||
@ -1701,16 +1695,16 @@ impl ExecutorContext {
|
|||||||
)),
|
)),
|
||||||
fs: Arc::new(FileManager::new(fs_manager)),
|
fs: Arc::new(FileManager::new(fs_manager)),
|
||||||
stdlib: Arc::new(StdLib::new()),
|
stdlib: Arc::new(StdLib::new()),
|
||||||
settings: ExecutorSettings {
|
settings,
|
||||||
units,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
context_type: ContextType::Live,
|
context_type: ContextType::Live,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub async fn new_mock(fs_manager: crate::fs::wasm::FileSystemManager, units: UnitLength) -> Result<Self, String> {
|
pub async fn new_mock(
|
||||||
|
fs_manager: crate::fs::wasm::FileSystemManager,
|
||||||
|
settings: ExecutorSettings,
|
||||||
|
) -> Result<Self, String> {
|
||||||
Ok(ExecutorContext {
|
Ok(ExecutorContext {
|
||||||
engine: Arc::new(Box::new(
|
engine: Arc::new(Box::new(
|
||||||
crate::engine::conn_mock::EngineConnection::new()
|
crate::engine::conn_mock::EngineConnection::new()
|
||||||
@ -1719,10 +1713,7 @@ impl ExecutorContext {
|
|||||||
)),
|
)),
|
||||||
fs: Arc::new(FileManager::new(fs_manager)),
|
fs: Arc::new(FileManager::new(fs_manager)),
|
||||||
stdlib: Arc::new(StdLib::new()),
|
stdlib: Arc::new(StdLib::new()),
|
||||||
settings: ExecutorSettings {
|
settings,
|
||||||
units,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
context_type: ContextType::Mock,
|
context_type: ContextType::Mock,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1811,6 +1802,71 @@ impl ExecutorContext {
|
|||||||
// AND if we aren't in wasm it doesn't really matter.
|
// AND if we aren't in wasm it doesn't really matter.
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
// Given an old ast, old program memory and new ast, find the parts of the code that need to be
|
||||||
|
// re-executed.
|
||||||
|
// This function should never error, because in the case of any internal error, we should just pop
|
||||||
|
// the cache.
|
||||||
|
pub async fn get_changed_program(&self, info: CacheInformation) -> Option<CacheResult> {
|
||||||
|
let Some(old) = info.old else {
|
||||||
|
// We have no old info, we need to re-execute the whole thing.
|
||||||
|
return Some(CacheResult {
|
||||||
|
clear_scene: true,
|
||||||
|
program: info.new_ast,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the settings are different we might need to bust the cache.
|
||||||
|
// We specifically do this before checking if they are the exact same.
|
||||||
|
if old.settings != self.settings {
|
||||||
|
// If the units are different we need to re-execute the whole thing.
|
||||||
|
if old.settings.units != self.settings.units {
|
||||||
|
return Some(CacheResult {
|
||||||
|
clear_scene: true,
|
||||||
|
program: info.new_ast,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If anything else is different we do not need to re-execute, but rather just
|
||||||
|
// run the settings again.
|
||||||
|
|
||||||
|
if self
|
||||||
|
.engine
|
||||||
|
.reapply_settings(&self.settings, Default::default())
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
// Bust the cache, we errored.
|
||||||
|
return Some(CacheResult {
|
||||||
|
clear_scene: true,
|
||||||
|
program: info.new_ast,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the ASTs are the EXACT same we return None.
|
||||||
|
// We don't even need to waste time computing the digests.
|
||||||
|
if old.ast == info.new_ast {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut old_ast = old.ast.inner;
|
||||||
|
old_ast.compute_digest();
|
||||||
|
let mut new_ast = info.new_ast.inner.clone();
|
||||||
|
new_ast.compute_digest();
|
||||||
|
|
||||||
|
// Check if the digest is the same.
|
||||||
|
if old_ast.digest == new_ast.digest {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the changes were only to Non-code areas, like comments or whitespace.
|
||||||
|
|
||||||
|
// For any unhandled cases just re-execute the whole thing.
|
||||||
|
Some(CacheResult {
|
||||||
|
clear_scene: true,
|
||||||
|
program: info.new_ast,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Perform the execution of a program.
|
/// Perform the execution of a program.
|
||||||
/// You can optionally pass in some initialization memory.
|
/// You can optionally pass in some initialization memory.
|
||||||
@ -1831,7 +1887,7 @@ impl ExecutorContext {
|
|||||||
let _stats = crate::log::LogPerfStats::new("Interpretation");
|
let _stats = crate::log::LogPerfStats::new("Interpretation");
|
||||||
|
|
||||||
// Get the program that actually changed from the old and new information.
|
// Get the program that actually changed from the old and new information.
|
||||||
let cache_result = get_changed_program(cache_info.clone(), &self.settings);
|
let cache_result = self.get_changed_program(cache_info.clone()).await;
|
||||||
|
|
||||||
// Check if we don't need to re-execute.
|
// Check if we don't need to re-execute.
|
||||||
let Some(cache_result) = cache_result else {
|
let Some(cache_result) = cache_result else {
|
||||||
@ -1848,23 +1904,9 @@ impl ExecutorContext {
|
|||||||
|
|
||||||
// TODO: Use the top-level file's path.
|
// TODO: Use the top-level file's path.
|
||||||
exec_state.add_module(std::path::PathBuf::from(""));
|
exec_state.add_module(std::path::PathBuf::from(""));
|
||||||
// Before we even start executing the program, set the units.
|
|
||||||
self.engine
|
// Re-apply the settings, in case the cache was busted.
|
||||||
.batch_modeling_cmd(
|
self.engine.reapply_settings(&self.settings, Default::default()).await?;
|
||||||
exec_state.id_generator.next_uuid(),
|
|
||||||
SourceRange::default(),
|
|
||||||
&ModelingCmd::from(mcmd::SetSceneUnits {
|
|
||||||
unit: match self.settings.units {
|
|
||||||
UnitLength::Cm => kcmc::units::UnitLength::Centimeters,
|
|
||||||
UnitLength::Ft => kcmc::units::UnitLength::Feet,
|
|
||||||
UnitLength::In => kcmc::units::UnitLength::Inches,
|
|
||||||
UnitLength::M => kcmc::units::UnitLength::Meters,
|
|
||||||
UnitLength::Mm => kcmc::units::UnitLength::Millimeters,
|
|
||||||
UnitLength::Yd => kcmc::units::UnitLength::Yards,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
self.inner_execute(&cache_result.program, exec_state, crate::execution::BodyType::Root)
|
self.inner_execute(&cache_result.program, exec_state, crate::execution::BodyType::Root)
|
||||||
.await?;
|
.await?;
|
||||||
@ -2075,7 +2117,8 @@ impl ExecutorContext {
|
|||||||
Ok((module_memory, module_exports))
|
Ok((module_memory, module_exports))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn execute_expr<'a>(
|
#[async_recursion]
|
||||||
|
pub async fn execute_expr<'a: 'async_recursion>(
|
||||||
&self,
|
&self,
|
||||||
init: &Expr,
|
init: &Expr,
|
||||||
exec_state: &mut ExecState,
|
exec_state: &mut ExecState,
|
||||||
@ -2085,7 +2128,7 @@ impl ExecutorContext {
|
|||||||
let item = match init {
|
let item = match init {
|
||||||
Expr::None(none) => KclValue::from(none),
|
Expr::None(none) => KclValue::from(none),
|
||||||
Expr::Literal(literal) => KclValue::from(literal),
|
Expr::Literal(literal) => KclValue::from(literal),
|
||||||
Expr::TagDeclarator(tag) => tag.execute(exec_state).await?,
|
Expr::TagDeclarator(tag) => KclValue::from(tag),
|
||||||
Expr::Identifier(identifier) => {
|
Expr::Identifier(identifier) => {
|
||||||
let value = exec_state.memory.get(&identifier.name, identifier.into())?;
|
let value = exec_state.memory.get(&identifier.name, identifier.into())?;
|
||||||
value.clone()
|
value.clone()
|
||||||
@ -2132,6 +2175,14 @@ impl ExecutorContext {
|
|||||||
Expr::MemberExpression(member_expression) => member_expression.get_result(exec_state)?,
|
Expr::MemberExpression(member_expression) => member_expression.get_result(exec_state)?,
|
||||||
Expr::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, self).await?,
|
Expr::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, self).await?,
|
||||||
Expr::IfExpression(expr) => expr.get_result(exec_state, self).await?,
|
Expr::IfExpression(expr) => expr.get_result(exec_state, self).await?,
|
||||||
|
Expr::LabelledExpression(expr) => {
|
||||||
|
let result = self
|
||||||
|
.execute_expr(&expr.expr, exec_state, metadata, statement_kind)
|
||||||
|
.await?;
|
||||||
|
exec_state.memory.add(&expr.label.name, result.clone(), init.into())?;
|
||||||
|
// TODO this lets us use the label as a variable name, but not as a tag in most cases
|
||||||
|
result
|
||||||
|
}
|
||||||
};
|
};
|
||||||
Ok(item)
|
Ok(item)
|
||||||
}
|
}
|
||||||
@ -2141,23 +2192,8 @@ impl ExecutorContext {
|
|||||||
self.settings.units = units;
|
self.settings.units = units;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute the program, then get a PNG screenshot.
|
/// Get a snapshot of the current scene.
|
||||||
pub async fn execute_and_prepare_snapshot(
|
pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
|
||||||
&self,
|
|
||||||
program: &Program,
|
|
||||||
exec_state: &mut ExecState,
|
|
||||||
) -> std::result::Result<TakeSnapshot, ExecError> {
|
|
||||||
self.execute_and_prepare(program, exec_state).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Execute the program, return the interpreter and outputs.
|
|
||||||
pub async fn execute_and_prepare(
|
|
||||||
&self,
|
|
||||||
program: &Program,
|
|
||||||
exec_state: &mut ExecState,
|
|
||||||
) -> std::result::Result<TakeSnapshot, ExecError> {
|
|
||||||
self.run(program.clone().into(), exec_state).await?;
|
|
||||||
|
|
||||||
// Zoom to fit.
|
// Zoom to fit.
|
||||||
self.engine
|
self.engine
|
||||||
.send_modeling_cmd(
|
.send_modeling_cmd(
|
||||||
@ -2193,6 +2229,17 @@ impl ExecutorContext {
|
|||||||
};
|
};
|
||||||
Ok(contents)
|
Ok(contents)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Execute the program, then get a PNG screenshot.
|
||||||
|
pub async fn execute_and_prepare_snapshot(
|
||||||
|
&self,
|
||||||
|
program: &Program,
|
||||||
|
exec_state: &mut ExecState,
|
||||||
|
) -> std::result::Result<TakeSnapshot, ExecError> {
|
||||||
|
self.run(program.clone().into(), exec_state).await?;
|
||||||
|
|
||||||
|
self.prepare_snapshot().await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For each argument given,
|
/// For each argument given,
|
||||||
@ -2247,6 +2294,59 @@ fn assign_args_to_params(
|
|||||||
Ok(fn_memory)
|
Ok(fn_memory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn assign_args_to_params_kw(
|
||||||
|
function_expression: NodeRef<'_, FunctionExpression>,
|
||||||
|
mut args: crate::std::args::KwArgs,
|
||||||
|
mut fn_memory: ProgramMemory,
|
||||||
|
) -> Result<ProgramMemory, KclError> {
|
||||||
|
// Add the arguments to the memory. A new call frame should have already
|
||||||
|
// been created.
|
||||||
|
let source_ranges = vec![function_expression.into()];
|
||||||
|
for param in function_expression.params.iter() {
|
||||||
|
if param.labeled {
|
||||||
|
let arg = args.labeled.get(¶m.identifier.name);
|
||||||
|
let arg_val = match arg {
|
||||||
|
Some(arg) => arg.value.clone(),
|
||||||
|
None => match param.default_value {
|
||||||
|
Some(ref default_val) => KclValue::from(default_val.clone()),
|
||||||
|
None => {
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
source_ranges,
|
||||||
|
message: format!(
|
||||||
|
"This function requires a parameter {}, but you haven't passed it one.",
|
||||||
|
param.identifier.name
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
fn_memory.add(¶m.identifier.name, arg_val, (¶m.identifier).into())?;
|
||||||
|
} else {
|
||||||
|
let Some(unlabeled) = args.unlabeled.take() else {
|
||||||
|
let param_name = ¶m.identifier.name;
|
||||||
|
return Err(if args.labeled.contains_key(param_name) {
|
||||||
|
KclError::Semantic(KclErrorDetails {
|
||||||
|
source_ranges,
|
||||||
|
message: format!("The function does declare a parameter named '{param_name}', but this parameter doesn't use a label. Try removing the `{param_name}:`"),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
KclError::Semantic(KclErrorDetails {
|
||||||
|
source_ranges,
|
||||||
|
message: "This function expects an unlabeled first parameter, but you haven't passed it one."
|
||||||
|
.to_owned(),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
fn_memory.add(
|
||||||
|
¶m.identifier.name,
|
||||||
|
unlabeled.value.clone(),
|
||||||
|
(¶m.identifier).into(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(fn_memory)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn call_user_defined_function(
|
pub(crate) async fn call_user_defined_function(
|
||||||
args: Vec<Arg>,
|
args: Vec<Arg>,
|
||||||
memory: &ProgramMemory,
|
memory: &ProgramMemory,
|
||||||
@ -2277,6 +2377,36 @@ pub(crate) async fn call_user_defined_function(
|
|||||||
result.map(|_| fn_memory.return_)
|
result.map(|_| fn_memory.return_)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn call_user_defined_function_kw(
|
||||||
|
args: crate::std::args::KwArgs,
|
||||||
|
memory: &ProgramMemory,
|
||||||
|
function_expression: NodeRef<'_, FunctionExpression>,
|
||||||
|
exec_state: &mut ExecState,
|
||||||
|
ctx: &ExecutorContext,
|
||||||
|
) -> Result<Option<KclValue>, KclError> {
|
||||||
|
// Create a new environment to execute the function body in so that local
|
||||||
|
// variables shadow variables in the parent scope. The new environment's
|
||||||
|
// parent should be the environment of the closure.
|
||||||
|
let mut body_memory = memory.clone();
|
||||||
|
let body_env = body_memory.new_env_for_call(memory.current_env);
|
||||||
|
body_memory.current_env = body_env;
|
||||||
|
let fn_memory = assign_args_to_params_kw(function_expression, args, body_memory)?;
|
||||||
|
|
||||||
|
// Execute the function body using the memory we just created.
|
||||||
|
let (result, fn_memory) = {
|
||||||
|
let previous_memory = std::mem::replace(&mut exec_state.memory, fn_memory);
|
||||||
|
let result = ctx
|
||||||
|
.inner_execute(&function_expression.body, exec_state, BodyType::Block)
|
||||||
|
.await;
|
||||||
|
// Restore the previous memory.
|
||||||
|
let fn_memory = std::mem::replace(&mut exec_state.memory, previous_memory);
|
||||||
|
|
||||||
|
(result, fn_memory)
|
||||||
|
};
|
||||||
|
|
||||||
|
result.map(|_| fn_memory.return_)
|
||||||
|
}
|
||||||
|
|
||||||
pub enum StatementKind<'a> {
|
pub enum StatementKind<'a> {
|
||||||
Declaration { name: &'a str },
|
Declaration { name: &'a str },
|
||||||
Expression,
|
Expression,
|
||||||
@ -2289,9 +2419,12 @@ mod tests {
|
|||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::parsing::ast::types::{DefaultParamVal, Identifier, Node, Parameter};
|
use crate::{
|
||||||
|
parsing::ast::types::{DefaultParamVal, Identifier, Node, Parameter},
|
||||||
|
OldAstState,
|
||||||
|
};
|
||||||
|
|
||||||
pub async fn parse_execute(code: &str) -> Result<ProgramMemory> {
|
pub async fn parse_execute(code: &str) -> Result<(Program, ExecutorContext, ExecState)> {
|
||||||
let program = Program::parse_no_errs(code)?;
|
let program = Program::parse_no_errs(code)?;
|
||||||
|
|
||||||
let ctx = ExecutorContext {
|
let ctx = ExecutorContext {
|
||||||
@ -2302,9 +2435,9 @@ mod tests {
|
|||||||
context_type: ContextType::Mock,
|
context_type: ContextType::Mock,
|
||||||
};
|
};
|
||||||
let mut exec_state = ExecState::default();
|
let mut exec_state = ExecState::default();
|
||||||
ctx.run(program.into(), &mut exec_state).await?;
|
ctx.run(program.clone().into(), &mut exec_state).await?;
|
||||||
|
|
||||||
Ok(exec_state.memory)
|
Ok((program, ctx, exec_state))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience function to get a JSON value from memory and unwrap.
|
/// Convenience function to get a JSON value from memory and unwrap.
|
||||||
@ -2715,36 +2848,39 @@ let shape = layer() |> patternTransform(10, transform, %)
|
|||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_math_execute_with_functions() {
|
async fn test_math_execute_with_functions() {
|
||||||
let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#;
|
let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#;
|
||||||
let memory = parse_execute(ast).await.unwrap();
|
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||||
assert_eq!(5.0, mem_get_json(&memory, "myVar").as_f64().unwrap());
|
assert_eq!(5.0, mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_math_execute() {
|
async fn test_math_execute() {
|
||||||
let ast = r#"const myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
|
let ast = r#"const myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
|
||||||
let memory = parse_execute(ast).await.unwrap();
|
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||||
assert_eq!(7.4, mem_get_json(&memory, "myVar").as_f64().unwrap());
|
assert_eq!(7.4, mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_math_execute_start_negative() {
|
async fn test_math_execute_start_negative() {
|
||||||
let ast = r#"const myVar = -5 + 6"#;
|
let ast = r#"const myVar = -5 + 6"#;
|
||||||
let memory = parse_execute(ast).await.unwrap();
|
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||||
assert_eq!(1.0, mem_get_json(&memory, "myVar").as_f64().unwrap());
|
assert_eq!(1.0, mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_math_execute_with_pi() {
|
async fn test_math_execute_with_pi() {
|
||||||
let ast = r#"const myVar = pi() * 2"#;
|
let ast = r#"const myVar = pi() * 2"#;
|
||||||
let memory = parse_execute(ast).await.unwrap();
|
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||||
assert_eq!(std::f64::consts::TAU, mem_get_json(&memory, "myVar").as_f64().unwrap());
|
assert_eq!(
|
||||||
|
std::f64::consts::TAU,
|
||||||
|
mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_math_define_decimal_without_leading_zero() {
|
async fn test_math_define_decimal_without_leading_zero() {
|
||||||
let ast = r#"let thing = .4 + 7"#;
|
let ast = r#"let thing = .4 + 7"#;
|
||||||
let memory = parse_execute(ast).await.unwrap();
|
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||||
assert_eq!(7.4, mem_get_json(&memory, "thing").as_f64().unwrap());
|
assert_eq!(7.4, mem_get_json(&exec_state.memory, "thing").as_f64().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
@ -2783,11 +2919,11 @@ fn check = (x) => {
|
|||||||
}
|
}
|
||||||
check(false)
|
check(false)
|
||||||
"#;
|
"#;
|
||||||
let mem = parse_execute(ast).await.unwrap();
|
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||||
assert_eq!(false, mem_get_json(&mem, "notTrue").as_bool().unwrap());
|
assert_eq!(false, mem_get_json(&exec_state.memory, "notTrue").as_bool().unwrap());
|
||||||
assert_eq!(true, mem_get_json(&mem, "notFalse").as_bool().unwrap());
|
assert_eq!(true, mem_get_json(&exec_state.memory, "notFalse").as_bool().unwrap());
|
||||||
assert_eq!(true, mem_get_json(&mem, "c").as_bool().unwrap());
|
assert_eq!(true, mem_get_json(&exec_state.memory, "c").as_bool().unwrap());
|
||||||
assert_eq!(false, mem_get_json(&mem, "d").as_bool().unwrap());
|
assert_eq!(false, mem_get_json(&exec_state.memory, "d").as_bool().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
@ -2888,8 +3024,10 @@ let notTagDeclarator = !myTagDeclarator";
|
|||||||
);
|
);
|
||||||
|
|
||||||
let code9 = "
|
let code9 = "
|
||||||
let myTagDeclarator = $myTag
|
sk = startSketchOn('XY')
|
||||||
let notTagIdentifier = !myTag";
|
|> startProfileAt([0, 0], %)
|
||||||
|
|> line([5, 0], %, $myTag)
|
||||||
|
notTagIdentifier = !myTag";
|
||||||
let tag_identifier_err = parse_execute(code9).await.unwrap_err().downcast::<KclError>().unwrap();
|
let tag_identifier_err = parse_execute(code9).await.unwrap_err().downcast::<KclError>().unwrap();
|
||||||
// These are currently printed out as JSON objects, so we don't want to
|
// These are currently printed out as JSON objects, so we don't want to
|
||||||
// check the full error.
|
// check the full error.
|
||||||
@ -3167,4 +3305,305 @@ let w = f() + f()
|
|||||||
let json = serde_json::to_string(&mem).unwrap();
|
let json = serde_json::to_string(&mem).unwrap();
|
||||||
assert_eq!(json, r#"{"type":"Solids","value":[]}"#);
|
assert_eq!(json, r#"{"type":"Solids","value":[]}"#);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Easy case where we have no old ast and memory.
|
||||||
|
// We need to re-execute everything.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn test_get_changed_program_no_old_information() {
|
||||||
|
let new = r#"// Remove the end face for the extrusion.
|
||||||
|
firstSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-12, 12], %)
|
||||||
|
|> line([24, 0], %)
|
||||||
|
|> line([0, -24], %)
|
||||||
|
|> line([-24, 0], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(6, %)
|
||||||
|
|
||||||
|
// Remove the end face for the extrusion.
|
||||||
|
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||||
|
let (program, ctx, _) = parse_execute(new).await.unwrap();
|
||||||
|
|
||||||
|
let result = ctx
|
||||||
|
.get_changed_program(CacheInformation {
|
||||||
|
old: None,
|
||||||
|
new_ast: program.ast.clone(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_some());
|
||||||
|
|
||||||
|
let result = result.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.program, program.ast);
|
||||||
|
assert!(result.clear_scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn test_get_changed_program_same_code() {
|
||||||
|
let new = r#"// Remove the end face for the extrusion.
|
||||||
|
firstSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-12, 12], %)
|
||||||
|
|> line([24, 0], %)
|
||||||
|
|> line([0, -24], %)
|
||||||
|
|> line([-24, 0], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(6, %)
|
||||||
|
|
||||||
|
// Remove the end face for the extrusion.
|
||||||
|
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||||
|
|
||||||
|
let (program, ctx, exec_state) = parse_execute(new).await.unwrap();
|
||||||
|
|
||||||
|
let result = ctx
|
||||||
|
.get_changed_program(CacheInformation {
|
||||||
|
old: Some(OldAstState {
|
||||||
|
ast: program.ast.clone(),
|
||||||
|
exec_state,
|
||||||
|
settings: Default::default(),
|
||||||
|
}),
|
||||||
|
new_ast: program.ast.clone(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(result, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn test_get_changed_program_same_code_changed_whitespace() {
|
||||||
|
let old = r#" // Remove the end face for the extrusion.
|
||||||
|
firstSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-12, 12], %)
|
||||||
|
|> line([24, 0], %)
|
||||||
|
|> line([0, -24], %)
|
||||||
|
|> line([-24, 0], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(6, %)
|
||||||
|
|
||||||
|
// Remove the end face for the extrusion.
|
||||||
|
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
|
||||||
|
|
||||||
|
let new = r#"// Remove the end face for the extrusion.
|
||||||
|
firstSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-12, 12], %)
|
||||||
|
|> line([24, 0], %)
|
||||||
|
|> line([0, -24], %)
|
||||||
|
|> line([-24, 0], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(6, %)
|
||||||
|
|
||||||
|
// Remove the end face for the extrusion.
|
||||||
|
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||||
|
|
||||||
|
let (program_old, ctx, exec_state) = parse_execute(old).await.unwrap();
|
||||||
|
|
||||||
|
let program_new = crate::Program::parse_no_errs(new).unwrap();
|
||||||
|
|
||||||
|
let result = ctx
|
||||||
|
.get_changed_program(CacheInformation {
|
||||||
|
old: Some(OldAstState {
|
||||||
|
ast: program_old.ast.clone(),
|
||||||
|
exec_state,
|
||||||
|
settings: Default::default(),
|
||||||
|
}),
|
||||||
|
new_ast: program_new.ast.clone(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(result, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
|
||||||
|
let old = r#" // Removed the end face for the extrusion.
|
||||||
|
firstSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-12, 12], %)
|
||||||
|
|> line([24, 0], %)
|
||||||
|
|> line([0, -24], %)
|
||||||
|
|> line([-24, 0], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(6, %)
|
||||||
|
|
||||||
|
// Remove the end face for the extrusion.
|
||||||
|
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
|
||||||
|
|
||||||
|
let new = r#"// Remove the end face for the extrusion.
|
||||||
|
firstSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-12, 12], %)
|
||||||
|
|> line([24, 0], %)
|
||||||
|
|> line([0, -24], %)
|
||||||
|
|> line([-24, 0], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(6, %)
|
||||||
|
|
||||||
|
// Remove the end face for the extrusion.
|
||||||
|
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||||
|
|
||||||
|
let (program, ctx, exec_state) = parse_execute(old).await.unwrap();
|
||||||
|
|
||||||
|
let program_new = crate::Program::parse_no_errs(new).unwrap();
|
||||||
|
|
||||||
|
let result = ctx
|
||||||
|
.get_changed_program(CacheInformation {
|
||||||
|
old: Some(OldAstState {
|
||||||
|
ast: program.ast.clone(),
|
||||||
|
exec_state,
|
||||||
|
settings: Default::default(),
|
||||||
|
}),
|
||||||
|
new_ast: program_new.ast.clone(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(result, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn test_get_changed_program_same_code_changed_code_comments() {
|
||||||
|
let old = r#" // Removed the end face for the extrusion.
|
||||||
|
firstSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-12, 12], %)
|
||||||
|
|> line([24, 0], %)
|
||||||
|
|> line([0, -24], %)
|
||||||
|
|> line([-24, 0], %) // my thing
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(6, %)
|
||||||
|
|
||||||
|
// Remove the end face for the extrusion.
|
||||||
|
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
|
||||||
|
|
||||||
|
let new = r#"// Remove the end face for the extrusion.
|
||||||
|
firstSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-12, 12], %)
|
||||||
|
|> line([24, 0], %)
|
||||||
|
|> line([0, -24], %)
|
||||||
|
|> line([-24, 0], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(6, %)
|
||||||
|
|
||||||
|
// Remove the end face for the extrusion.
|
||||||
|
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||||
|
|
||||||
|
let (program, ctx, exec_state) = parse_execute(old).await.unwrap();
|
||||||
|
|
||||||
|
let program_new = crate::Program::parse_no_errs(new).unwrap();
|
||||||
|
|
||||||
|
let result = ctx
|
||||||
|
.get_changed_program(CacheInformation {
|
||||||
|
old: Some(OldAstState {
|
||||||
|
ast: program.ast.clone(),
|
||||||
|
exec_state,
|
||||||
|
settings: Default::default(),
|
||||||
|
}),
|
||||||
|
new_ast: program_new.ast.clone(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changing the units with the exact same file should bust the cache.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn test_get_changed_program_same_code_but_different_units() {
|
||||||
|
let new = r#"// Remove the end face for the extrusion.
|
||||||
|
firstSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-12, 12], %)
|
||||||
|
|> line([24, 0], %)
|
||||||
|
|> line([0, -24], %)
|
||||||
|
|> line([-24, 0], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(6, %)
|
||||||
|
|
||||||
|
// Remove the end face for the extrusion.
|
||||||
|
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||||
|
|
||||||
|
let (program, mut ctx, exec_state) = parse_execute(new).await.unwrap();
|
||||||
|
|
||||||
|
// Change the settings to cm.
|
||||||
|
ctx.settings.units = crate::UnitLength::Cm;
|
||||||
|
|
||||||
|
let result = ctx
|
||||||
|
.get_changed_program(CacheInformation {
|
||||||
|
old: Some(OldAstState {
|
||||||
|
ast: program.ast.clone(),
|
||||||
|
exec_state,
|
||||||
|
settings: Default::default(),
|
||||||
|
}),
|
||||||
|
new_ast: program.ast.clone(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_some());
|
||||||
|
|
||||||
|
let result = result.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.program, program.ast);
|
||||||
|
assert!(result.clear_scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changing the grid settings with the exact same file should NOT bust the cache.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn test_get_changed_program_same_code_but_different_grid_setting() {
|
||||||
|
let new = r#"// Remove the end face for the extrusion.
|
||||||
|
firstSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-12, 12], %)
|
||||||
|
|> line([24, 0], %)
|
||||||
|
|> line([0, -24], %)
|
||||||
|
|> line([-24, 0], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(6, %)
|
||||||
|
|
||||||
|
// Remove the end face for the extrusion.
|
||||||
|
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||||
|
|
||||||
|
let (program, mut ctx, exec_state) = parse_execute(new).await.unwrap();
|
||||||
|
|
||||||
|
// Change the settings.
|
||||||
|
ctx.settings.show_grid = !ctx.settings.show_grid;
|
||||||
|
|
||||||
|
let result = ctx
|
||||||
|
.get_changed_program(CacheInformation {
|
||||||
|
old: Some(OldAstState {
|
||||||
|
ast: program.ast.clone(),
|
||||||
|
exec_state,
|
||||||
|
settings: Default::default(),
|
||||||
|
}),
|
||||||
|
new_ast: program.ast.clone(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(result, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changing the edge visibility settings with the exact same file should NOT bust the cache.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn test_get_changed_program_same_code_but_different_edge_visiblity_setting() {
|
||||||
|
let new = r#"// Remove the end face for the extrusion.
|
||||||
|
firstSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-12, 12], %)
|
||||||
|
|> line([24, 0], %)
|
||||||
|
|> line([0, -24], %)
|
||||||
|
|> line([-24, 0], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(6, %)
|
||||||
|
|
||||||
|
// Remove the end face for the extrusion.
|
||||||
|
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||||
|
|
||||||
|
let (program, mut ctx, exec_state) = parse_execute(new).await.unwrap();
|
||||||
|
|
||||||
|
// Change the settings.
|
||||||
|
ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
|
||||||
|
|
||||||
|
let result = ctx
|
||||||
|
.get_changed_program(CacheInformation {
|
||||||
|
old: Some(OldAstState {
|
||||||
|
ast: program.ast.clone(),
|
||||||
|
exec_state,
|
||||||
|
settings: Default::default(),
|
||||||
|
}),
|
||||||
|
new_ast: program.ast.clone(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(result, None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|