Compare commits
108 Commits
kcl-test-s
...
stream-pau
Author | SHA1 | Date | |
---|---|---|---|
c6b80eec68 | |||
1b68f5dc19 | |||
a0aa4802d1 | |||
746f76ec63 | |||
8e624935c2 | |||
0a5f22c80a | |||
142db64796 | |||
59b0cdc3ac | |||
4763257dc3 | |||
b48ba7f081 | |||
64537a59b8 | |||
c24efaf2e4 | |||
2fb16ed074 | |||
82e647db3b | |||
fa2d0a69bf | |||
e372b2680e | |||
c1c1f817c9 | |||
00c0c993f2 | |||
b505c0be07 | |||
6c2d06c2c6 | |||
a4b7dd5182 | |||
0c2ca726d0 | |||
fcfecf702b | |||
347a6ef15a | |||
eed4386f76 | |||
14afcba599 | |||
faee6cbc64 | |||
0673e98fad | |||
b4eea5f842 | |||
2c9eb7f7c0 | |||
e259b2e3e8 | |||
91049204c5 | |||
0128c67aae | |||
ecc42b1e9c | |||
31811d0269 | |||
def5959836 | |||
4c7fab405b | |||
1e12e8d36b | |||
ad775891a3 | |||
efe207f4d2 | |||
01f0162991 | |||
2bbf7fad67 | |||
98549945a4 | |||
315fdc3060 | |||
c7e77e2597 | |||
a48679c014 | |||
c5e74866a9 | |||
731cb6c532 | |||
8a36a4c205 | |||
f29f2557de | |||
5f0ffb56c4 | |||
70078176b0 | |||
098fa2b5c9 | |||
2755156b84 | |||
a8b3ec660d | |||
3747c6ff0e | |||
bc1bc817ba | |||
b415e88746 | |||
9173e368a2 | |||
25928813e3 | |||
aec9cac7c7 | |||
0e82fbf7b0 | |||
f5975bbd61 | |||
765e587f6b | |||
6ccd5e22b8 | |||
c8bf82ba04 | |||
daad2039ec | |||
b567f6dfad | |||
41e85c77ac | |||
9f615b9d3e | |||
e8b5618b34 | |||
799b2d77b4 | |||
7b569f9b4f | |||
0f0c396a0c | |||
83214a88a3 | |||
dbab7876de | |||
6706695502 | |||
fa1f8d8d02 | |||
b4e59b5c56 | |||
20495383ac | |||
7f5fb83761 | |||
2ac874971e | |||
230e3132e9 | |||
bf9bb4fb22 | |||
31e7634669 | |||
84c71aa046 | |||
721b3e8cbd | |||
89309b6ccd | |||
15b163bba8 | |||
60d047ef6a | |||
f105044a47 | |||
9388e09c47 | |||
d71f2af9bd | |||
2bb372de12 | |||
0a3a8afbbd | |||
351df2f306 | |||
05a2eada9a | |||
788270d4fc | |||
6845f0c4bc | |||
563096fba4 | |||
35133c4f45 | |||
b78c6508c2 | |||
08b776134f | |||
cca544189c | |||
69754c82a2 | |||
afbee552ee | |||
b11772b27c | |||
6dc87aa4fe |
4
.github/workflows/cargo-test.yml
vendored
@ -5,6 +5,8 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- 'src/wasm-lib/**.rs'
|
- 'src/wasm-lib/**.rs'
|
||||||
- 'src/wasm-lib/**.hbs'
|
- 'src/wasm-lib/**.hbs'
|
||||||
|
- 'src/wasm-lib/**.gen'
|
||||||
|
- 'src/wasm-lib/**.snap'
|
||||||
- '**/Cargo.toml'
|
- '**/Cargo.toml'
|
||||||
- '**/Cargo.lock'
|
- '**/Cargo.lock'
|
||||||
- '**/rust-toolchain.toml'
|
- '**/rust-toolchain.toml'
|
||||||
@ -15,6 +17,8 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- 'src/wasm-lib/**.rs'
|
- 'src/wasm-lib/**.rs'
|
||||||
- 'src/wasm-lib/**.hbs'
|
- 'src/wasm-lib/**.hbs'
|
||||||
|
- 'src/wasm-lib/**.gen'
|
||||||
|
- 'src/wasm-lib/**.snap'
|
||||||
- '**/Cargo.toml'
|
- '**/Cargo.toml'
|
||||||
- '**/Cargo.lock'
|
- '**/Cargo.lock'
|
||||||
- '**/rust-toolchain.toml'
|
- '**/rust-toolchain.toml'
|
||||||
|
2
Makefile
@ -19,7 +19,7 @@ $(XSTATE_TYPEGENS): $(TS_SRC)
|
|||||||
yarn xstate typegen 'src/**/*.ts?(x)'
|
yarn xstate typegen 'src/**/*.ts?(x)'
|
||||||
|
|
||||||
public/wasm_lib_bg.wasm: $(WASM_LIB_FILES)
|
public/wasm_lib_bg.wasm: $(WASM_LIB_FILES)
|
||||||
yarn build:wasm-dev
|
yarn build:wasm
|
||||||
|
|
||||||
node_modules: package.json yarn.lock
|
node_modules: package.json yarn.lock
|
||||||
yarn install
|
yarn install
|
||||||
|
@ -110,7 +110,7 @@ Which commands from setup are one off vs need to be run every time?
|
|||||||
The following will need to be run when checking out a new commit and guarantees the build is not stale:
|
The following will need to be run when checking out a new commit and guarantees the build is not stale:
|
||||||
```bash
|
```bash
|
||||||
yarn install
|
yarn install
|
||||||
yarn build:wasm-dev # or yarn build:wasm for slower but more production-like build
|
yarn build:wasm
|
||||||
yarn start # or yarn build:local && yarn serve for slower but more production-like build
|
yarn start # or yarn build:local && yarn serve for slower but more production-like build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: "angleToMatchLengthX"
|
title: "angleToMatchLengthX"
|
||||||
excerpt: "Compute the angle (in degrees) in o"
|
excerpt: "Returns the angle to match the given length for x."
|
||||||
layout: manual
|
layout: manual
|
||||||
---
|
---
|
||||||
|
|
||||||
Compute the angle (in degrees) in o
|
Returns the angle to match the given length for x.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
59
docs/kcl/modules.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
title: "KCL Modules"
|
||||||
|
excerpt: "Documentation of modules for the KCL language for the Zoo Modeling App."
|
||||||
|
layout: manual
|
||||||
|
---
|
||||||
|
|
||||||
|
`KCL` allows splitting code up into multiple files. Each file is somewhat
|
||||||
|
isolated from other files as a separate module.
|
||||||
|
|
||||||
|
When you define a function, you can use `export` before it to make it available
|
||||||
|
to other modules.
|
||||||
|
|
||||||
|
```
|
||||||
|
// util.kcl
|
||||||
|
export fn increment = (x) => {
|
||||||
|
return x + 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Other files in the project can now import functions that have been exported.
|
||||||
|
This makes them available to use in another file.
|
||||||
|
|
||||||
|
```
|
||||||
|
// main.kcl
|
||||||
|
import increment from "util.kcl"
|
||||||
|
|
||||||
|
answer = increment(41)
|
||||||
|
```
|
||||||
|
|
||||||
|
Imported files _must_ be in the same project so that units are uniform across
|
||||||
|
modules. This means that it must be in the same directory.
|
||||||
|
|
||||||
|
Import statements must be at the top-level of a file. It is not allowed to have
|
||||||
|
an `import` statement inside a function or in the body of an if-else.
|
||||||
|
|
||||||
|
Multiple functions can be exported in a file.
|
||||||
|
|
||||||
|
```
|
||||||
|
// util.kcl
|
||||||
|
export fn increment = (x) => {
|
||||||
|
return x + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn decrement = (x) => {
|
||||||
|
return x - 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When importing, you can import multiple functions at once.
|
||||||
|
|
||||||
|
```
|
||||||
|
import increment, decrement from "util.kcl"
|
||||||
|
```
|
||||||
|
|
||||||
|
Imported symbols can be renamed for convenience or to avoid name collisions.
|
||||||
|
|
||||||
|
```
|
||||||
|
import increment as inc, decrement as dec from "util.kcl"
|
||||||
|
```
|
4815
docs/kcl/std.json
16
docs/kcl/types/KclNone.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "KclNone"
|
||||||
|
excerpt: "KCL value for an optional parameter which was not given an argument. (remember, parameters are in the function declaration, arguments are in the function call/application)."
|
||||||
|
layout: manual
|
||||||
|
---
|
||||||
|
|
||||||
|
KCL value for an optional parameter which was not given an argument. (remember, parameters are in the function declaration, arguments are in the function call/application).
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -23,8 +23,110 @@ Any KCL value.
|
|||||||
|
|
||||||
| Property | Type | Description | Required |
|
| Property | Type | Description | Required |
|
||||||
|----------|------|-------------|----------|
|
|----------|------|-------------|----------|
|
||||||
| `type` |enum: `UserVal`| | No |
|
| `type` |enum: `Uuid`| | No |
|
||||||
| `value` |``| | No |
|
| `value` |`string`| | No |
|
||||||
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Type | Description | Required |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| `type` |enum: `Bool`| | No |
|
||||||
|
| `value` |`boolean`| | No |
|
||||||
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Type | Description | Required |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| `type` |enum: `Number`| | No |
|
||||||
|
| `value` |`number`| | No |
|
||||||
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Type | Description | Required |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| `type` |enum: `Int`| | No |
|
||||||
|
| `value` |`integer`| | No |
|
||||||
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Type | Description | Required |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| `type` |enum: `String`| | No |
|
||||||
|
| `value` |`string`| | No |
|
||||||
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Type | Description | Required |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| `type` |enum: `Array`| | No |
|
||||||
|
| `value` |`[` [`KclValue`](/docs/kcl/types/KclValue) `]`| | No |
|
||||||
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Type | Description | Required |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| `type` |enum: `Object`| | No |
|
||||||
|
| `value` |`object`| | No |
|
||||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
@ -111,6 +213,38 @@ A face.
|
|||||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Type | Description | Required |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| `type` |enum: [`Sketch`](/docs/kcl/types/Sketch)| | No |
|
||||||
|
| `value` |[`Sketch`](/docs/kcl/types/Sketch)| Any KCL value. | No |
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Type | Description | Required |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| `type` |enum: `Sketches`| | No |
|
||||||
|
| `value` |`[` [`Sketch`](/docs/kcl/types/Sketch) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
----
|
----
|
||||||
An solid is a collection of extrude surfaces.
|
An solid is a collection of extrude surfaces.
|
||||||
|
|
||||||
@ -190,6 +324,23 @@ Data for an imported geometry.
|
|||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Type | Description | Required |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| `type` |enum: [`KclNone`](/docs/kcl/types/KclNone)| | No |
|
||||||
|
| `value` |[`KclNone`](/docs/kcl/types/KclNone)| Any KCL value. | No |
|
||||||
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,15 +67,15 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
|
|||||||
if (openPanes.includes('code')) {
|
if (openPanes.includes('code')) {
|
||||||
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)`)
|
|> xLine(${commonPoints.num1}, %)`)
|
||||||
}
|
}
|
||||||
await page.waitForTimeout(500)
|
await page.waitForTimeout(500)
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||||
if (openPanes.includes('code')) {
|
if (openPanes.includes('code')) {
|
||||||
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)
|
|> xLine(${commonPoints.num1}, %)
|
||||||
|> line([0, ${commonPoints.num1 + 0.01}], %)`)
|
|> yLine(${commonPoints.num1 + 0.01}, %)`)
|
||||||
} else {
|
} else {
|
||||||
await page.waitForTimeout(500)
|
await page.waitForTimeout(500)
|
||||||
}
|
}
|
||||||
@ -84,9 +84,9 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
|
|||||||
if (openPanes.includes('code')) {
|
if (openPanes.includes('code')) {
|
||||||
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)
|
|> xLine(${commonPoints.num1}, %)
|
||||||
|> line([0, ${commonPoints.num1 + 0.01}], %)
|
|> yLine(${commonPoints.num1 + 0.01}, %)
|
||||||
|> lineTo([0, ${commonPoints.num3}], %)`)
|
|> xLine(${commonPoints.num2 * -1}, %)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// deselect line tool
|
// deselect line tool
|
||||||
@ -142,9 +142,9 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
|
|||||||
await u.openKclCodePanel()
|
await u.openKclCodePanel()
|
||||||
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %, $seg01)
|
|> xLine(${commonPoints.num1}, %, $seg01)
|
||||||
|> line([0, ${commonPoints.num1 + 0.01}], %)
|
|> yLine(${commonPoints.num1 + 0.01}, %)
|
||||||
|> angledLine([180, segLen(seg01)], %)`)
|
|> xLine(-segLen(seg01), %)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
test.describe('Basic sketch', () => {
|
test.describe('Basic sketch', () => {
|
||||||
|
@ -694,6 +694,9 @@ test.describe('Editor tests', () => {
|
|||||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
.toHaveText(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt([3.14, 12], %)
|
|> startProfileAt([3.14, 12], %)
|
||||||
|> xLine(5, %) // lin`)
|
|> xLine(5, %) // lin`)
|
||||||
|
|
||||||
|
// expect there to be no KCL errors
|
||||||
|
await expect(page.locator('.cm-lint-marker-error')).toHaveCount(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('with tab to accept the completion', async ({ page }) => {
|
test('with tab to accept the completion', async ({ page }) => {
|
||||||
|
@ -452,7 +452,7 @@ sketch002 = startSketchOn(extrude001, seg03)
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test(`Verify axis and origin snapping`, async ({
|
test(`Verify axis, origin, and horizontal snapping`, async ({
|
||||||
app,
|
app,
|
||||||
editor,
|
editor,
|
||||||
toolbar,
|
toolbar,
|
||||||
@ -505,7 +505,7 @@ test(`Verify axis and origin snapping`, async ({
|
|||||||
const expectedCodeSnippets = {
|
const expectedCodeSnippets = {
|
||||||
sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`,
|
sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`,
|
||||||
pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], %)`,
|
pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], %)`,
|
||||||
segmentOnXAxis: `lineTo([${xAxisSloppy.kcl[0]}, ${xAxisSloppy.kcl[1]}], %)`,
|
segmentOnXAxis: `xLine(${xAxisSloppy.kcl[0]}, %)`,
|
||||||
afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], %)`,
|
afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], %)`,
|
||||||
afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`,
|
afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`,
|
||||||
}
|
}
|
||||||
|
@ -115,7 +115,7 @@ test.describe('Sketch tests', () => {
|
|||||||
'persistCode',
|
'persistCode',
|
||||||
`sketch001 = startSketchOn('XZ')
|
`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt([4.61, -14.01], %)
|
|> startProfileAt([4.61, -14.01], %)
|
||||||
|> line([12.73, -0.09], %)
|
|> xLine(12.73, %)
|
||||||
|> tangentialArcTo([24.95, -5.38], %)`
|
|> tangentialArcTo([24.95, -5.38], %)`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -156,7 +156,7 @@ test.describe('Sketch tests', () => {
|
|||||||
await expect.poll(u.normalisedEditorCode, { timeout: 1000 })
|
await expect.poll(u.normalisedEditorCode, { timeout: 1000 })
|
||||||
.toBe(`sketch001 = startSketchOn('XZ')
|
.toBe(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt([12.34, -12.34], %)
|
|> startProfileAt([12.34, -12.34], %)
|
||||||
|> line([-12.34, 12.34], %)
|
|> yLine(12.34, %)
|
||||||
|
|
||||||
`)
|
`)
|
||||||
}).toPass({ timeout: 40_000, intervals: [1_000] })
|
}).toPass({ timeout: 40_000, intervals: [1_000] })
|
||||||
@ -202,35 +202,19 @@ test.describe('Sketch tests', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
const viewport = { width: 1200, height: 500 }
|
||||||
|
await page.setViewportSize(viewport)
|
||||||
|
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
).not.toBeDisabled()
|
).not.toBeDisabled()
|
||||||
|
|
||||||
await page.waitForTimeout(100)
|
const center = {
|
||||||
await u.openAndClearDebugPanel()
|
x: viewport.width / 2,
|
||||||
await u.sendCustomCmd({
|
y: viewport.height / 2,
|
||||||
type: 'modeling_cmd_req',
|
}
|
||||||
cmd_id: uuidv4(),
|
const modelAreaSize = await u.getModelViewAreaSize()
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_look_at',
|
|
||||||
vantage: { x: 0, y: -1250, z: 580 },
|
|
||||||
center: { x: 0, y: 0, z: 0 },
|
|
||||||
up: { x: 0, y: 0, z: 1 },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await u.sendCustomCmd({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_get_settings',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await u.closeDebugPanel()
|
|
||||||
|
|
||||||
// If we have the code pane open, we should see the code.
|
// If we have the code pane open, we should see the code.
|
||||||
if (openPanes.includes('code')) {
|
if (openPanes.includes('code')) {
|
||||||
@ -244,7 +228,7 @@ test.describe('Sketch tests', () => {
|
|||||||
await expect(u.codeLocator).not.toBeVisible()
|
await expect(u.codeLocator).not.toBeVisible()
|
||||||
}
|
}
|
||||||
|
|
||||||
const startPX = [665, 458]
|
const startPX = [center.x + 65, 458]
|
||||||
|
|
||||||
const dragPX = 30
|
const dragPX = 30
|
||||||
let prevContent = ''
|
let prevContent = ''
|
||||||
@ -255,7 +239,7 @@ test.describe('Sketch tests', () => {
|
|||||||
// Wait for the render.
|
// Wait for the render.
|
||||||
await page.waitForTimeout(1000)
|
await page.waitForTimeout(1000)
|
||||||
// Select the sketch
|
// Select the sketch
|
||||||
await page.mouse.click(700, 370)
|
await page.mouse.click(center.x + 100, 370)
|
||||||
}
|
}
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Edit Sketch' })
|
page.getByRole('button', { name: 'Edit Sketch' })
|
||||||
@ -266,45 +250,74 @@ test.describe('Sketch tests', () => {
|
|||||||
prevContent = await page.locator('.cm-content').innerText()
|
prevContent = await page.locator('.cm-content').innerText()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await page.waitForTimeout(1000)
|
||||||
|
await u.openAndClearDebugPanel()
|
||||||
|
await u.sendCustomCmd({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_look_at',
|
||||||
|
vantage: { x: 0, y: -1250, z: 580 - modelAreaSize.w },
|
||||||
|
center: { x: 0, y: 0, z: 0 },
|
||||||
|
up: { x: 0, y: 0, z: 1 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await u.sendCustomCmd({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_get_settings',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await page.waitForTimeout(1000)
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
const step5 = { steps: 5 }
|
const step5 = { steps: 5 }
|
||||||
|
|
||||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
|
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
|
||||||
|
|
||||||
// drag startProfieAt handle
|
test.step('drag startProfileAt handle', async () => {
|
||||||
await page.mouse.move(startPX[0], startPX[1])
|
await page.mouse.move(startPX[0], startPX[1])
|
||||||
await page.mouse.down()
|
await page.mouse.down()
|
||||||
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
|
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
|
||||||
await page.mouse.up()
|
await page.mouse.up()
|
||||||
|
if (openPanes.includes('code')) {
|
||||||
|
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||||
|
prevContent = await page.locator('.cm-content').innerText()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if (openPanes.includes('code')) {
|
|
||||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
|
||||||
prevContent = await page.locator('.cm-content').innerText()
|
|
||||||
}
|
|
||||||
|
|
||||||
// drag line handle
|
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
|
test.step('drag line handle', async () => {
|
||||||
await page.mouse.move(lineEnd.x - 5, lineEnd.y)
|
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
|
||||||
await page.mouse.down()
|
await page.mouse.move(lineEnd.x - 5, lineEnd.y)
|
||||||
await page.mouse.move(lineEnd.x + dragPX, lineEnd.y - dragPX, step5)
|
await page.mouse.down()
|
||||||
await page.mouse.up()
|
await page.mouse.move(lineEnd.x + dragPX, lineEnd.y - dragPX, step5)
|
||||||
await page.waitForTimeout(100)
|
await page.mouse.up()
|
||||||
if (openPanes.includes('code')) {
|
await page.waitForTimeout(100)
|
||||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
if (openPanes.includes('code')) {
|
||||||
prevContent = await page.locator('.cm-content').innerText()
|
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||||
}
|
prevContent = await page.locator('.cm-content').innerText()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// drag tangentialArcTo handle
|
test.step('drag tangentialArcTo handle', async () => {
|
||||||
const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]')
|
const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]')
|
||||||
await page.mouse.move(tangentEnd.x, tangentEnd.y - 5)
|
await page.mouse.move(tangentEnd.x, tangentEnd.y - 5)
|
||||||
await page.mouse.down()
|
await page.mouse.down()
|
||||||
await page.mouse.move(tangentEnd.x + dragPX, tangentEnd.y - dragPX, step5)
|
await page.mouse.move(
|
||||||
await page.mouse.up()
|
tangentEnd.x + dragPX,
|
||||||
await page.waitForTimeout(100)
|
tangentEnd.y - dragPX,
|
||||||
if (openPanes.includes('code')) {
|
step5
|
||||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
)
|
||||||
}
|
await page.mouse.up()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
if (openPanes.includes('code')) {
|
||||||
|
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Open the code pane
|
// Open the code pane
|
||||||
await u.openKclCodePanel()
|
await u.openKclCodePanel()
|
||||||
@ -580,7 +593,7 @@ test.describe('Sketch tests', () => {
|
|||||||
})
|
})
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
const startPX = [665, 458]
|
const center = await u.getCenterOfModelViewArea()
|
||||||
|
|
||||||
const dragPX = 30
|
const dragPX = 30
|
||||||
|
|
||||||
@ -596,7 +609,7 @@ test.describe('Sketch tests', () => {
|
|||||||
|
|
||||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
|
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
|
||||||
|
|
||||||
// drag startProfieAt handle
|
// drag startProfileAt handle
|
||||||
await page.mouse.move(startPX[0], startPX[1])
|
await page.mouse.move(startPX[0], startPX[1])
|
||||||
await page.mouse.down()
|
await page.mouse.down()
|
||||||
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
|
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
|
||||||
@ -638,6 +651,7 @@ test.describe('Sketch tests', () => {
|
|||||||
})
|
})
|
||||||
test('Can add multiple sketches', async ({ page }) => {
|
test('Can add multiple sketches', async ({ page }) => {
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
|
|
||||||
const viewportSize = { width: 1200, height: 500 }
|
const viewportSize = { width: 1200, height: 500 }
|
||||||
await page.setViewportSize(viewportSize)
|
await page.setViewportSize(viewportSize)
|
||||||
|
|
||||||
@ -645,7 +659,7 @@ test.describe('Sketch tests', () => {
|
|||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
|
|
||||||
const center = { x: viewportSize.width / 2, y: viewportSize.height / 2 }
|
const center = { x: viewportSize.width / 2, y: viewportSize.height / 2 }
|
||||||
const { toSU, click00r } = getMovementUtils({ center, page })
|
const { toSU, toU, click00r } = getMovementUtils({ center, page })
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
@ -661,29 +675,32 @@ test.describe('Sketch tests', () => {
|
|||||||
200
|
200
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const center = await u.getCenterOfModelViewArea()
|
||||||
|
|
||||||
let codeStr = "sketch001 = startSketchOn('XY')"
|
let codeStr = "sketch001 = startSketchOn('XY')"
|
||||||
|
|
||||||
await page.mouse.click(center.x, viewportSize.height * 0.55)
|
await page.mouse.click(center.x - 50, viewportSize.height * 0.55)
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||||
|
|
||||||
await click00r(0, 0)
|
const { click00r } = await getMovementUtils({ center, page })
|
||||||
codeStr += ` |> startProfileAt(${toSU([0, 0])}, %)`
|
|
||||||
|
let coord = await click00r(0, 0)
|
||||||
|
codeStr += ` |> startProfileAt(${coord.kcl}, %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(50, 0)
|
await click00r(50, 0)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
codeStr += ` |> lineTo(${toSU([50, 0])}, %)`
|
codeStr += ` |> xLine(${toU(50, 0)[0]}, %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(0, 50)
|
await click00r(0, 50)
|
||||||
codeStr += ` |> line(${toSU([0, 50])}, %)`
|
codeStr += ` |> yLine(${toU(0, 50)[1]}, %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
let clickCoords = await click00r(-50, 0)
|
await click00r(-50, 0)
|
||||||
expect(clickCoords).not.toBeUndefined()
|
codeStr += ` |> xLine(${toU(-50, 0)[0]}, %)`
|
||||||
codeStr += ` |> lineTo(${toSU(clickCoords!)}, %)`
|
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
// exit the sketch, reset relative clicker
|
// exit the sketch, reset relative clicker
|
||||||
@ -699,28 +716,29 @@ test.describe('Sketch tests', () => {
|
|||||||
|
|
||||||
// when exiting the sketch above the camera is still looking down at XY,
|
// when exiting the sketch above the camera is still looking down at XY,
|
||||||
// so selecting the plane again is a bit easier.
|
// so selecting the plane again is a bit easier.
|
||||||
await page.mouse.click(center.x + 200, center.y + 100)
|
await page.mouse.move(center.x - 100, center.y + 50, { steps: 5 })
|
||||||
|
await page.mouse.click(center.x - 100, center.y + 50)
|
||||||
await page.waitForTimeout(600) // TODO detect animation ending, or disable animation
|
await page.waitForTimeout(600) // TODO detect animation ending, or disable animation
|
||||||
codeStr += "sketch002 = startSketchOn('XY')"
|
codeStr += "sketch002 = startSketchOn('XY')"
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
await click00r(30, 0)
|
coord = await click00r(30, 0)
|
||||||
codeStr += ` |> startProfileAt([2.03, 0], %)`
|
codeStr += ` |> startProfileAt(${coord.kcl}, %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
// TODO: I couldn't use `toSU` here because of some rounding error causing
|
// TODO: I couldn't use `toSU` here because of some rounding error causing
|
||||||
// it to be off by 0.01
|
// it to be off by 0.01
|
||||||
await click00r(30, 0)
|
await click00r(30, 0)
|
||||||
codeStr += ` |> lineTo([4.07, 0], %)`
|
codeStr += ` |> xLine(2.04, %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(0, 30)
|
await click00r(0, 30)
|
||||||
codeStr += ` |> line([0, -2.03], %)`
|
codeStr += ` |> yLine(-2.03, %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(-30, 0)
|
await click00r(-30, 0)
|
||||||
codeStr += ` |> line([-2.04, 0], %)`
|
codeStr += ` |> xLine(-2.04, %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(undefined, undefined)
|
await click00r(undefined, undefined)
|
||||||
@ -744,8 +762,8 @@ test.describe('Sketch tests', () => {
|
|||||||
|
|
||||||
const code = `sketch001 = startSketchOn('-XZ')
|
const code = `sketch001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(scale * 34.8)}], %)
|
|> startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(scale * 34.8)}], %)
|
||||||
|> line([${roundOff(scale * 139.19)}, 0], %)
|
|> xLine(${roundOff(scale * 139.19)}, %)
|
||||||
|> line([0, -${roundOff(scale * 139.2)}], %)
|
|> yLine(-${roundOff(scale * 139.2)}, %)
|
||||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|> close(%)`
|
|> close(%)`
|
||||||
|
|
||||||
@ -764,20 +782,21 @@ test.describe('Sketch tests', () => {
|
|||||||
await u.updateCamPosition(camPos)
|
await u.updateCamPosition(camPos)
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
|
const center = await u.getCenterOfModelViewArea()
|
||||||
await page.mouse.move(0, 0)
|
await page.mouse.move(0, 0)
|
||||||
|
|
||||||
// select a plane
|
// select a plane
|
||||||
await page.mouse.move(700, 200, { steps: 10 })
|
await page.mouse.move(center.x + 100, 200, { steps: 10 })
|
||||||
await page.mouse.click(700, 200, { delay: 200 })
|
await page.mouse.click(center.x + 100, 200, { delay: 200 })
|
||||||
await expect(page.locator('.cm-content')).toHaveText(
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
`sketch001 = startSketchOn('-XZ')`
|
`sketch001 = startSketchOn('-XZ')`
|
||||||
)
|
)
|
||||||
|
|
||||||
let prevContent = await page.locator('.cm-content').innerText()
|
let prevContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
const pointA = [700, 200]
|
const pointA = [center.x + 100, 200]
|
||||||
const pointB = [900, 200]
|
const pointB = [center.x + 300, 200]
|
||||||
const pointC = [900, 400]
|
const pointC = [center.x + 300, 400]
|
||||||
|
|
||||||
// draw three lines
|
// draw three lines
|
||||||
await page.waitForTimeout(500)
|
await page.waitForTimeout(500)
|
||||||
@ -914,7 +933,9 @@ extrude001 = extrude(5, sketch001)
|
|||||||
|
|
||||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
|
|
||||||
await page.mouse.click(622, 355)
|
const center = await u.getCenterOfModelViewArea()
|
||||||
|
|
||||||
|
await page.mouse.click(center.x + 22, 355)
|
||||||
|
|
||||||
await page.waitForTimeout(800)
|
await page.waitForTimeout(800)
|
||||||
await page.getByText(`END')`).click()
|
await page.getByText(`END')`).click()
|
||||||
|
@ -462,7 +462,7 @@ test(
|
|||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
code += `
|
code += `
|
||||||
|> line([7.25, 0], %)`
|
|> xLine(7.25, %)`
|
||||||
await expect(page.locator('.cm-content')).toHaveText(code)
|
await expect(page.locator('.cm-content')).toHaveText(code)
|
||||||
|
|
||||||
await page
|
await page
|
||||||
@ -647,7 +647,7 @@ test.describe(
|
|||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
code += `
|
code += `
|
||||||
|> line([7.25, 0], %)`
|
|> xLine(7.25, %)`
|
||||||
await expect(u.codeLocator).toHaveText(code)
|
await expect(u.codeLocator).toHaveText(code)
|
||||||
|
|
||||||
await page
|
await page
|
||||||
@ -752,7 +752,7 @@ test.describe(
|
|||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
code += `
|
code += `
|
||||||
|> line([184.3, 0], %)`
|
|> xLine(184.3, %)`
|
||||||
await expect(u.codeLocator).toHaveText(code)
|
await expect(u.codeLocator).toHaveText(code)
|
||||||
|
|
||||||
await page
|
await page
|
||||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
@ -141,7 +141,7 @@ test.describe('Test network and connection issues', () => {
|
|||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
.toHaveText(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)`)
|
|> xLine(${commonPoints.num1}, %)`)
|
||||||
|
|
||||||
// Expect the network to be up
|
// Expect the network to be up
|
||||||
await expect(networkToggle).toContainText('Connected')
|
await expect(networkToggle).toContainText('Connected')
|
||||||
@ -207,7 +207,7 @@ test.describe('Test network and connection issues', () => {
|
|||||||
await expect.poll(u.normalisedEditorCode)
|
await expect.poll(u.normalisedEditorCode)
|
||||||
.toBe(`sketch001 = startSketchOn('XZ')
|
.toBe(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt([12.34, -12.34], %)
|
|> startProfileAt([12.34, -12.34], %)
|
||||||
|> line([12.34, 0], %)
|
|> xLine(12.34, %)
|
||||||
|> line([-12.34, 12.34], %)
|
|> line([-12.34, 12.34], %)
|
||||||
|
|
||||||
`)
|
`)
|
||||||
@ -217,9 +217,9 @@ test.describe('Test network and connection issues', () => {
|
|||||||
await expect.poll(u.normalisedEditorCode)
|
await expect.poll(u.normalisedEditorCode)
|
||||||
.toBe(`sketch001 = startSketchOn('XZ')
|
.toBe(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt([12.34, -12.34], %)
|
|> startProfileAt([12.34, -12.34], %)
|
||||||
|> line([12.34, 0], %)
|
|> xLine(12.34, %)
|
||||||
|> line([-12.34, 12.34], %)
|
|> line([-12.34, 12.34], %)
|
||||||
|> lineTo([0, -12.34], %)
|
|> xLine(-12.34, %)
|
||||||
|
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
@ -8,6 +8,21 @@ import {
|
|||||||
Locator,
|
Locator,
|
||||||
test,
|
test,
|
||||||
} from '@playwright/test'
|
} from '@playwright/test'
|
||||||
|
import {
|
||||||
|
OrthographicCamera,
|
||||||
|
Mesh,
|
||||||
|
Scene,
|
||||||
|
Raycaster,
|
||||||
|
PlaneGeometry,
|
||||||
|
MeshBasicMaterial,
|
||||||
|
DoubleSide,
|
||||||
|
Vector2,
|
||||||
|
Vector3,
|
||||||
|
} from 'three'
|
||||||
|
import {
|
||||||
|
RAYCASTABLE_PLANE,
|
||||||
|
INTERSECTION_PLANE_LAYER,
|
||||||
|
} from 'clientSideScene/constants'
|
||||||
import { EngineCommand } from 'lang/std/artifactGraph'
|
import { EngineCommand } from 'lang/std/artifactGraph'
|
||||||
import fsp from 'fs/promises'
|
import fsp from 'fs/promises'
|
||||||
import fsSync from 'fs'
|
import fsSync from 'fs'
|
||||||
@ -257,55 +272,141 @@ export const circleMove = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getMovementUtils = (opts: any) => {
|
export function rollingRound(n: number, digitsAfterDecimal: number) {
|
||||||
// The way we truncate is kinda odd apparently, so we need this function
|
const s = String(n).split('.')
|
||||||
// "[k]itty[c]ad round"
|
|
||||||
const kcRound = (n: number) => Math.trunc(n * 100) / 100
|
|
||||||
|
|
||||||
// To translate between screen and engine ("[U]nit") coordinates
|
// There are no decimals, just return the number.
|
||||||
// NOTE: these pretty much can't be perfect because of screen scaling.
|
if (s.length === 1) return n
|
||||||
// Handle on a case-by-case.
|
|
||||||
const toU = (x: number, y: number) => [
|
|
||||||
kcRound(x * 0.0678),
|
|
||||||
kcRound(-y * 0.0678), // Y is inverted in our coordinate system
|
|
||||||
]
|
|
||||||
|
|
||||||
// Turn the array into a string with specific formatting
|
// Find the closest 9. We don't care about anything beyond that.
|
||||||
const fromUToString = (xy: number[]) => `[${xy[0]}, ${xy[1]}]`
|
const nineIndex = s[1].indexOf('9')
|
||||||
|
|
||||||
// Combine because used often
|
const fractStr = nineIndex > 0 ? s[1].slice(0, nineIndex + 1) : s[1]
|
||||||
const toSU = (xy: number[]) => fromUToString(toU(xy[0], xy[1]))
|
|
||||||
|
let fract = Number(fractStr) / 10 ** fractStr.length
|
||||||
|
|
||||||
|
for (let i = fractStr.length - 1; i >= 0; i -= 1) {
|
||||||
|
if (i === digitsAfterDecimal) break
|
||||||
|
fract = Math.round(fract * 10 ** i) / 10 ** i
|
||||||
|
}
|
||||||
|
|
||||||
|
return (Number(s[0]) + fract).toFixed(digitsAfterDecimal)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMovementUtils = async (opts: any) => {
|
||||||
|
const sceneInfra = await opts.page.evaluate(() => window.sceneInfra)
|
||||||
|
|
||||||
|
// Various data for raycasting into the scene to get our XY.
|
||||||
|
const hundredM = 100_0000
|
||||||
|
const planeGeometry = new PlaneGeometry(hundredM, hundredM)
|
||||||
|
const planeMaterial = new MeshBasicMaterial({
|
||||||
|
color: 0xff0000,
|
||||||
|
side: DoubleSide,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.5,
|
||||||
|
})
|
||||||
|
const scene = new Scene()
|
||||||
|
const intersectionPlane = new Mesh(planeGeometry, planeMaterial)
|
||||||
|
intersectionPlane.userData = { type: RAYCASTABLE_PLANE }
|
||||||
|
intersectionPlane.name = RAYCASTABLE_PLANE
|
||||||
|
intersectionPlane.layers.set(INTERSECTION_PLANE_LAYER)
|
||||||
|
scene.add(intersectionPlane)
|
||||||
|
const planeRaycaster = new Raycaster()
|
||||||
|
planeRaycaster.far = Infinity
|
||||||
|
planeRaycaster.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||||
|
|
||||||
|
const kcRound = (n: number) => Math.round(n * 100) / 100
|
||||||
|
|
||||||
// Make it easier to click around from center ("click [from] zero zero")
|
// Make it easier to click around from center ("click [from] zero zero")
|
||||||
const click00 = (x: number, y: number) =>
|
const click00 = (x: number, y: number) =>
|
||||||
opts.page.mouse.click(opts.center.x + x, opts.center.y + y, { delay: 100 })
|
opts.page.mouse.click(x, y, { delay: 100 })
|
||||||
|
|
||||||
// Relative clicker, must keep state
|
// Relative clicker, must keep state
|
||||||
let last = { x: 0, y: 0 }
|
let last = { x: 0, y: 0 }
|
||||||
|
let lastScreenSpace = { x: 0, y: 0 }
|
||||||
|
|
||||||
const click00r = async (x?: number, y?: number) => {
|
const click00r = async (x?: number, y?: number) => {
|
||||||
// reset relative coordinates when anything is undefined
|
// reset relative coordinates when anything is undefined
|
||||||
if (x === undefined || y === undefined) {
|
if (x === undefined || y === undefined) {
|
||||||
last.x = 0
|
last = { x: 0, y: 0 }
|
||||||
last.y = 0
|
lastScreenSpace = { x: 0, y: 0 }
|
||||||
return
|
return {
|
||||||
|
nextXY: [0, 0],
|
||||||
|
kcl: `[0, 0]`,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await circleMove(
|
const absX = opts.center.x + x
|
||||||
opts.page,
|
const absY = opts.center.y + y
|
||||||
opts.center.x + last.x + x,
|
|
||||||
opts.center.y + last.y + y,
|
const nextX = last.x + x
|
||||||
10,
|
const nextY = last.y + y
|
||||||
10
|
|
||||||
|
const targetX = opts.center.x + nextX
|
||||||
|
const targetY = opts.center.y + -nextY
|
||||||
|
|
||||||
|
// Use the current camera specification
|
||||||
|
const camera = await opts.page.evaluate(() => {
|
||||||
|
window.sceneInfra.camControls.onCameraChange(true)
|
||||||
|
return window.sceneInfra.camControls.camera
|
||||||
|
})
|
||||||
|
|
||||||
|
const windowWH = await opts.page.evaluate(() => ({
|
||||||
|
w: window.innerWidth,
|
||||||
|
h: window.innerHeight,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// I didn't write this math, it's copied from sceneInfra.ts, and I understand
|
||||||
|
// it's just normalizing the point, but why *-2 ± 1 I have no idea.
|
||||||
|
const mouseVector = new Vector2(
|
||||||
|
(targetX / windowWH.w) * 2 - 1,
|
||||||
|
-(targetY / windowWH.h) * 2 + 1
|
||||||
)
|
)
|
||||||
await click00(last.x + x, last.y + y)
|
planeRaycaster.setFromCamera(mouseVector, camera)
|
||||||
|
const intersections = planeRaycaster.intersectObjects(scene.children, true)
|
||||||
|
|
||||||
|
const planePosition = intersections[0].object.position
|
||||||
|
const inversePlaneQuaternion = intersections[0].object.quaternion
|
||||||
|
.clone()
|
||||||
|
.invert()
|
||||||
|
let transformedPoint = intersections[0].point.clone()
|
||||||
|
if (transformedPoint) {
|
||||||
|
transformedPoint.applyQuaternion(inversePlaneQuaternion)
|
||||||
|
}
|
||||||
|
const twoD = new Vector2(
|
||||||
|
// I think the intersection plane doesn't get scale when nearly everything else does, maybe that should change
|
||||||
|
transformedPoint.x / sceneInfra._baseUnitMultiplier,
|
||||||
|
transformedPoint.y / sceneInfra._baseUnitMultiplier
|
||||||
|
) // z should be 0
|
||||||
|
const planePositionCorrected = new Vector3(
|
||||||
|
...planePosition
|
||||||
|
).applyQuaternion(inversePlaneQuaternion)
|
||||||
|
twoD.sub(new Vector2(...planePositionCorrected))
|
||||||
|
|
||||||
|
await circleMove(opts.page, targetX, targetY, 10, 10)
|
||||||
|
await click00(targetX, targetY)
|
||||||
|
|
||||||
last.x += x
|
last.x += x
|
||||||
last.y += y
|
last.y += y
|
||||||
|
|
||||||
// Returns the new absolute coordinate if you need it.
|
const relativeScreenSpace = {
|
||||||
return [last.x, last.y]
|
x: twoD.x - lastScreenSpace.x,
|
||||||
|
y: -(twoD.y - lastScreenSpace.y),
|
||||||
|
}
|
||||||
|
|
||||||
|
lastScreenSpace.x = kcRound(twoD.x)
|
||||||
|
lastScreenSpace.y = kcRound(twoD.y)
|
||||||
|
|
||||||
|
// Returns the new absolute coordinate and the screen space coordinate if you need it.
|
||||||
|
return {
|
||||||
|
nextXY: [last.x, last.y],
|
||||||
|
kcl: `[${kcRound(relativeScreenSpace.x)}, ${-kcRound(
|
||||||
|
relativeScreenSpace.y
|
||||||
|
)}]`,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { toSU, click00r }
|
return { toSU, toU, click00r }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForAuthAndLsp(page: Page) {
|
async function waitForAuthAndLsp(page: Page) {
|
||||||
@ -356,6 +457,30 @@ export async function getUtils(page: Page, test_?: typeof test) {
|
|||||||
browserType !== 'chromium' ? null : await page.context().newCDPSession(page)
|
browserType !== 'chromium' ? null : await page.context().newCDPSession(page)
|
||||||
|
|
||||||
const util = {
|
const util = {
|
||||||
|
async getModelViewAreaSize() {
|
||||||
|
const windowInnerWidth = await page.evaluate(() => window.innerWidth)
|
||||||
|
const windowInnerHeight = await page.evaluate(() => window.innerHeight)
|
||||||
|
|
||||||
|
const sidebar = page.getByTestId('modeling-sidebar')
|
||||||
|
const bb = await sidebar.boundingBox()
|
||||||
|
return {
|
||||||
|
w: windowInnerWidth - (bb?.width ?? 0),
|
||||||
|
h: windowInnerHeight - (bb?.height ?? 0),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getCenterOfModelViewArea() {
|
||||||
|
const windowInnerWidth = await page.evaluate(() => window.innerWidth)
|
||||||
|
const windowInnerHeight = await page.evaluate(() => window.innerHeight)
|
||||||
|
|
||||||
|
const sidebar = page.getByTestId('modeling-sidebar')
|
||||||
|
const bb = await sidebar.boundingBox()
|
||||||
|
const goRightPx = (bb?.width ?? 0) / 2
|
||||||
|
const borderWidthsCombined = 2
|
||||||
|
return {
|
||||||
|
x: Math.round(windowInnerWidth / 2 + goRightPx) - borderWidthsCombined,
|
||||||
|
y: Math.round(windowInnerHeight / 2),
|
||||||
|
}
|
||||||
|
},
|
||||||
waitForAuthSkipAppStart: () => waitForAuthAndLsp(page),
|
waitForAuthSkipAppStart: () => waitForAuthAndLsp(page),
|
||||||
waitForPageLoad: () => waitForPageLoad(page),
|
waitForPageLoad: () => waitForPageLoad(page),
|
||||||
waitForPageLoadWithRetry: () => waitForPageLoadWithRetry(page),
|
waitForPageLoadWithRetry: () => waitForPageLoadWithRetry(page),
|
||||||
|
@ -43,10 +43,12 @@ test.describe('Testing constraints', () => {
|
|||||||
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
|
const center = await u.getCenterOfModelViewArea()
|
||||||
|
|
||||||
|
const startXPx = center.x - 100
|
||||||
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
|
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
|
||||||
await page.keyboard.down('Shift')
|
await page.keyboard.down('Shift')
|
||||||
await page.mouse.click(834, 244)
|
await page.mouse.click(center.x + 234, 244)
|
||||||
await page.keyboard.up('Shift')
|
await page.keyboard.up('Shift')
|
||||||
|
|
||||||
await page
|
await page
|
||||||
|
@ -32,10 +32,17 @@ test.describe('Testing selections', () => {
|
|||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
|
|
||||||
const xAxisClick = () =>
|
const yAxisClick = () =>
|
||||||
page.mouse.click(700, 253).then(() => page.waitForTimeout(100))
|
test.step('Click on Y axis', async () => {
|
||||||
|
await page.mouse.move(600, 200, { steps: 5 })
|
||||||
|
await page.mouse.click(600, 200)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
})
|
||||||
const xAxisClickAfterExitingSketch = () =>
|
const xAxisClickAfterExitingSketch = () =>
|
||||||
page.mouse.click(639, 278).then(() => page.waitForTimeout(100))
|
test.step(`Click on X axis after exiting sketch, which shifts it at the moment`, async () => {
|
||||||
|
await page.mouse.click(639, 278)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
})
|
||||||
const emptySpaceHover = () =>
|
const emptySpaceHover = () =>
|
||||||
test.step('Hover over empty space', async () => {
|
test.step('Hover over empty space', async () => {
|
||||||
await page.mouse.move(700, 143, { steps: 5 })
|
await page.mouse.move(700, 143, { steps: 5 })
|
||||||
@ -80,23 +87,23 @@ test.describe('Testing selections', () => {
|
|||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
.toHaveText(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)`)
|
|> xLine(${commonPoints.num1}, %)`)
|
||||||
|
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
.toHaveText(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)
|
|> xLine(${commonPoints.num1}, %)
|
||||||
|> line([0, ${commonPoints.num1 + 0.01}], %)`)
|
|> yLine(${commonPoints.num1 + 0.01}, %)`)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
.toHaveText(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)
|
|> xLine(${commonPoints.num1}, %)
|
||||||
|> line([0, ${commonPoints.num1 + 0.01}], %)
|
|> yLine(${commonPoints.num1 + 0.01}, %)
|
||||||
|> lineTo([0, ${commonPoints.num3}], %)`)
|
|> xLine(${commonPoints.num2 * -1}, %)`)
|
||||||
|
|
||||||
// deselect line tool
|
// deselect line tool
|
||||||
await page.getByRole('button', { name: 'line Line', exact: true }).click()
|
await page.getByRole('button', { name: 'line Line', exact: true }).click()
|
||||||
@ -121,53 +128,58 @@ test.describe('Testing selections', () => {
|
|||||||
// now check clicking works including axis
|
// now check clicking works including axis
|
||||||
|
|
||||||
// click a segment hold shift and click an axis, see that a relevant constraint is enabled
|
// click a segment hold shift and click an axis, see that a relevant constraint is enabled
|
||||||
await topHorzSegmentClick()
|
|
||||||
await page.keyboard.down('Shift')
|
|
||||||
const constrainButton = page.getByRole('button', {
|
const constrainButton = page.getByRole('button', {
|
||||||
name: 'Length: open menu',
|
name: 'Length: open menu',
|
||||||
})
|
})
|
||||||
const absYButton = page.getByRole('button', { name: 'Absolute Y' })
|
const absXButton = page.getByRole('button', { name: 'Absolute X' })
|
||||||
await constrainButton.click()
|
|
||||||
await expect(absYButton).toBeDisabled()
|
await test.step(`Select a segment and an axis, see that a relevant constraint is enabled`, async () => {
|
||||||
await page.waitForTimeout(100)
|
await topHorzSegmentClick()
|
||||||
await xAxisClick()
|
await page.keyboard.down('Shift')
|
||||||
await page.keyboard.up('Shift')
|
await constrainButton.click()
|
||||||
await constrainButton.click()
|
await expect(absXButton).toBeDisabled()
|
||||||
await absYButton.and(page.locator(':not([disabled])')).waitFor()
|
await page.waitForTimeout(100)
|
||||||
await expect(absYButton).not.toBeDisabled()
|
await yAxisClick()
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
await constrainButton.click()
|
||||||
|
await absXButton.and(page.locator(':not([disabled])')).waitFor()
|
||||||
|
await expect(absXButton).not.toBeDisabled()
|
||||||
|
})
|
||||||
|
|
||||||
// clear selection by clicking on nothing
|
|
||||||
await emptySpaceClick()
|
await emptySpaceClick()
|
||||||
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
// same selection but click the axis first
|
|
||||||
await xAxisClick()
|
|
||||||
await constrainButton.click()
|
|
||||||
await expect(absYButton).toBeDisabled()
|
|
||||||
await page.keyboard.down('Shift')
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await topHorzSegmentClick()
|
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await page.keyboard.up('Shift')
|
await test.step(`Same selection but click the axis first`, async () => {
|
||||||
await constrainButton.click()
|
await yAxisClick()
|
||||||
await expect(absYButton).not.toBeDisabled()
|
await constrainButton.click()
|
||||||
|
await expect(absXButton).toBeDisabled()
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await topHorzSegmentClick()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
await constrainButton.click()
|
||||||
|
await expect(absXButton).not.toBeDisabled()
|
||||||
|
})
|
||||||
|
|
||||||
// clear selection by clicking on nothing
|
// clear selection by clicking on nothing
|
||||||
await emptySpaceClick()
|
await emptySpaceClick()
|
||||||
|
|
||||||
// check the same selection again by putting cursor in code first then selecting axis
|
// check the same selection again by putting cursor in code first then selecting axis
|
||||||
await page
|
await test.step(`Same selection but code selection then axis`, async () => {
|
||||||
.getByText(` |> lineTo([0, ${commonPoints.num3}], %)`)
|
await page
|
||||||
.click()
|
.getByText(` |> xLine(${commonPoints.num2 * -1}, %)`)
|
||||||
await page.keyboard.down('Shift')
|
.click()
|
||||||
await constrainButton.click()
|
await page.keyboard.down('Shift')
|
||||||
await expect(absYButton).toBeDisabled()
|
await constrainButton.click()
|
||||||
await page.waitForTimeout(100)
|
await expect(absXButton).toBeDisabled()
|
||||||
await xAxisClick()
|
await page.waitForTimeout(100)
|
||||||
await page.keyboard.up('Shift')
|
await yAxisClick()
|
||||||
await constrainButton.click()
|
await page.keyboard.up('Shift')
|
||||||
await expect(absYButton).not.toBeDisabled()
|
await constrainButton.click()
|
||||||
|
await expect(absXButton).not.toBeDisabled()
|
||||||
|
})
|
||||||
|
|
||||||
// clear selection by clicking on nothing
|
// clear selection by clicking on nothing
|
||||||
await emptySpaceClick()
|
await emptySpaceClick()
|
||||||
@ -182,9 +194,7 @@ test.describe('Testing selections', () => {
|
|||||||
process.platform === 'linux' ? 'Control' : 'Meta'
|
process.platform === 'linux' ? 'Control' : 'Meta'
|
||||||
)
|
)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await page
|
await page.getByText(` |> xLine(${commonPoints.num2 * -1}, %)`).click()
|
||||||
.getByText(` |> lineTo([0, ${commonPoints.num3}], %)`)
|
|
||||||
.click()
|
|
||||||
|
|
||||||
await expect(page.locator('.cm-cursor')).toHaveCount(2)
|
await expect(page.locator('.cm-cursor')).toHaveCount(2)
|
||||||
await page.waitForTimeout(500)
|
await page.waitForTimeout(500)
|
||||||
@ -928,6 +938,7 @@ sketch002 = startSketchOn(extrude001, $seg01)
|
|||||||
// test fillet button with the body in the scene
|
// test fillet button with the body in the scene
|
||||||
const codeToAdd = `${await u.codeLocator.allInnerTexts()}
|
const codeToAdd = `${await u.codeLocator.allInnerTexts()}
|
||||||
extrude001 = extrude(10, sketch001)`
|
extrude001 = extrude(10, sketch001)`
|
||||||
|
await u.codeLocator.clear()
|
||||||
await u.codeLocator.fill(codeToAdd)
|
await u.codeLocator.fill(codeToAdd)
|
||||||
await selectSegment()
|
await selectSegment()
|
||||||
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
|
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
|
||||||
|
@ -258,7 +258,7 @@ test.describe('Testing settings', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test(
|
test.fixme(
|
||||||
`Project settings override user settings on desktop`,
|
`Project settings override user settings on desktop`,
|
||||||
{ tag: ['@electron', '@skipWin'] },
|
{ tag: ['@electron', '@skipWin'] },
|
||||||
async ({ browser: _ }, testInfo) => {
|
async ({ browser: _ }, testInfo) => {
|
||||||
@ -318,7 +318,6 @@ test.describe('Testing settings', () => {
|
|||||||
timeout: 5_000,
|
timeout: 5_000,
|
||||||
})
|
})
|
||||||
.toContain(`themeColor = "${userThemeColor}"`)
|
.toContain(`themeColor = "${userThemeColor}"`)
|
||||||
// Only close the button after we've confirmed
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step('Set project theme color', async () => {
|
await test.step('Set project theme color', async () => {
|
||||||
@ -744,18 +743,19 @@ extrude001 = extrude(5, sketch001)
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
// Selectors and constants
|
// Selectors and constants
|
||||||
const editSketchButton = page.getByRole('button', { name: 'Edit Sketch' })
|
const editSketchButton = page.getByRole('button', { name: 'Edit Sketch' })
|
||||||
const lineToolButton = page.getByTestId('line')
|
const lineToolButton = page.getByTestId('line')
|
||||||
const segmentOverlays = page.getByTestId('segment-overlay')
|
const segmentOverlays = page.getByTestId('segment-overlay')
|
||||||
const sketchOriginLocation = { x: 600, y: 250 }
|
const sketchOriginLocation = await u.getCenterOfModelViewArea()
|
||||||
const darkThemeSegmentColor: [number, number, number] = [215, 215, 215]
|
const darkThemeSegmentColor: [number, number, number] = [215, 215, 215]
|
||||||
const lightThemeSegmentColor: [number, number, number] = [90, 90, 90]
|
const lightThemeSegmentColor: [number, number, number] = [90, 90, 90]
|
||||||
|
|
||||||
await test.step(`Get into sketch mode`, async () => {
|
await test.step(`Get into sketch mode`, async () => {
|
||||||
await u.waitForAuthSkipAppStart()
|
await page.mouse.click(sketchOriginLocation.x, sketchOriginLocation.y)
|
||||||
await page.mouse.click(700, 200)
|
|
||||||
await expect(editSketchButton).toBeVisible()
|
await expect(editSketchButton).toBeVisible()
|
||||||
await editSketchButton.click()
|
await editSketchButton.click()
|
||||||
|
|
||||||
@ -766,12 +766,18 @@ extrude001 = extrude(5, sketch001)
|
|||||||
await page.waitForTimeout(1000)
|
await page.waitForTimeout(1000)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const line1 = await u.getSegmentBodyCoords(`[data-overlay-index="${0}"]`, 0)
|
||||||
|
|
||||||
|
// Our lines are translucent (surprise!), so we need to get on portion
|
||||||
|
// of the line that is only on the background, and not on top of something
|
||||||
|
// like the axis lines.
|
||||||
|
line1.x -= 1
|
||||||
|
line1.y -= 1
|
||||||
|
|
||||||
await test.step(`Check the sketch line color before`, async () => {
|
await test.step(`Check the sketch line color before`, async () => {
|
||||||
await expect
|
await expect
|
||||||
.poll(() =>
|
.poll(() => u.getGreatestPixDiff(line1, darkThemeSegmentColor))
|
||||||
u.getGreatestPixDiff(sketchOriginLocation, darkThemeSegmentColor)
|
.toBeLessThanOrEqual(34)
|
||||||
)
|
|
||||||
.toBeLessThan(15)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step(`Change theme to light using command palette`, async () => {
|
await test.step(`Change theme to light using command palette`, async () => {
|
||||||
@ -786,10 +792,8 @@ extrude001 = extrude(5, sketch001)
|
|||||||
|
|
||||||
await test.step(`Check the sketch line color after`, async () => {
|
await test.step(`Check the sketch line color after`, async () => {
|
||||||
await expect
|
await expect
|
||||||
.poll(() =>
|
.poll(() => u.getGreatestPixDiff(line1, lightThemeSegmentColor))
|
||||||
u.getGreatestPixDiff(sketchOriginLocation, lightThemeSegmentColor)
|
.toBeLessThanOrEqual(34)
|
||||||
)
|
|
||||||
.toBeLessThan(15)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -503,14 +503,16 @@ test('Sketch on face', async ({ page }) => {
|
|||||||
|
|
||||||
let previousCodeContent = await page.locator('.cm-content').innerText()
|
let previousCodeContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
await u.openAndClearDebugPanel()
|
const center = await u.getCenterOfModelViewArea()
|
||||||
|
|
||||||
|
// This basically waits for sketch mode to be ready.
|
||||||
await u.doAndWaitForCmd(
|
await u.doAndWaitForCmd(
|
||||||
() => page.mouse.click(625, 165),
|
async () => page.mouse.click(center.x, 180),
|
||||||
'default_camera_get_settings',
|
'default_camera_get_settings',
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
await page.waitForTimeout(150)
|
|
||||||
await u.closeDebugPanel()
|
await page.waitForTimeout(300)
|
||||||
|
|
||||||
const firstClickPosition = [612, 238]
|
const firstClickPosition = [612, 238]
|
||||||
const secondClickPosition = [661, 242]
|
const secondClickPosition = [661, 242]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "zoo-modeling-app",
|
"name": "zoo-modeling-app",
|
||||||
"version": "0.26.3",
|
"version": "0.26.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"productName": "Zoo Modeling App",
|
"productName": "Zoo Modeling App",
|
||||||
"author": {
|
"author": {
|
||||||
|
46
src/App.tsx
@ -1,15 +1,14 @@
|
|||||||
import { useEffect, useMemo, useRef } from 'react'
|
import { useEffect, useMemo, useRef } from 'react'
|
||||||
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
||||||
import { Stream } from './components/Stream'
|
|
||||||
import { AppHeader } from './components/AppHeader'
|
import { AppHeader } from './components/AppHeader'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { useLoaderData, useNavigate } from 'react-router-dom'
|
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { type IndexLoaderData } from 'lib/types'
|
import { type IndexLoaderData } from 'lib/types'
|
||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||||
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
|
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
|
||||||
import { codeManager, engineCommandManager } from 'lib/singletons'
|
import { codeManager, engineCommandManager, sceneInfra } from 'lib/singletons'
|
||||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { useLspContext } from 'components/LspProvider'
|
import { useLspContext } from 'components/LspProvider'
|
||||||
@ -22,6 +21,8 @@ import Gizmo from 'components/Gizmo'
|
|||||||
import { CoreDumpManager } from 'lib/coredump'
|
import { CoreDumpManager } from 'lib/coredump'
|
||||||
import { UnitsMenu } from 'components/UnitsMenu'
|
import { UnitsMenu } from 'components/UnitsMenu'
|
||||||
import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
|
import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
|
||||||
|
import EngineStreamContext from 'hooks/useEngineStreamContext'
|
||||||
|
import { EngineStream } from 'components/EngineStream'
|
||||||
import { maybeWriteToDisk } from 'lib/telemetry'
|
import { maybeWriteToDisk } from 'lib/telemetry'
|
||||||
maybeWriteToDisk()
|
maybeWriteToDisk()
|
||||||
.then(() => {})
|
.then(() => {})
|
||||||
@ -37,6 +38,13 @@ export function App() {
|
|||||||
// the coredump.
|
// the coredump.
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
// Stream related refs and data
|
||||||
|
const videoRef = useRef<HTMLVideoElement>(null)
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
|
const modelingSidebarRef = useRef<HTMLUListElement>(null)
|
||||||
|
let [searchParams] = useSearchParams()
|
||||||
|
const pool = searchParams.get('pool')
|
||||||
|
|
||||||
const projectName = project?.name || null
|
const projectName = project?.name || null
|
||||||
const projectPath = project?.path || null
|
const projectPath = project?.path || null
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -57,6 +65,10 @@ export function App() {
|
|||||||
app: { onboardingStatus },
|
app: { onboardingStatus },
|
||||||
} = settings.context
|
} = settings.context
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sceneInfra.camControls.modelingSidebarRef = modelingSidebarRef
|
||||||
|
}, [modelingSidebarRef.current])
|
||||||
|
|
||||||
useHotkeys('backspace', (e) => {
|
useHotkeys('backspace', (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
})
|
})
|
||||||
@ -84,14 +96,26 @@ export function App() {
|
|||||||
enableMenu={true}
|
enableMenu={true}
|
||||||
/>
|
/>
|
||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
<ModelingSidebar paneOpacity={paneOpacity} />
|
<ModelingSidebar paneOpacity={paneOpacity} ref={modelingSidebarRef} />
|
||||||
<Stream />
|
<EngineStreamContext.Provider
|
||||||
{/* <CamToggle /> */}
|
options={{
|
||||||
<LowerRightControls coreDumpManager={coreDumpManager}>
|
input: {
|
||||||
<UnitsMenu />
|
videoRef,
|
||||||
<Gizmo />
|
canvasRef,
|
||||||
<CameraProjectionToggle />
|
mediaStream: null,
|
||||||
</LowerRightControls>
|
authToken: auth?.context?.token ?? null,
|
||||||
|
pool,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EngineStream />
|
||||||
|
{/* <CamToggle /> */}
|
||||||
|
<LowerRightControls coreDumpManager={coreDumpManager}>
|
||||||
|
<UnitsMenu />
|
||||||
|
<Gizmo />
|
||||||
|
<CameraProjectionToggle />
|
||||||
|
</LowerRightControls>
|
||||||
|
</EngineStreamContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import {
|
|||||||
} from 'lib/toolbar'
|
} from 'lib/toolbar'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
|
import { EngineConnectionStateType } from 'lang/std/engineConnection'
|
||||||
|
|
||||||
export function Toolbar({
|
export function Toolbar({
|
||||||
className = '',
|
className = '',
|
||||||
@ -48,7 +49,7 @@ export function Toolbar({
|
|||||||
}, [engineCommandManager.artifactGraph, context.selectionRanges])
|
}, [engineCommandManager.artifactGraph, context.selectionRanges])
|
||||||
|
|
||||||
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
||||||
const { overallState } = useNetworkContext()
|
const { overallState, immediateState } = useNetworkContext()
|
||||||
const { isExecuting } = useKclContext()
|
const { isExecuting } = useKclContext()
|
||||||
const { isStreamReady } = useAppState()
|
const { isStreamReady } = useAppState()
|
||||||
|
|
||||||
@ -56,6 +57,7 @@ export function Toolbar({
|
|||||||
(overallState !== NetworkHealthState.Ok &&
|
(overallState !== NetworkHealthState.Ok &&
|
||||||
overallState !== NetworkHealthState.Weak) ||
|
overallState !== NetworkHealthState.Weak) ||
|
||||||
isExecuting ||
|
isExecuting ||
|
||||||
|
immediateState.type !== EngineConnectionStateType.ConnectionEstablished ||
|
||||||
!isStreamReady
|
!isStreamReady
|
||||||
|
|
||||||
const currentMode =
|
const currentMode =
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Models } from '@kittycad/lib'
|
||||||
|
import { MutableRefObject } from 'react'
|
||||||
import { cameraMouseDragGuards, MouseGuard } from 'lib/cameraControls'
|
import { cameraMouseDragGuards, MouseGuard } from 'lib/cameraControls'
|
||||||
import {
|
import {
|
||||||
Euler,
|
Euler,
|
||||||
@ -87,6 +89,9 @@ class CameraRateLimiter {
|
|||||||
|
|
||||||
export class CameraControls {
|
export class CameraControls {
|
||||||
engineCommandManager: EngineCommandManager
|
engineCommandManager: EngineCommandManager
|
||||||
|
modelingSidebarRef: MutableRefObject<HTMLUListElement | null> = {
|
||||||
|
current: null,
|
||||||
|
}
|
||||||
syncDirection: 'clientToEngine' | 'engineToClient' = 'engineToClient'
|
syncDirection: 'clientToEngine' | 'engineToClient' = 'engineToClient'
|
||||||
camera: PerspectiveCamera | OrthographicCamera
|
camera: PerspectiveCamera | OrthographicCamera
|
||||||
target: Vector3
|
target: Vector3
|
||||||
@ -95,6 +100,13 @@ export class CameraControls {
|
|||||||
wasDragging: boolean
|
wasDragging: boolean
|
||||||
mouseDownPosition: Vector2
|
mouseDownPosition: Vector2
|
||||||
mouseNewPosition: Vector2
|
mouseNewPosition: Vector2
|
||||||
|
cameraDragStartXY = new Vector2()
|
||||||
|
old:
|
||||||
|
| {
|
||||||
|
camera: PerspectiveCamera | OrthographicCamera
|
||||||
|
target: Vector3
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
rotationSpeed = 0.3
|
rotationSpeed = 0.3
|
||||||
enableRotate = true
|
enableRotate = true
|
||||||
enablePan = true
|
enablePan = true
|
||||||
@ -461,6 +473,7 @@ export class CameraControls {
|
|||||||
if (this.syncDirection === 'engineToClient') {
|
if (this.syncDirection === 'engineToClient') {
|
||||||
const interaction = this.getInteractionType(event)
|
const interaction = this.getInteractionType(event)
|
||||||
if (interaction === 'none') return
|
if (interaction === 'none') return
|
||||||
|
|
||||||
void this.engineCommandManager.sendSceneCommand({
|
void this.engineCommandManager.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd: {
|
cmd: {
|
||||||
@ -909,18 +922,123 @@ export class CameraControls {
|
|||||||
up: { x: 0, y: 0, z: 1 },
|
up: { x: 0, y: 0, z: 1 },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await this.engineCommandManager.sendSceneCommand({
|
|
||||||
|
await this.centerModelRelativeToPanes({
|
||||||
|
zoomToFit: true,
|
||||||
|
resetLastPaneWidth: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.cameraDragStartXY = new Vector2()
|
||||||
|
this.cameraDragStartXY.x = 0
|
||||||
|
this.cameraDragStartXY.y = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
async restoreCameraPosition(): Promise<void> {
|
||||||
|
if (!this.old) return
|
||||||
|
|
||||||
|
this.camera = this.old.camera.clone()
|
||||||
|
this.target = this.old.target.clone()
|
||||||
|
|
||||||
|
void this.engineCommandManager.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
cmd: {
|
cmd: {
|
||||||
type: 'zoom_to_fit',
|
type: 'default_camera_look_at',
|
||||||
object_ids: [], // leave empty to zoom to all objects
|
...convertThreeCamValuesToEngineCam({
|
||||||
padding: 0.2, // padding around the objects
|
isPerspective: true,
|
||||||
animated: false, // don't animate the zoom for now
|
position: this.camera.position,
|
||||||
|
quaternion: this.camera.quaternion,
|
||||||
|
zoom: this.camera.zoom,
|
||||||
|
target: this.target,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private lastFramePaneWidth: number = 0
|
||||||
|
|
||||||
|
async centerModelRelativeToPanes(args?: {
|
||||||
|
zoomObjectId?: string
|
||||||
|
zoomToFit?: boolean
|
||||||
|
resetLastPaneWidth?: boolean
|
||||||
|
}): Promise<void> {
|
||||||
|
const panes = this.modelingSidebarRef?.current
|
||||||
|
if (!panes) return
|
||||||
|
|
||||||
|
const panesWidth = panes.offsetWidth + panes.offsetLeft
|
||||||
|
|
||||||
|
if (args?.resetLastPaneWidth) {
|
||||||
|
this.lastFramePaneWidth = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const goPx =
|
||||||
|
(panesWidth - this.lastFramePaneWidth) / 2 / window.devicePixelRatio
|
||||||
|
this.lastFramePaneWidth = panesWidth
|
||||||
|
|
||||||
|
// Originally I had tried to use the default_camera_look_at endpoint and
|
||||||
|
// some quaternion math to move the camera right, but it ended up being
|
||||||
|
// overly complicated, and I think the threejs scene also doesn't have the
|
||||||
|
// camera coordinates after a zoom-to-fit... So this is much easier, and
|
||||||
|
// maps better to screen coordinates.
|
||||||
|
|
||||||
|
const requests: Models['ModelingCmdReq_type'][] = [
|
||||||
|
{
|
||||||
|
cmd: {
|
||||||
|
type: 'camera_drag_start',
|
||||||
|
interaction: 'pan',
|
||||||
|
window: { x: goPx < 0 ? -goPx : 0, y: 0 },
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: {
|
||||||
|
type: 'camera_drag_move',
|
||||||
|
interaction: 'pan',
|
||||||
|
window: {
|
||||||
|
x: goPx < 0 ? 0 : goPx,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if (args?.zoomToFit) {
|
||||||
|
requests.unshift({
|
||||||
|
cmd: {
|
||||||
|
type: 'zoom_to_fit',
|
||||||
|
object_ids: args?.zoomObjectId ? [args?.zoomObjectId] : [], // leave empty to zoom to all objects
|
||||||
|
padding: 0.2, // padding around the objects
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.engineCommandManager
|
||||||
|
.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_batch_req',
|
||||||
|
batch_id: uuidv4(),
|
||||||
|
responses: true,
|
||||||
|
requests,
|
||||||
|
})
|
||||||
|
// engineCommandManager can't subscribe to batch responses so we'll send
|
||||||
|
// this one off by its lonesome after.
|
||||||
|
.then(() =>
|
||||||
|
this.engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd: {
|
||||||
|
type: 'camera_drag_end',
|
||||||
|
interaction: 'pan',
|
||||||
|
window: {
|
||||||
|
x: goPx < 0 ? 0 : goPx,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async tweenCameraToQuaternion(
|
async tweenCameraToQuaternion(
|
||||||
targetQuaternion: Quaternion,
|
targetQuaternion: Quaternion,
|
||||||
targetPosition = new Vector3(),
|
targetPosition = new Vector3(),
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
import { useRef, useEffect, useState, useMemo, Fragment } from 'react'
|
import {
|
||||||
|
CSSProperties,
|
||||||
|
useRef,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
useMemo,
|
||||||
|
Fragment,
|
||||||
|
} from 'react'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
|
||||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||||
@ -202,12 +209,20 @@ const Overlay = ({
|
|||||||
let xAlignment = overlay.angle < 0 ? '0%' : '-100%'
|
let xAlignment = overlay.angle < 0 ? '0%' : '-100%'
|
||||||
let yAlignment = overlay.angle < -90 || overlay.angle >= 90 ? '0%' : '-100%'
|
let yAlignment = overlay.angle < -90 || overlay.angle >= 90 ? '0%' : '-100%'
|
||||||
|
|
||||||
|
// It's possible for the pathToNode to request a newer AST node
|
||||||
|
// than what's available in the AST at the moment of query.
|
||||||
|
// It eventually settles on being updated.
|
||||||
const _node1 = getNodeFromPath<Node<CallExpression>>(
|
const _node1 = getNodeFromPath<Node<CallExpression>>(
|
||||||
kclManager.ast,
|
kclManager.ast,
|
||||||
overlay.pathToNode,
|
overlay.pathToNode,
|
||||||
'CallExpression'
|
'CallExpression'
|
||||||
)
|
)
|
||||||
if (err(_node1)) return
|
|
||||||
|
// For that reason, to prevent console noise, we do not use err here.
|
||||||
|
if (_node1 instanceof Error) {
|
||||||
|
console.warn('ast older than pathToNode, not fatal, eventually settles', '')
|
||||||
|
return
|
||||||
|
}
|
||||||
const callExpression = _node1.node
|
const callExpression = _node1.node
|
||||||
|
|
||||||
const constraints = getConstraintInfo(
|
const constraints = getConstraintInfo(
|
||||||
@ -234,6 +249,13 @@ const Overlay = ({
|
|||||||
state.matches({ Sketch: 'Rectangle tool' })
|
state.matches({ Sketch: 'Rectangle tool' })
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Line labels will cover the constraints overlay if this is not used.
|
||||||
|
// For each line label, ThreeJS increments each CSS2DObject z-index as they
|
||||||
|
// are added. I have looked into overriding renderOrder and depthTest and
|
||||||
|
// while renderOrder is set, ThreeJS still sets z-index on these 2D objects.
|
||||||
|
// It is easier to set this to a large number, such as a billion.
|
||||||
|
const zIndex = 1000000000
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`absolute w-0 h-0`}>
|
<div className={`absolute w-0 h-0`}>
|
||||||
<div
|
<div
|
||||||
@ -244,6 +266,7 @@ const Overlay = ({
|
|||||||
data-overlay-angle={overlay.angle}
|
data-overlay-angle={overlay.angle}
|
||||||
className="pointer-events-auto absolute w-0 h-0"
|
className="pointer-events-auto absolute w-0 h-0"
|
||||||
style={{
|
style={{
|
||||||
|
zIndex,
|
||||||
transform: `translate3d(${overlay.windowCoords[0]}px, ${overlay.windowCoords[1]}px, 0)`,
|
transform: `translate3d(${overlay.windowCoords[0]}px, ${overlay.windowCoords[1]}px, 0)`,
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
@ -252,6 +275,7 @@ const Overlay = ({
|
|||||||
data-overlay-toolbar-index={overlayIndex}
|
data-overlay-toolbar-index={overlayIndex}
|
||||||
className={`px-0 pointer-events-auto absolute flex gap-1`}
|
className={`px-0 pointer-events-auto absolute flex gap-1`}
|
||||||
style={{
|
style={{
|
||||||
|
zIndex,
|
||||||
transform: `translate3d(calc(${
|
transform: `translate3d(calc(${
|
||||||
overlay.windowCoords[0] + xOffset
|
overlay.windowCoords[0] + xOffset
|
||||||
}px + ${xAlignment}), calc(${
|
}px + ${xAlignment}), calc(${
|
||||||
@ -293,6 +317,7 @@ const Overlay = ({
|
|||||||
*/}
|
*/}
|
||||||
{callExpression?.callee?.name !== 'circle' && (
|
{callExpression?.callee?.name !== 'circle' && (
|
||||||
<SegmentMenu
|
<SegmentMenu
|
||||||
|
style={{ zIndex }}
|
||||||
verticalPosition={
|
verticalPosition={
|
||||||
overlay.windowCoords[1] > window.innerHeight / 2
|
overlay.windowCoords[1] > window.innerHeight / 2
|
||||||
? 'top'
|
? 'top'
|
||||||
@ -434,15 +459,17 @@ const SegmentMenu = ({
|
|||||||
verticalPosition,
|
verticalPosition,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
stdLibFnName,
|
stdLibFnName,
|
||||||
|
style,
|
||||||
}: {
|
}: {
|
||||||
verticalPosition: 'top' | 'bottom'
|
verticalPosition: 'top' | 'bottom'
|
||||||
pathToNode: PathToNode
|
pathToNode: PathToNode
|
||||||
stdLibFnName: string
|
stdLibFnName: string
|
||||||
|
style?: CSSProperties
|
||||||
}) => {
|
}) => {
|
||||||
const { send } = useModelingContext()
|
const { send } = useModelingContext()
|
||||||
const dependentSourceRanges = findUsesOfTagInPipe(kclManager.ast, pathToNode)
|
const dependentSourceRanges = findUsesOfTagInPipe(kclManager.ast, pathToNode)
|
||||||
return (
|
return (
|
||||||
<Popover className="relative">
|
<Popover style={style} className="relative">
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
@ -637,10 +664,16 @@ const ConstraintSymbol = ({
|
|||||||
kclManager.ast,
|
kclManager.ast,
|
||||||
kclManager.programMemory
|
kclManager.programMemory
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!transform) return
|
if (!transform) return
|
||||||
const { modifiedAst } = transform
|
const { modifiedAst } = transform
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
kclManager.updateAst(modifiedAst, true)
|
await kclManager.updateAst(modifiedAst, true)
|
||||||
|
|
||||||
|
// Code editor will be updated in the modelingMachine.
|
||||||
|
const newCode = recast(modifiedAst)
|
||||||
|
if (err(newCode)) return
|
||||||
|
await codeManager.updateCodeEditor(newCode)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('error', e)
|
console.log('error', e)
|
||||||
}
|
}
|
||||||
|
22
src/clientSideScene/constants.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// 63.5 is definitely a bit of a magic number, play with it until it looked right
|
||||||
|
// if it were 64, that would feel like it's something in the engine where a random
|
||||||
|
// power of 2 is used, but it's the 0.5 seems to make things look much more correct
|
||||||
|
export const ZOOM_MAGIC_NUMBER = 63.5
|
||||||
|
|
||||||
|
export const INTERSECTION_PLANE_LAYER = 1
|
||||||
|
export const SKETCH_LAYER = 2
|
||||||
|
|
||||||
|
export const RAYCASTABLE_PLANE = 'raycastable-plane'
|
||||||
|
|
||||||
|
// redundant types so that it can be changed temporarily but CI will catch the wrong type
|
||||||
|
export const DEBUG_SHOW_INTERSECTION_PLANE: false = false
|
||||||
|
export const DEBUG_SHOW_BOTH_SCENES: false = false
|
||||||
|
|
||||||
|
export const X_AXIS = 'xAxis'
|
||||||
|
export const Y_AXIS = 'yAxis'
|
||||||
|
export const AXIS_GROUP = 'axisGroup'
|
||||||
|
export const SKETCH_GROUP_SEGMENTS = 'sketch-group-segments'
|
||||||
|
export const ARROWHEAD = 'arrowhead'
|
||||||
|
export const SEGMENT_LENGTH_LABEL = 'segment-length-label'
|
||||||
|
export const SEGMENT_LENGTH_LABEL_TEXT = 'segment-length-label-text'
|
||||||
|
export const SEGMENT_LENGTH_LABEL_OFFSET_PX = 30
|
@ -2,10 +2,7 @@ import { compareVec2Epsilon2 } from 'lang/std/sketch'
|
|||||||
import {
|
import {
|
||||||
GridHelper,
|
GridHelper,
|
||||||
LineBasicMaterial,
|
LineBasicMaterial,
|
||||||
OrthographicCamera,
|
|
||||||
PerspectiveCamera,
|
PerspectiveCamera,
|
||||||
Group,
|
|
||||||
Mesh,
|
|
||||||
Quaternion,
|
Quaternion,
|
||||||
Vector3,
|
Vector3,
|
||||||
} from 'three'
|
} from 'three'
|
||||||
@ -28,15 +25,9 @@ export function createGridHelper({
|
|||||||
gridHelper.rotation.x = Math.PI / 2
|
gridHelper.rotation.x = Math.PI / 2
|
||||||
return gridHelper
|
return gridHelper
|
||||||
}
|
}
|
||||||
const fudgeFactor = 72.66985970437086
|
|
||||||
|
|
||||||
export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) =>
|
// Re-export scale.ts
|
||||||
(0.55 * fudgeFactor) / cam.zoom / window.innerHeight
|
export * from './scale'
|
||||||
|
|
||||||
export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) =>
|
|
||||||
(group.position.distanceTo(cam.position) * cam.fov * fudgeFactor) /
|
|
||||||
4000 /
|
|
||||||
window.innerHeight
|
|
||||||
|
|
||||||
export function isQuaternionVertical(q: Quaternion) {
|
export function isQuaternionVertical(q: Quaternion) {
|
||||||
const v = new Vector3(0, 0, 1).applyQuaternion(q)
|
const v = new Vector3(0, 0, 1).applyQuaternion(q)
|
||||||
|
17
src/clientSideScene/scale.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { OrthographicCamera, PerspectiveCamera, Group, Mesh } from 'three'
|
||||||
|
|
||||||
|
export const fudgeFactor = 72.66985970437086
|
||||||
|
|
||||||
|
export const orthoScale = (
|
||||||
|
cam: OrthographicCamera | PerspectiveCamera,
|
||||||
|
innerHeight?: number
|
||||||
|
) => (0.55 * fudgeFactor) / cam.zoom / (innerHeight ?? window.innerHeight)
|
||||||
|
|
||||||
|
export const perspScale = (
|
||||||
|
cam: PerspectiveCamera,
|
||||||
|
group: Group | Mesh,
|
||||||
|
innerHeight?: number
|
||||||
|
) =>
|
||||||
|
(group.position.distanceTo(cam.position) * cam.fov * fudgeFactor) /
|
||||||
|
4000 /
|
||||||
|
(innerHeight ?? window.innerHeight)
|
@ -17,6 +17,7 @@ import {
|
|||||||
Vector3,
|
Vector3,
|
||||||
} from 'three'
|
} from 'three'
|
||||||
import {
|
import {
|
||||||
|
ANGLE_SNAP_THRESHOLD_DEGREES,
|
||||||
ARROWHEAD,
|
ARROWHEAD,
|
||||||
AXIS_GROUP,
|
AXIS_GROUP,
|
||||||
DRAFT_POINT,
|
DRAFT_POINT,
|
||||||
@ -95,6 +96,7 @@ import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
|
|||||||
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
|
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
|
||||||
import { SegmentInputs } from 'lang/std/stdTypes'
|
import { SegmentInputs } from 'lang/std/stdTypes'
|
||||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
|
import { radToDeg } from 'three/src/math/MathUtils'
|
||||||
|
|
||||||
type DraftSegment = 'line' | 'tangentialArcTo'
|
type DraftSegment = 'line' | 'tangentialArcTo'
|
||||||
|
|
||||||
@ -451,6 +453,7 @@ export class SceneEntities {
|
|||||||
const { modifiedAst } = addStartProfileAtRes
|
const { modifiedAst } = addStartProfileAtRes
|
||||||
|
|
||||||
await kclManager.updateAst(modifiedAst, false)
|
await kclManager.updateAst(modifiedAst, false)
|
||||||
|
|
||||||
this.removeIntersectionPlane()
|
this.removeIntersectionPlane()
|
||||||
this.scene.remove(draftPointGroup)
|
this.scene.remove(draftPointGroup)
|
||||||
|
|
||||||
@ -683,7 +686,7 @@ export class SceneEntities {
|
|||||||
})
|
})
|
||||||
return nextAst
|
return nextAst
|
||||||
}
|
}
|
||||||
setUpDraftSegment = async (
|
setupDraftSegment = async (
|
||||||
sketchPathToNode: PathToNode,
|
sketchPathToNode: PathToNode,
|
||||||
forward: [number, number, number],
|
forward: [number, number, number],
|
||||||
up: [number, number, number],
|
up: [number, number, number],
|
||||||
@ -798,11 +801,24 @@ export class SceneEntities {
|
|||||||
(sceneObject) => sceneObject.object.name === X_AXIS
|
(sceneObject) => sceneObject.object.name === X_AXIS
|
||||||
)
|
)
|
||||||
|
|
||||||
const lastSegment = sketch.paths.slice(-1)[0]
|
const lastSegment = sketch.paths.slice(-1)[0] || sketch.start
|
||||||
const snappedPoint = {
|
const snappedPoint = {
|
||||||
x: intersectsYAxis ? 0 : intersection2d.x,
|
x: intersectsYAxis ? 0 : intersection2d.x,
|
||||||
y: intersectsXAxis ? 0 : intersection2d.y,
|
y: intersectsXAxis ? 0 : intersection2d.y,
|
||||||
}
|
}
|
||||||
|
// Get the angle between the previous segment (or sketch start)'s end and this one's
|
||||||
|
const angle = Math.atan2(
|
||||||
|
snappedPoint.y - lastSegment.to[1],
|
||||||
|
snappedPoint.x - lastSegment.to[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
const isHorizontal =
|
||||||
|
radToDeg(Math.abs(angle)) < ANGLE_SNAP_THRESHOLD_DEGREES ||
|
||||||
|
Math.abs(radToDeg(Math.abs(angle) - Math.PI)) <
|
||||||
|
ANGLE_SNAP_THRESHOLD_DEGREES
|
||||||
|
const isVertical =
|
||||||
|
Math.abs(radToDeg(Math.abs(angle) - Math.PI / 2)) <
|
||||||
|
ANGLE_SNAP_THRESHOLD_DEGREES
|
||||||
|
|
||||||
let resolvedFunctionName: ToolTip = 'line'
|
let resolvedFunctionName: ToolTip = 'line'
|
||||||
|
|
||||||
@ -810,6 +826,12 @@ export class SceneEntities {
|
|||||||
// case-based logic for different segment types
|
// case-based logic for different segment types
|
||||||
if (lastSegment.type === 'TangentialArcTo') {
|
if (lastSegment.type === 'TangentialArcTo') {
|
||||||
resolvedFunctionName = 'tangentialArcTo'
|
resolvedFunctionName = 'tangentialArcTo'
|
||||||
|
} else if (isHorizontal) {
|
||||||
|
// If the angle between is 0 or 180 degrees (+/- the snapping angle), make the line an xLine
|
||||||
|
resolvedFunctionName = 'xLine'
|
||||||
|
} else if (isVertical) {
|
||||||
|
// If the angle between is 90 or 270 degrees (+/- the snapping angle), make the line a yLine
|
||||||
|
resolvedFunctionName = 'yLine'
|
||||||
} else if (snappedPoint.x === 0 || snappedPoint.y === 0) {
|
} else if (snappedPoint.x === 0 || snappedPoint.y === 0) {
|
||||||
// We consider a point placed on axes or origin to be absolute
|
// We consider a point placed on axes or origin to be absolute
|
||||||
resolvedFunctionName = 'lineTo'
|
resolvedFunctionName = 'lineTo'
|
||||||
@ -835,10 +857,11 @@ export class SceneEntities {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await kclManager.executeAstMock(modifiedAst)
|
await kclManager.executeAstMock(modifiedAst)
|
||||||
|
|
||||||
if (intersectsProfileStart) {
|
if (intersectsProfileStart) {
|
||||||
sceneInfra.modelingSend({ type: 'CancelSketch' })
|
sceneInfra.modelingSend({ type: 'CancelSketch' })
|
||||||
} else {
|
} else {
|
||||||
await this.setUpDraftSegment(
|
await this.setupDraftSegment(
|
||||||
sketchPathToNode,
|
sketchPathToNode,
|
||||||
forward,
|
forward,
|
||||||
up,
|
up,
|
||||||
@ -846,6 +869,8 @@ export class SceneEntities {
|
|||||||
segmentName
|
segmentName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(modifiedAst)
|
||||||
},
|
},
|
||||||
onMove: (args) => {
|
onMove: (args) => {
|
||||||
this.onDragSegment({
|
this.onDragSegment({
|
||||||
@ -970,43 +995,51 @@ export class SceneEntities {
|
|||||||
if (trap(_node)) return
|
if (trap(_node)) return
|
||||||
const sketchInit = _node.node?.declarations?.[0]?.init
|
const sketchInit = _node.node?.declarations?.[0]?.init
|
||||||
|
|
||||||
if (sketchInit.type === 'PipeExpression') {
|
if (sketchInit.type !== 'PipeExpression') {
|
||||||
updateRectangleSketch(sketchInit, x, y, tags[0])
|
return
|
||||||
|
|
||||||
let _recastAst = parse(recast(_ast))
|
|
||||||
if (trap(_recastAst)) return
|
|
||||||
_ast = _recastAst
|
|
||||||
|
|
||||||
// Update the primary AST and unequip the rectangle tool
|
|
||||||
await kclManager.executeAstMock(_ast)
|
|
||||||
sceneInfra.modelingSend({ type: 'Finish rectangle' })
|
|
||||||
|
|
||||||
const { execState } = await executeAst({
|
|
||||||
ast: _ast,
|
|
||||||
useFakeExecutor: true,
|
|
||||||
engineCommandManager: this.engineCommandManager,
|
|
||||||
programMemoryOverride,
|
|
||||||
idGenerator: kclManager.execState.idGenerator,
|
|
||||||
})
|
|
||||||
const programMemory = execState.memory
|
|
||||||
|
|
||||||
// Prepare to update the THREEjs scene
|
|
||||||
this.sceneProgramMemory = programMemory
|
|
||||||
const sketch = sketchFromKclValue(
|
|
||||||
programMemory.get(variableDeclarationName),
|
|
||||||
variableDeclarationName
|
|
||||||
)
|
|
||||||
if (err(sketch)) return
|
|
||||||
const sgPaths = sketch.paths
|
|
||||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
|
||||||
|
|
||||||
// Update the starting segment of the THREEjs scene
|
|
||||||
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
|
|
||||||
// Update the rest of the segments of the THREEjs scene
|
|
||||||
sgPaths.forEach((seg, index) =>
|
|
||||||
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateRectangleSketch(sketchInit, x, y, tags[0])
|
||||||
|
|
||||||
|
const newCode = recast(_ast)
|
||||||
|
let _recastAst = parse(newCode)
|
||||||
|
if (trap(_recastAst)) return
|
||||||
|
_ast = _recastAst
|
||||||
|
|
||||||
|
// Update the primary AST and unequip the rectangle tool
|
||||||
|
await kclManager.executeAstMock(_ast)
|
||||||
|
sceneInfra.modelingSend({ type: 'Finish rectangle' })
|
||||||
|
|
||||||
|
// lee: I had this at the bottom of the function, but it's
|
||||||
|
// possible sketchFromKclValue "fails" when sketching on a face,
|
||||||
|
// and this couldn't wouldn't run.
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(_ast)
|
||||||
|
|
||||||
|
const { execState } = await executeAst({
|
||||||
|
ast: _ast,
|
||||||
|
useFakeExecutor: true,
|
||||||
|
engineCommandManager: this.engineCommandManager,
|
||||||
|
programMemoryOverride,
|
||||||
|
idGenerator: kclManager.execState.idGenerator,
|
||||||
|
})
|
||||||
|
const programMemory = execState.memory
|
||||||
|
|
||||||
|
// Prepare to update the THREEjs scene
|
||||||
|
this.sceneProgramMemory = programMemory
|
||||||
|
const sketch = sketchFromKclValue(
|
||||||
|
programMemory.get(variableDeclarationName),
|
||||||
|
variableDeclarationName
|
||||||
|
)
|
||||||
|
if (err(sketch)) return
|
||||||
|
const sgPaths = sketch.paths
|
||||||
|
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||||
|
|
||||||
|
// Update the starting segment of the THREEjs scene
|
||||||
|
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
|
||||||
|
// Update the rest of the segments of the THREEjs scene
|
||||||
|
sgPaths.forEach((seg, index) =>
|
||||||
|
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1166,13 +1199,17 @@ export class SceneEntities {
|
|||||||
if (err(moddedResult)) return
|
if (err(moddedResult)) return
|
||||||
modded = moddedResult.modifiedAst
|
modded = moddedResult.modifiedAst
|
||||||
|
|
||||||
let _recastAst = parse(recast(modded))
|
const newCode = recast(modded)
|
||||||
|
if (err(newCode)) return
|
||||||
|
let _recastAst = parse(newCode)
|
||||||
if (trap(_recastAst)) return Promise.reject(_recastAst)
|
if (trap(_recastAst)) return Promise.reject(_recastAst)
|
||||||
_ast = _recastAst
|
_ast = _recastAst
|
||||||
|
|
||||||
// Update the primary AST and unequip the rectangle tool
|
// Update the primary AST and unequip the rectangle tool
|
||||||
await kclManager.executeAstMock(_ast)
|
await kclManager.executeAstMock(_ast)
|
||||||
sceneInfra.modelingSend({ type: 'Finish circle' })
|
sceneInfra.modelingSend({ type: 'Finish circle' })
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(_ast)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -1208,6 +1245,7 @@ export class SceneEntities {
|
|||||||
forward,
|
forward,
|
||||||
position,
|
position,
|
||||||
})
|
})
|
||||||
|
await codeManager.writeToFile()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDrag: async ({
|
onDrag: async ({
|
||||||
|
@ -50,6 +50,8 @@ export const RAYCASTABLE_PLANE = 'raycastable-plane'
|
|||||||
|
|
||||||
export const X_AXIS = 'xAxis'
|
export const X_AXIS = 'xAxis'
|
||||||
export const Y_AXIS = 'yAxis'
|
export const Y_AXIS = 'yAxis'
|
||||||
|
/** If a segment angle is less than this many degrees off a meanginful angle it'll snap to it */
|
||||||
|
export const ANGLE_SNAP_THRESHOLD_DEGREES = 3
|
||||||
/** the THREEjs representation of the group surrounding a "snapped" point that is not yet placed */
|
/** the THREEjs representation of the group surrounding a "snapped" point that is not yet placed */
|
||||||
export const DRAFT_POINT_GROUP = 'draft-point-group'
|
export const DRAFT_POINT_GROUP = 'draft-point-group'
|
||||||
/** the THREEjs representation of a "snapped" point that is not yet placed */
|
/** the THREEjs representation of a "snapped" point that is not yet placed */
|
||||||
@ -289,14 +291,14 @@ export class SceneInfra {
|
|||||||
engineCommandManager
|
engineCommandManager
|
||||||
)
|
)
|
||||||
this.camControls.subscribeToCamChange(() => this.onCameraChange())
|
this.camControls.subscribeToCamChange(() => this.onCameraChange())
|
||||||
this.camControls.camera.layers.enable(SKETCH_LAYER)
|
this.camControls.camera.layers.enable(constants.SKETCH_LAYER)
|
||||||
if (DEBUG_SHOW_INTERSECTION_PLANE)
|
if (constants.DEBUG_SHOW_INTERSECTION_PLANE)
|
||||||
this.camControls.camera.layers.enable(INTERSECTION_PLANE_LAYER)
|
this.camControls.camera.layers.enable(constants.INTERSECTION_PLANE_LAYER)
|
||||||
|
|
||||||
// RAYCASTERS
|
// RAYCASTERS
|
||||||
this.raycaster.layers.enable(SKETCH_LAYER)
|
this.raycaster.layers.enable(constants.SKETCH_LAYER)
|
||||||
this.raycaster.layers.disable(0)
|
this.raycaster.layers.disable(0)
|
||||||
this.planeRaycaster.layers.enable(INTERSECTION_PLANE_LAYER)
|
this.planeRaycaster.layers.enable(constants.INTERSECTION_PLANE_LAYER)
|
||||||
|
|
||||||
// GRID
|
// GRID
|
||||||
const size = 100
|
const size = 100
|
||||||
@ -331,7 +333,7 @@ export class SceneInfra {
|
|||||||
this.camControls.target
|
this.camControls.target
|
||||||
)
|
)
|
||||||
const axisGroup = this.scene
|
const axisGroup = this.scene
|
||||||
.getObjectByName(AXIS_GROUP)
|
.getObjectByName(constants.AXIS_GROUP)
|
||||||
?.getObjectByName('gridHelper')
|
?.getObjectByName('gridHelper')
|
||||||
axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale)
|
axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale)
|
||||||
}
|
}
|
||||||
@ -342,7 +344,6 @@ export class SceneInfra {
|
|||||||
}
|
}
|
||||||
|
|
||||||
animate = () => {
|
animate = () => {
|
||||||
requestAnimationFrame(this.animate)
|
|
||||||
TWEEN.update() // This will update all tweens during the animation loop
|
TWEEN.update() // This will update all tweens during the animation loop
|
||||||
if (!this.isFovAnimationInProgress) {
|
if (!this.isFovAnimationInProgress) {
|
||||||
// console.log('animation frame', this.cameraControls.camera)
|
// console.log('animation frame', this.cameraControls.camera)
|
||||||
@ -350,6 +351,7 @@ export class SceneInfra {
|
|||||||
this.renderer.render(this.scene, this.camControls.camera)
|
this.renderer.render(this.scene, this.camControls.camera)
|
||||||
this.labelRenderer.render(this.scene, this.camControls.camera)
|
this.labelRenderer.render(this.scene, this.camControls.camera)
|
||||||
}
|
}
|
||||||
|
requestAnimationFrame(this.animate)
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose = () => {
|
dispose = () => {
|
||||||
@ -653,11 +655,11 @@ export class SceneInfra {
|
|||||||
}
|
}
|
||||||
updateOtherSelectionColors = (otherSelections: Axis[]) => {
|
updateOtherSelectionColors = (otherSelections: Axis[]) => {
|
||||||
const axisGroup = this.scene.children.find(
|
const axisGroup = this.scene.children.find(
|
||||||
({ userData }) => userData?.type === AXIS_GROUP
|
({ userData }) => userData?.type === constants.AXIS_GROUP
|
||||||
)
|
)
|
||||||
const axisMap: { [key: string]: Axis } = {
|
const axisMap: { [key: string]: Axis } = {
|
||||||
[X_AXIS]: 'x-axis',
|
[constants.X_AXIS]: 'x-axis',
|
||||||
[Y_AXIS]: 'y-axis',
|
[constants.Y_AXIS]: 'y-axis',
|
||||||
}
|
}
|
||||||
axisGroup?.children.forEach((_mesh) => {
|
axisGroup?.children.forEach((_mesh) => {
|
||||||
const mesh = _mesh as Mesh
|
const mesh = _mesh as Mesh
|
||||||
|
@ -300,7 +300,7 @@ class StraightSegment implements SegmentUtils {
|
|||||||
sceneInfra.updateOverlayDetails({
|
sceneInfra.updateOverlayDetails({
|
||||||
arrowGroup,
|
arrowGroup,
|
||||||
group,
|
group,
|
||||||
isHandlesVisible,
|
isHandlesVisible: true,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
})
|
})
|
||||||
@ -476,7 +476,7 @@ class TangentialArcToSegment implements SegmentUtils {
|
|||||||
sceneInfra.updateOverlayDetails({
|
sceneInfra.updateOverlayDetails({
|
||||||
arrowGroup,
|
arrowGroup,
|
||||||
group,
|
group,
|
||||||
isHandlesVisible,
|
isHandlesVisible: true,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
angle,
|
angle,
|
||||||
@ -542,7 +542,7 @@ class CircleSegment implements SegmentUtils {
|
|||||||
}
|
}
|
||||||
group.name = CIRCLE_SEGMENT
|
group.name = CIRCLE_SEGMENT
|
||||||
|
|
||||||
group.add(arcMesh, arrowGroup, circleCenterGroup, radiusIndicatorGroup)
|
group.add(arcMesh, arrowGroup, circleCenterGroup)
|
||||||
const updateOverlaysCallback = this.update({
|
const updateOverlaysCallback = this.update({
|
||||||
prevSegment,
|
prevSegment,
|
||||||
input,
|
input,
|
||||||
@ -677,7 +677,7 @@ class CircleSegment implements SegmentUtils {
|
|||||||
sceneInfra.updateOverlayDetails({
|
sceneInfra.updateOverlayDetails({
|
||||||
arrowGroup,
|
arrowGroup,
|
||||||
group,
|
group,
|
||||||
isHandlesVisible,
|
isHandlesVisible: true,
|
||||||
from: from,
|
from: from,
|
||||||
to: [center[0], center[1]],
|
to: [center[0], center[1]],
|
||||||
angle: Math.PI / 4,
|
angle: Math.PI / 4,
|
||||||
|
@ -145,7 +145,7 @@ export function useCalc({
|
|||||||
const _programMem: ProgramMemory = ProgramMemory.empty()
|
const _programMem: ProgramMemory = ProgramMemory.empty()
|
||||||
for (const { key, value } of availableVarInfo.variables) {
|
for (const { key, value } of availableVarInfo.variables) {
|
||||||
const error = _programMem.set(key, {
|
const error = _programMem.set(key, {
|
||||||
type: 'UserVal',
|
type: 'String',
|
||||||
value,
|
value,
|
||||||
__meta: [],
|
__meta: [],
|
||||||
})
|
})
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { Dialog, Popover, Transition } from '@headlessui/react'
|
import { Dialog, Popover, Transition } from '@headlessui/react'
|
||||||
import { Fragment, useEffect } from 'react'
|
import { Fragment, useEffect } from 'react'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
|
import { EngineConnectionStateType } from 'lang/std/engineConnection'
|
||||||
import CommandBarArgument from './CommandBarArgument'
|
import CommandBarArgument from './CommandBarArgument'
|
||||||
import CommandComboBox from '../CommandComboBox'
|
import CommandComboBox from '../CommandComboBox'
|
||||||
import CommandBarReview from './CommandBarReview'
|
import CommandBarReview from './CommandBarReview'
|
||||||
@ -14,6 +16,7 @@ export const COMMAND_PALETTE_HOTKEY = 'mod+k'
|
|||||||
export const CommandBar = () => {
|
export const CommandBar = () => {
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
|
const { immediateState } = useNetworkContext()
|
||||||
const {
|
const {
|
||||||
context: { selectedCommand, currentArgument, commands },
|
context: { selectedCommand, currentArgument, commands },
|
||||||
} = commandBarState
|
} = commandBarState
|
||||||
@ -25,6 +28,14 @@ export const CommandBar = () => {
|
|||||||
commandBarSend({ type: 'Close' })
|
commandBarSend({ type: 'Close' })
|
||||||
}, [pathname])
|
}, [pathname])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
immediateState.type !== EngineConnectionStateType.ConnectionEstablished
|
||||||
|
) {
|
||||||
|
commandBarSend({ type: 'Close' })
|
||||||
|
}
|
||||||
|
}, [immediateState])
|
||||||
|
|
||||||
// Hook up keyboard shortcuts
|
// Hook up keyboard shortcuts
|
||||||
useHotkeyWrapper([COMMAND_PALETTE_HOTKEY], () => {
|
useHotkeyWrapper([COMMAND_PALETTE_HOTKEY], () => {
|
||||||
if (commandBarState.context.commands.length === 0) return
|
if (commandBarState.context.commands.length === 0) return
|
||||||
|
@ -2,13 +2,20 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
|
|||||||
import usePlatform from 'hooks/usePlatform'
|
import usePlatform from 'hooks/usePlatform'
|
||||||
import { hotkeyDisplay } from 'lib/hotkeyWrapper'
|
import { hotkeyDisplay } from 'lib/hotkeyWrapper'
|
||||||
import { COMMAND_PALETTE_HOTKEY } from './CommandBar/CommandBar'
|
import { COMMAND_PALETTE_HOTKEY } from './CommandBar/CommandBar'
|
||||||
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
|
import { EngineConnectionStateType } from 'lang/std/engineConnection'
|
||||||
|
|
||||||
export function CommandBarOpenButton() {
|
export function CommandBarOpenButton() {
|
||||||
const { commandBarSend } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
|
const { immediateState } = useNetworkContext()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
|
|
||||||
|
const isDisabled =
|
||||||
|
immediateState.type !== EngineConnectionStateType.ConnectionEstablished
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
disabled={isDisabled}
|
||||||
className="group rounded-full flex items-center justify-center gap-2 px-2 py-1 bg-primary/10 dark:bg-chalkboard-90 dark:backdrop-blur-sm border-primary hover:border-primary dark:border-chalkboard-50 dark:hover:border-inherit text-primary dark:text-inherit"
|
className="group rounded-full flex items-center justify-center gap-2 px-2 py-1 bg-primary/10 dark:bg-chalkboard-90 dark:backdrop-blur-sm border-primary hover:border-primary dark:border-chalkboard-50 dark:hover:border-inherit text-primary dark:text-inherit"
|
||||||
onClick={() => commandBarSend({ type: 'Open' })}
|
onClick={() => commandBarSend({ type: 'Open' })}
|
||||||
data-testid="command-bar-open-button"
|
data-testid="command-bar-open-button"
|
||||||
|
293
src/components/EngineStream.tsx
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
import { MouseEventHandler, useEffect, useRef } from 'react'
|
||||||
|
import { useAppState } from 'AppState'
|
||||||
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
|
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||||
|
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
|
||||||
|
import { btnName } from 'lib/cameraControls'
|
||||||
|
import { trap } from 'lib/trap'
|
||||||
|
import { sendSelectEventToEngine } from 'lib/selections'
|
||||||
|
import { kclManager, engineCommandManager } from 'lib/singletons'
|
||||||
|
import { EngineCommandManagerEvents } from 'lang/std/engineConnection'
|
||||||
|
import { useRouteLoaderData } from 'react-router-dom'
|
||||||
|
import { PATHS } from 'lib/paths'
|
||||||
|
import { IndexLoaderData } from 'lib/types'
|
||||||
|
import useEngineStreamContext, {
|
||||||
|
EngineStreamState,
|
||||||
|
EngineStreamTransition,
|
||||||
|
} from 'hooks/useEngineStreamContext'
|
||||||
|
import { REASONABLE_TIME_TO_REFRESH_STREAM_SIZE } from 'lib/timings'
|
||||||
|
|
||||||
|
export const EngineStream = () => {
|
||||||
|
const { setAppState } = useAppState()
|
||||||
|
|
||||||
|
const { overallState } = useNetworkContext()
|
||||||
|
const { settings } = useSettingsAuthContext()
|
||||||
|
const { file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||||
|
const last = useRef<number>(Date.now())
|
||||||
|
|
||||||
|
const settingsEngine = {
|
||||||
|
theme: settings.context.app.theme.current,
|
||||||
|
enableSSAO: settings.context.app.enableSSAO.current,
|
||||||
|
highlightEdges: settings.context.modeling.highlightEdges.current,
|
||||||
|
showScaleGrid: settings.context.modeling.showScaleGrid.current,
|
||||||
|
cameraProjection: settings.context.modeling.cameraProjection.current,
|
||||||
|
}
|
||||||
|
|
||||||
|
const { state: modelingMachineState, send: modelingMachineActorSend } =
|
||||||
|
useModelingContext()
|
||||||
|
|
||||||
|
const engineStreamActor = useEngineStreamContext.useActorRef()
|
||||||
|
const engineStreamState = engineStreamActor.getSnapshot()
|
||||||
|
|
||||||
|
const streamIdleMode = settings.context.app.streamIdleMode.current
|
||||||
|
|
||||||
|
const configure = () => {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.StartOrReconfigureEngine,
|
||||||
|
modelingMachineActorSend,
|
||||||
|
settings: settingsEngine,
|
||||||
|
setAppState,
|
||||||
|
|
||||||
|
// It's possible a reconnect happens as we drag the window :')
|
||||||
|
onMediaStream(mediaStream: MediaStream) {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.SetMediaStream,
|
||||||
|
mediaStream,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const play = () => {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.Play,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
engineCommandManager.addEventListener(
|
||||||
|
EngineCommandManagerEvents.SceneReady,
|
||||||
|
play
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
engineCommandManager.removeEventListener(
|
||||||
|
EngineCommandManagerEvents.SceneReady,
|
||||||
|
play
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const video = engineStreamState.context.videoRef?.current
|
||||||
|
if (!video) return
|
||||||
|
const canvas = engineStreamState.context.canvasRef?.current
|
||||||
|
if (!canvas) return
|
||||||
|
|
||||||
|
new ResizeObserver(() => {
|
||||||
|
if (Date.now() - last.current < REASONABLE_TIME_TO_REFRESH_STREAM_SIZE)
|
||||||
|
return
|
||||||
|
last.current = Date.now()
|
||||||
|
|
||||||
|
if (
|
||||||
|
Math.abs(video.width - window.innerWidth) > 4 ||
|
||||||
|
Math.abs(video.height - window.innerHeight) > 4
|
||||||
|
) {
|
||||||
|
timeoutStart.current = Date.now()
|
||||||
|
configure()
|
||||||
|
}
|
||||||
|
}).observe(document.body)
|
||||||
|
}, [engineStreamState.value])
|
||||||
|
|
||||||
|
// When the video and canvas element references are set, start the engine.
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
engineStreamState.context.canvasRef.current &&
|
||||||
|
engineStreamState.context.videoRef.current
|
||||||
|
) {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.StartOrReconfigureEngine,
|
||||||
|
modelingMachineActorSend,
|
||||||
|
settings: settingsEngine,
|
||||||
|
setAppState,
|
||||||
|
onMediaStream(mediaStream: MediaStream) {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.SetMediaStream,
|
||||||
|
mediaStream,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
engineStreamState.context.canvasRef.current,
|
||||||
|
engineStreamState.context.videoRef.current,
|
||||||
|
])
|
||||||
|
|
||||||
|
// On settings change, reconfigure the engine. When paused this gets really tricky,
|
||||||
|
// and also requires onMediaStream to be set!
|
||||||
|
useEffect(() => {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.StartOrReconfigureEngine,
|
||||||
|
modelingMachineActorSend,
|
||||||
|
settings: settingsEngine,
|
||||||
|
setAppState,
|
||||||
|
onMediaStream(mediaStream: MediaStream) {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.SetMediaStream,
|
||||||
|
mediaStream,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, [settings.context])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to execute code when the file changes
|
||||||
|
* but only if the scene is already ready.
|
||||||
|
* See onSceneReady for the initial scene setup.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (engineCommandManager.engineConnection?.isReady() && file?.path) {
|
||||||
|
console.log('execute on file change')
|
||||||
|
void kclManager.executeCode(true).catch(trap)
|
||||||
|
}
|
||||||
|
}, [file?.path, engineCommandManager.engineConnection])
|
||||||
|
|
||||||
|
const IDLE_TIME_MS = Number(streamIdleMode)
|
||||||
|
|
||||||
|
// When streamIdleMode is changed, setup or teardown the timeouts
|
||||||
|
const timeoutStart = useRef<number | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
timeoutStart.current = streamIdleMode ? Date.now() : null
|
||||||
|
}, [streamIdleMode])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let frameId: ReturnType<typeof window.requestAnimationFrame> = 0
|
||||||
|
const frameLoop = () => {
|
||||||
|
// Do not pause if the user is in the middle of an operation
|
||||||
|
if (!modelingMachineState.matches('idle')) {
|
||||||
|
// In fact, stop the timeout, because we don't want to trigger the
|
||||||
|
// pause when we exit the operation.
|
||||||
|
timeoutStart.current = null
|
||||||
|
} else if (timeoutStart.current) {
|
||||||
|
const elapsed = Date.now() - timeoutStart.current
|
||||||
|
if (elapsed >= IDLE_TIME_MS) {
|
||||||
|
timeoutStart.current = null
|
||||||
|
engineStreamActor.send({ type: EngineStreamTransition.Pause })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
frameId = window.requestAnimationFrame(frameLoop)
|
||||||
|
}
|
||||||
|
frameId = window.requestAnimationFrame(frameLoop)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.cancelAnimationFrame(frameId)
|
||||||
|
}
|
||||||
|
}, [modelingMachineState])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!streamIdleMode) return
|
||||||
|
|
||||||
|
const onAnyInput = () => {
|
||||||
|
// Just in case it happens in the middle of the user turning off
|
||||||
|
// idle mode.
|
||||||
|
if (!streamIdleMode) {
|
||||||
|
timeoutStart.current = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (engineStreamState.value === EngineStreamState.Paused) {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.StartOrReconfigureEngine,
|
||||||
|
modelingMachineActorSend,
|
||||||
|
settings: settingsEngine,
|
||||||
|
setAppState,
|
||||||
|
onMediaStream(mediaStream: MediaStream) {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.SetMediaStream,
|
||||||
|
mediaStream,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutStart.current = Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's possible after a reconnect, the user doesn't move their mouse at
|
||||||
|
// all, meaning the timer is not reset to run. We need to set it every
|
||||||
|
// time our effect dependencies change then.
|
||||||
|
timeoutStart.current = Date.now()
|
||||||
|
|
||||||
|
window.document.addEventListener('keydown', onAnyInput)
|
||||||
|
window.document.addEventListener('keyup', onAnyInput)
|
||||||
|
window.document.addEventListener('mousemove', onAnyInput)
|
||||||
|
window.document.addEventListener('mousedown', onAnyInput)
|
||||||
|
window.document.addEventListener('mouseup', onAnyInput)
|
||||||
|
window.document.addEventListener('scroll', onAnyInput)
|
||||||
|
window.document.addEventListener('touchstart', onAnyInput)
|
||||||
|
window.document.addEventListener('touchstop', onAnyInput)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
timeoutStart.current = null
|
||||||
|
window.document.removeEventListener('keydown', onAnyInput)
|
||||||
|
window.document.removeEventListener('keyup', onAnyInput)
|
||||||
|
window.document.removeEventListener('mousemove', onAnyInput)
|
||||||
|
window.document.removeEventListener('mousedown', onAnyInput)
|
||||||
|
window.document.removeEventListener('mouseup', onAnyInput)
|
||||||
|
window.document.removeEventListener('scroll', onAnyInput)
|
||||||
|
window.document.removeEventListener('touchstart', onAnyInput)
|
||||||
|
window.document.removeEventListener('touchstop', onAnyInput)
|
||||||
|
}
|
||||||
|
}, [streamIdleMode, engineStreamState.value])
|
||||||
|
|
||||||
|
const isNetworkOkay =
|
||||||
|
overallState === NetworkHealthState.Ok ||
|
||||||
|
overallState === NetworkHealthState.Weak
|
||||||
|
|
||||||
|
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||||
|
if (!isNetworkOkay) return
|
||||||
|
if (!engineStreamState.context.videoRef.current) return
|
||||||
|
if (modelingMachineState.matches('Sketch')) return
|
||||||
|
if (modelingMachineState.matches({ idle: 'showPlanes' })) return
|
||||||
|
|
||||||
|
if (btnName(e.nativeEvent).left) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
sendSelectEventToEngine(e, engineStreamState.context.videoRef.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 z-0"
|
||||||
|
id="stream"
|
||||||
|
data-testid="stream"
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
|
onContextMenuCapture={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
autoPlay
|
||||||
|
muted
|
||||||
|
key={engineStreamActor.id + 'video'}
|
||||||
|
ref={engineStreamState.context.videoRef}
|
||||||
|
controls={false}
|
||||||
|
className="cursor-pointer"
|
||||||
|
disablePictureInPicture
|
||||||
|
id="video-stream"
|
||||||
|
/>
|
||||||
|
<canvas
|
||||||
|
key={engineStreamActor.id + 'canvas'}
|
||||||
|
ref={engineStreamState.context.canvasRef}
|
||||||
|
className="cursor-pointer"
|
||||||
|
id="freeze-frame"
|
||||||
|
>
|
||||||
|
No canvas support
|
||||||
|
</canvas>
|
||||||
|
<ClientSideScene
|
||||||
|
cameraControls={settings.context.modeling.mouseControls.current}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -22,6 +22,7 @@ import usePlatform from 'hooks/usePlatform'
|
|||||||
import { FileEntry } from 'lib/project'
|
import { FileEntry } from 'lib/project'
|
||||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||||
import { normalizeLineEndings } from 'lib/codeEditor'
|
import { normalizeLineEndings } from 'lib/codeEditor'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
function getIndentationCSS(level: number) {
|
function getIndentationCSS(level: number) {
|
||||||
return `calc(1rem * ${level + 1})`
|
return `calc(1rem * ${level + 1})`
|
||||||
@ -196,8 +197,7 @@ const FileTreeItem = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't try to read a file that was removed.
|
if (isCurrentFile && eventType === 'change') {
|
||||||
if (isCurrentFile && eventType !== 'unlink') {
|
|
||||||
let code = await window.electron.readFile(path, { encoding: 'utf-8' })
|
let code = await window.electron.readFile(path, { encoding: 'utf-8' })
|
||||||
code = normalizeLineEndings(code)
|
code = normalizeLineEndings(code)
|
||||||
codeManager.updateCodeStateEditor(code)
|
codeManager.updateCodeStateEditor(code)
|
||||||
@ -242,7 +242,7 @@ const FileTreeItem = ({
|
|||||||
// Show the renaming form
|
// Show the renaming form
|
||||||
addCurrentItemToRenaming()
|
addCurrentItemToRenaming()
|
||||||
} else if (e.code === 'Space') {
|
} else if (e.code === 'Space') {
|
||||||
void handleClick()
|
void handleClick().catch(reportRejection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,7 +293,7 @@ const FileTreeItem = ({
|
|||||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.currentTarget.focus()
|
e.currentTarget.focus()
|
||||||
void handleClick()
|
void handleClick().catch(reportRejection)
|
||||||
}}
|
}}
|
||||||
onKeyUp={handleKeyUp}
|
onKeyUp={handleKeyUp}
|
||||||
>
|
>
|
||||||
|
@ -1,40 +1,47 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
import { useEngineCommands } from './EngineCommands'
|
import { useEngineCommands } from './EngineCommands'
|
||||||
import { Spinner } from './Spinner'
|
|
||||||
import { CustomIcon } from './CustomIcon'
|
import { CustomIcon } from './CustomIcon'
|
||||||
|
import useEngineStreamContext, {
|
||||||
|
EngineStreamState,
|
||||||
|
} from 'hooks/useEngineStreamContext'
|
||||||
|
import { CommandLogType } from 'lang/std/engineConnection'
|
||||||
|
|
||||||
export const ModelStateIndicator = () => {
|
export const ModelStateIndicator = () => {
|
||||||
const [commands] = useEngineCommands()
|
const [commands] = useEngineCommands()
|
||||||
|
const [isDone, setIsDone] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const engineStreamActor = useEngineStreamContext.useActorRef()
|
||||||
|
const engineStreamState = engineStreamActor.getSnapshot()
|
||||||
|
|
||||||
const lastCommandType = commands[commands.length - 1]?.type
|
const lastCommandType = commands[commands.length - 1]?.type
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (lastCommandType === CommandLogType.SetDefaultSystemProperties) {
|
||||||
|
setIsDone(false)
|
||||||
|
}
|
||||||
|
if (lastCommandType === CommandLogType.ExecutionDone) {
|
||||||
|
setIsDone(true)
|
||||||
|
}
|
||||||
|
}, [lastCommandType])
|
||||||
|
|
||||||
let className = 'w-6 h-6 '
|
let className = 'w-6 h-6 '
|
||||||
let icon = <Spinner className={className} />
|
let icon = <div className={className}></div>
|
||||||
let dataTestId = 'model-state-indicator'
|
let dataTestId = 'model-state-indicator'
|
||||||
|
|
||||||
if (lastCommandType === 'receive-reliable') {
|
if (engineStreamState.value === EngineStreamState.Paused) {
|
||||||
className +=
|
className += 'text-secondary'
|
||||||
'bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed'
|
icon = <CustomIcon data-testid={dataTestId + '-paused'} name="parallel" />
|
||||||
icon = (
|
} else if (engineStreamState.value === EngineStreamState.Resuming) {
|
||||||
<CustomIcon
|
className += 'text-secondary'
|
||||||
data-testid={dataTestId + '-receive-reliable'}
|
icon = <CustomIcon data-testid={dataTestId + '-resuming'} name="parallel" />
|
||||||
name="checkmark"
|
} else if (isDone) {
|
||||||
/>
|
className += 'text-secondary'
|
||||||
)
|
|
||||||
} else if (lastCommandType === 'execution-done') {
|
|
||||||
className +=
|
|
||||||
'border-6 border border-solid border-chalkboard-60 dark:border-chalkboard-80 bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed'
|
|
||||||
icon = (
|
icon = (
|
||||||
<CustomIcon
|
<CustomIcon
|
||||||
data-testid={dataTestId + '-execution-done'}
|
data-testid={dataTestId + '-execution-done'}
|
||||||
name="checkmark"
|
name="checkmark"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
} else if (lastCommandType === 'export-done') {
|
|
||||||
className +=
|
|
||||||
'border-6 border border-solid border-chalkboard-60 dark:border-chalkboard-80 bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed'
|
|
||||||
icon = (
|
|
||||||
<CustomIcon data-testid={dataTestId + '-export-done'} name="checkmark" />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -20,7 +20,6 @@ import {
|
|||||||
modelingMachine,
|
modelingMachine,
|
||||||
modelingMachineDefaultContext,
|
modelingMachineDefaultContext,
|
||||||
} from 'machines/modelingMachine'
|
} from 'machines/modelingMachine'
|
||||||
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
|
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import {
|
import {
|
||||||
isCursorInSketchCommandRange,
|
isCursorInSketchCommandRange,
|
||||||
@ -112,13 +111,8 @@ export const ModelingMachineProvider = ({
|
|||||||
auth,
|
auth,
|
||||||
settings: {
|
settings: {
|
||||||
context: {
|
context: {
|
||||||
app: { theme, enableSSAO },
|
app: { theme },
|
||||||
modeling: {
|
modeling: { defaultUnit, highlightEdges, cameraProjection },
|
||||||
defaultUnit,
|
|
||||||
cameraProjection,
|
|
||||||
highlightEdges,
|
|
||||||
showScaleGrid,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} = useSettingsAuthContext()
|
} = useSettingsAuthContext()
|
||||||
@ -129,9 +123,6 @@ export const ModelingMachineProvider = ({
|
|||||||
const streamRef = useRef<HTMLDivElement>(null)
|
const streamRef = useRef<HTMLDivElement>(null)
|
||||||
const persistedContext = useMemo(() => getPersistedContext(), [])
|
const persistedContext = useMemo(() => getPersistedContext(), [])
|
||||||
|
|
||||||
let [searchParams] = useSearchParams()
|
|
||||||
const pool = searchParams.get('pool')
|
|
||||||
|
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
|
|
||||||
// Settings machine setup
|
// Settings machine setup
|
||||||
@ -304,6 +295,7 @@ export const ModelingMachineProvider = ({
|
|||||||
const dispatchSelection = (selection?: EditorSelection) => {
|
const dispatchSelection = (selection?: EditorSelection) => {
|
||||||
if (!selection) return // TODO less of hack for the below please
|
if (!selection) return // TODO less of hack for the below please
|
||||||
if (!editorManager.editorView) return
|
if (!editorManager.editorView) return
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!editorManager.editorView) return
|
if (!editorManager.editorView) return
|
||||||
editorManager.editorView.dispatch({
|
editorManager.editorView.dispatch({
|
||||||
@ -657,6 +649,9 @@ export const ModelingMachineProvider = ({
|
|||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
input.faceId
|
input.faceId
|
||||||
)
|
)
|
||||||
|
await sceneInfra.camControls.centerModelRelativeToPanes({
|
||||||
|
resetLastPaneWidth: true,
|
||||||
|
})
|
||||||
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||||||
return {
|
return {
|
||||||
sketchPathToNode: pathToNewSketchNode,
|
sketchPathToNode: pathToNewSketchNode,
|
||||||
@ -677,6 +672,9 @@ export const ModelingMachineProvider = ({
|
|||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
input.planeId
|
input.planeId
|
||||||
)
|
)
|
||||||
|
await sceneInfra.camControls.centerModelRelativeToPanes({
|
||||||
|
resetLastPaneWidth: true,
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sketchPathToNode: pathToNode,
|
sketchPathToNode: pathToNode,
|
||||||
@ -699,6 +697,9 @@ export const ModelingMachineProvider = ({
|
|||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
info?.sketchDetails?.faceId || ''
|
info?.sketchDetails?.faceId || ''
|
||||||
)
|
)
|
||||||
|
await sceneInfra.camControls.centerModelRelativeToPanes({
|
||||||
|
resetLastPaneWidth: true,
|
||||||
|
})
|
||||||
return {
|
return {
|
||||||
sketchPathToNode: sketchPathToNode || [],
|
sketchPathToNode: sketchPathToNode || [],
|
||||||
zAxis: info.sketchDetails.zAxis || null,
|
zAxis: info.sketchDetails.zAxis || null,
|
||||||
@ -732,6 +733,11 @@ export const ModelingMachineProvider = ({
|
|||||||
sketchDetails.origin
|
sketchDetails.origin
|
||||||
)
|
)
|
||||||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||||
|
updatedAst.newAst
|
||||||
|
)
|
||||||
|
|
||||||
const selection = updateSelections(
|
const selection = updateSelections(
|
||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -768,6 +774,11 @@ export const ModelingMachineProvider = ({
|
|||||||
sketchDetails.origin
|
sketchDetails.origin
|
||||||
)
|
)
|
||||||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||||
|
updatedAst.newAst
|
||||||
|
)
|
||||||
|
|
||||||
const selection = updateSelections(
|
const selection = updateSelections(
|
||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -813,6 +824,11 @@ export const ModelingMachineProvider = ({
|
|||||||
sketchDetails.origin
|
sketchDetails.origin
|
||||||
)
|
)
|
||||||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||||
|
updatedAst.newAst
|
||||||
|
)
|
||||||
|
|
||||||
const selection = updateSelections(
|
const selection = updateSelections(
|
||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -846,6 +862,11 @@ export const ModelingMachineProvider = ({
|
|||||||
sketchDetails.origin
|
sketchDetails.origin
|
||||||
)
|
)
|
||||||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||||
|
updatedAst.newAst
|
||||||
|
)
|
||||||
|
|
||||||
const selection = updateSelections(
|
const selection = updateSelections(
|
||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -881,6 +902,11 @@ export const ModelingMachineProvider = ({
|
|||||||
sketchDetails.origin
|
sketchDetails.origin
|
||||||
)
|
)
|
||||||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||||
|
updatedAst.newAst
|
||||||
|
)
|
||||||
|
|
||||||
const selection = updateSelections(
|
const selection = updateSelections(
|
||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -917,6 +943,11 @@ export const ModelingMachineProvider = ({
|
|||||||
sketchDetails.origin
|
sketchDetails.origin
|
||||||
)
|
)
|
||||||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||||
|
updatedAst.newAst
|
||||||
|
)
|
||||||
|
|
||||||
const selection = updateSelections(
|
const selection = updateSelections(
|
||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -953,6 +984,11 @@ export const ModelingMachineProvider = ({
|
|||||||
sketchDetails.origin
|
sketchDetails.origin
|
||||||
)
|
)
|
||||||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||||
|
updatedAst.newAst
|
||||||
|
)
|
||||||
|
|
||||||
const selection = updateSelections(
|
const selection = updateSelections(
|
||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -999,6 +1035,11 @@ export const ModelingMachineProvider = ({
|
|||||||
sketchDetails.origin
|
sketchDetails.origin
|
||||||
)
|
)
|
||||||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||||
|
updatedAst.newAst
|
||||||
|
)
|
||||||
|
|
||||||
const selection = updateSelections(
|
const selection = updateSelections(
|
||||||
{ 0: pathToReplacedNode },
|
{ 0: pathToReplacedNode },
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -1027,21 +1068,6 @@ export const ModelingMachineProvider = ({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
useSetupEngineManager(
|
|
||||||
streamRef,
|
|
||||||
modelingSend,
|
|
||||||
modelingState.context,
|
|
||||||
{
|
|
||||||
pool: pool,
|
|
||||||
theme: theme.current,
|
|
||||||
highlightEdges: highlightEdges.current,
|
|
||||||
enableSSAO: enableSSAO.current,
|
|
||||||
showScaleGrid: showScaleGrid.current,
|
|
||||||
cameraProjection: cameraProjection.current,
|
|
||||||
},
|
|
||||||
token
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
kclManager.registerExecuteCallback(() => {
|
kclManager.registerExecuteCallback(() => {
|
||||||
modelingSend({ type: 'Re-execute' })
|
modelingSend({ type: 'Re-execute' })
|
||||||
|
@ -89,9 +89,9 @@ export const processMemory = (programMemory: ProgramMemory) => {
|
|||||||
const processedMemory: any = {}
|
const processedMemory: any = {}
|
||||||
for (const [key, val] of programMemory?.visibleEntries()) {
|
for (const [key, val] of programMemory?.visibleEntries()) {
|
||||||
if (
|
if (
|
||||||
(val.type === 'UserVal' && val.value.type === 'Sketch') ||
|
val.type === 'Sketch' ||
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
(val.type !== 'Function' && val.type !== 'UserVal')
|
val.type !== 'Function'
|
||||||
) {
|
) {
|
||||||
const sg = sketchFromKclValue(val, key)
|
const sg = sketchFromKclValue(val, key)
|
||||||
if (val.type === 'Solid') {
|
if (val.type === 'Solid') {
|
||||||
@ -110,8 +110,6 @@ export const processMemory = (programMemory: ProgramMemory) => {
|
|||||||
processedMemory[key] = `__function(${(val as any)?.expression?.params
|
processedMemory[key] = `__function(${(val as any)?.expression?.params
|
||||||
?.map?.(({ identifier }: any) => identifier?.name || '')
|
?.map?.(({ identifier }: any) => identifier?.name || '')
|
||||||
.join(', ')})__`
|
.join(', ')})__`
|
||||||
} else {
|
|
||||||
processedMemory[key] = val.value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return processedMemory
|
return processedMemory
|
||||||
|
@ -6,6 +6,11 @@ import {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useContext,
|
useContext,
|
||||||
|
MutableRefObject,
|
||||||
|
forwardRef,
|
||||||
|
// https://stackoverflow.com/a/77055468 Thank you.
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { SidebarAction, SidebarType, sidebarPanes } from './ModelingPanes'
|
import { SidebarAction, SidebarType, sidebarPanes } from './ModelingPanes'
|
||||||
@ -19,9 +24,12 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
|
|||||||
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { useKclContext } from 'lang/KclProvider'
|
import { useKclContext } from 'lang/KclProvider'
|
||||||
import { MachineManagerContext } from 'components/MachineManagerProvider'
|
import { MachineManagerContext } from 'components/MachineManagerProvider'
|
||||||
|
import { sceneInfra } from 'lib/singletons'
|
||||||
|
import { REASONABLE_TIME_TO_REFRESH_STREAM_SIZE } from 'lib/timings'
|
||||||
|
|
||||||
interface ModelingSidebarProps {
|
interface ModelingSidebarProps {
|
||||||
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
||||||
|
ref: MutableRefObject<HTMLDivElement>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BadgeInfoComputed {
|
interface BadgeInfoComputed {
|
||||||
@ -33,19 +41,34 @@ function getPlatformString(): 'web' | 'desktop' {
|
|||||||
return isDesktop() ? 'desktop' : 'web'
|
return isDesktop() ? 'desktop' : 'web'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
export const ModelingSidebar = forwardRef<
|
||||||
|
HTMLUListElement,
|
||||||
|
ModelingSidebarProps
|
||||||
|
>(function ModelingSidebar({ paneOpacity }, outerRef) {
|
||||||
const machineManager = useContext(MachineManagerContext)
|
const machineManager = useContext(MachineManagerContext)
|
||||||
const { commandBarSend } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
const kclContext = useKclContext()
|
const kclContext = useKclContext()
|
||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const onboardingStatus = settings.context.app.onboardingStatus
|
const onboardingStatus = settings.context.app.onboardingStatus
|
||||||
const { send, context } = useModelingContext()
|
const { send, state, context } = useModelingContext()
|
||||||
const pointerEventsCssClass =
|
const pointerEventsCssClass =
|
||||||
onboardingStatus.current === 'camera' ||
|
onboardingStatus.current === 'camera' ||
|
||||||
context.store?.openPanes.length === 0
|
context.store?.openPanes.length === 0
|
||||||
? 'pointer-events-none '
|
? 'pointer-events-none '
|
||||||
: 'pointer-events-auto '
|
: 'pointer-events-auto '
|
||||||
const showDebugPanel = settings.context.modeling.showDebugPanel
|
const showDebugPanel = settings.context.modeling.showDebugPanel
|
||||||
|
const innerRef = useRef<HTMLUListElement>(null)
|
||||||
|
|
||||||
|
// forwardRef's type causes me to do this type narrowing.
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof outerRef === 'function') {
|
||||||
|
outerRef(innerRef.current)
|
||||||
|
} else {
|
||||||
|
if (outerRef) {
|
||||||
|
outerRef.current = innerRef.current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [innerRef.current])
|
||||||
|
|
||||||
const paneCallbackProps = useMemo(
|
const paneCallbackProps = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@ -159,8 +182,37 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
[context.store?.openPanes, send]
|
[context.store?.openPanes, send]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Don't send camera adjustment commands after 1 pane is open. It
|
||||||
|
// won't make any difference.
|
||||||
|
if (context.store?.openPanes.length > 1) return
|
||||||
|
|
||||||
|
void sceneInfra.camControls.centerModelRelativeToPanes()
|
||||||
|
}, [context.store?.openPanes])
|
||||||
|
|
||||||
|
// If the panes are resized then center the model also
|
||||||
|
useEffect(() => {
|
||||||
|
if (!innerRef.current) return
|
||||||
|
|
||||||
|
let last = Date.now()
|
||||||
|
const observer = new ResizeObserver(() => {
|
||||||
|
if (Date.now() - last < REASONABLE_TIME_TO_REFRESH_STREAM_SIZE) return
|
||||||
|
if (!innerRef.current) return
|
||||||
|
|
||||||
|
last = Date.now()
|
||||||
|
void sceneInfra.camControls.centerModelRelativeToPanes()
|
||||||
|
})
|
||||||
|
|
||||||
|
observer.observe(innerRef.current)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}, [state, innerRef.current])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Resizable
|
<Resizable
|
||||||
|
data-testid="modeling-sidebar"
|
||||||
className={`group flex-1 flex flex-col z-10 my-2 pr-1 ${paneOpacity} ${pointerEventsCssClass}`}
|
className={`group flex-1 flex flex-col z-10 my-2 pr-1 ${paneOpacity} ${pointerEventsCssClass}`}
|
||||||
defaultSize={{
|
defaultSize={{
|
||||||
width: '550px',
|
width: '550px',
|
||||||
@ -192,6 +244,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
>
|
>
|
||||||
<ul
|
<ul
|
||||||
id="pane-buttons-section"
|
id="pane-buttons-section"
|
||||||
|
data-testid="pane-buttons-section"
|
||||||
className={
|
className={
|
||||||
'w-fit p-2 flex flex-col gap-2 ' +
|
'w-fit p-2 flex flex-col gap-2 ' +
|
||||||
(context.store?.openPanes.length >= 1 ? 'pr-0.5' : '')
|
(context.store?.openPanes.length >= 1 ? 'pr-0.5' : '')
|
||||||
@ -236,6 +289,8 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
</ul>
|
</ul>
|
||||||
<ul
|
<ul
|
||||||
id="pane-section"
|
id="pane-section"
|
||||||
|
data-testid="pane-section"
|
||||||
|
ref={innerRef}
|
||||||
className={
|
className={
|
||||||
'ml-[-1px] col-start-2 col-span-1 flex flex-col items-stretch gap-2 ' +
|
'ml-[-1px] col-start-2 col-span-1 flex flex-col items-stretch gap-2 ' +
|
||||||
(context.store?.openPanes.length >= 1 ? `w-full` : `hidden`)
|
(context.store?.openPanes.length >= 1 ? `w-full` : `hidden`)
|
||||||
@ -265,7 +320,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
</div>
|
</div>
|
||||||
</Resizable>
|
</Resizable>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
interface ModelingPaneButtonProps
|
interface ModelingPaneButtonProps
|
||||||
extends React.HTMLAttributes<HTMLButtonElement> {
|
extends React.HTMLAttributes<HTMLButtonElement> {
|
||||||
|
@ -1,340 +0,0 @@
|
|||||||
import { MouseEventHandler, useEffect, useRef, useState } from 'react'
|
|
||||||
import Loading from './Loading'
|
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
|
||||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
|
||||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
|
||||||
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
|
|
||||||
import { btnName } from 'lib/cameraControls'
|
|
||||||
import { sendSelectEventToEngine } from 'lib/selections'
|
|
||||||
import { kclManager, engineCommandManager, sceneInfra } from 'lib/singletons'
|
|
||||||
import { useAppStream } from 'AppState'
|
|
||||||
import {
|
|
||||||
EngineCommandManagerEvents,
|
|
||||||
EngineConnectionStateType,
|
|
||||||
DisconnectingType,
|
|
||||||
} from 'lang/std/engineConnection'
|
|
||||||
import { useRouteLoaderData } from 'react-router-dom'
|
|
||||||
import { PATHS } from 'lib/paths'
|
|
||||||
import { IndexLoaderData } from 'lib/types'
|
|
||||||
|
|
||||||
enum StreamState {
|
|
||||||
Playing = 'playing',
|
|
||||||
Paused = 'paused',
|
|
||||||
Resuming = 'resuming',
|
|
||||||
Unset = 'unset',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Stream = () => {
|
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
|
||||||
const videoRef = useRef<HTMLVideoElement>(null)
|
|
||||||
const { settings } = useSettingsAuthContext()
|
|
||||||
const { state, send } = useModelingContext()
|
|
||||||
const { mediaStream } = useAppStream()
|
|
||||||
const { overallState, immediateState } = useNetworkContext()
|
|
||||||
const [streamState, setStreamState] = useState(StreamState.Unset)
|
|
||||||
const { file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
|
||||||
|
|
||||||
const IDLE = settings.context.app.streamIdleMode.current
|
|
||||||
|
|
||||||
const isNetworkOkay =
|
|
||||||
overallState === NetworkHealthState.Ok ||
|
|
||||||
overallState === NetworkHealthState.Weak
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute code and show a "building scene message"
|
|
||||||
* in Stream.tsx in the meantime.
|
|
||||||
*
|
|
||||||
* I would like for this to live somewhere more central,
|
|
||||||
* but it seems to me that we need the video element ref
|
|
||||||
* to be able to play the video after the code has been
|
|
||||||
* executed. If we can find a way to do this from a more
|
|
||||||
* central place, we can move this code there.
|
|
||||||
*/
|
|
||||||
function executeCodeAndPlayStream() {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
kclManager.executeCode(true).then(async () => {
|
|
||||||
await videoRef.current?.play().catch((e) => {
|
|
||||||
console.warn('Video playing was prevented', e, videoRef.current)
|
|
||||||
})
|
|
||||||
setStreamState(StreamState.Playing)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribe to execute code when the file changes
|
|
||||||
* but only if the scene is already ready.
|
|
||||||
* See onSceneReady for the initial scene setup.
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (engineCommandManager.engineConnection?.isReady() && file?.path) {
|
|
||||||
console.log('execute on file change')
|
|
||||||
executeCodeAndPlayStream()
|
|
||||||
}
|
|
||||||
}, [file?.path, engineCommandManager.engineConnection])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
immediateState.type === EngineConnectionStateType.Disconnecting &&
|
|
||||||
immediateState.value.type === DisconnectingType.Pause
|
|
||||||
) {
|
|
||||||
setStreamState(StreamState.Paused)
|
|
||||||
}
|
|
||||||
}, [immediateState])
|
|
||||||
|
|
||||||
// Linux has a default behavior to paste text on middle mouse up
|
|
||||||
// This adds a listener to block that pasting if the click target
|
|
||||||
// is not a text input, so users can move in the 3D scene with
|
|
||||||
// middle mouse drag with a text input focused without pasting.
|
|
||||||
useEffect(() => {
|
|
||||||
const handlePaste = (e: ClipboardEvent) => {
|
|
||||||
const isHtmlElement = e.target && e.target instanceof HTMLElement
|
|
||||||
const isEditable =
|
|
||||||
(isHtmlElement && !('explicitOriginalTarget' in e)) ||
|
|
||||||
('explicitOriginalTarget' in e &&
|
|
||||||
((e.explicitOriginalTarget as HTMLElement).contentEditable ===
|
|
||||||
'true' ||
|
|
||||||
['INPUT', 'TEXTAREA'].some(
|
|
||||||
(tagName) =>
|
|
||||||
tagName === (e.explicitOriginalTarget as HTMLElement).tagName
|
|
||||||
)))
|
|
||||||
if (!isEditable) {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
e.stopImmediatePropagation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
globalThis?.window?.document?.addEventListener('paste', handlePaste, {
|
|
||||||
capture: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const IDLE_TIME_MS = 1000 * 60 * 2
|
|
||||||
let timeoutIdIdleA: ReturnType<typeof setTimeout> | undefined = undefined
|
|
||||||
|
|
||||||
const teardown = () => {
|
|
||||||
// Already paused
|
|
||||||
if (streamState === StreamState.Paused) return
|
|
||||||
|
|
||||||
videoRef.current?.pause()
|
|
||||||
setStreamState(StreamState.Paused)
|
|
||||||
sceneInfra.modelingSend({ type: 'Cancel' })
|
|
||||||
// Give video time to pause
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
engineCommandManager.tearDown({ idleMode: true })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const onVisibilityChange = () => {
|
|
||||||
if (globalThis.window.document.visibilityState === 'hidden') {
|
|
||||||
clearTimeout(timeoutIdIdleA)
|
|
||||||
timeoutIdIdleA = setTimeout(teardown, IDLE_TIME_MS)
|
|
||||||
} else if (!engineCommandManager.engineConnection?.isReady()) {
|
|
||||||
clearTimeout(timeoutIdIdleA)
|
|
||||||
setStreamState(StreamState.Resuming)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Teardown everything if we go hidden or reconnect
|
|
||||||
if (IDLE) {
|
|
||||||
globalThis?.window?.document?.addEventListener(
|
|
||||||
'visibilitychange',
|
|
||||||
onVisibilityChange
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let timeoutIdIdleB: ReturnType<typeof setTimeout> | undefined = undefined
|
|
||||||
|
|
||||||
const onAnyInput = () => {
|
|
||||||
if (streamState === StreamState.Playing) {
|
|
||||||
// Clear both timers
|
|
||||||
clearTimeout(timeoutIdIdleA)
|
|
||||||
clearTimeout(timeoutIdIdleB)
|
|
||||||
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
|
|
||||||
}
|
|
||||||
if (streamState === StreamState.Paused) {
|
|
||||||
setStreamState(StreamState.Resuming)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IDLE) {
|
|
||||||
globalThis?.window?.document?.addEventListener('keydown', onAnyInput)
|
|
||||||
globalThis?.window?.document?.addEventListener('mousemove', onAnyInput)
|
|
||||||
globalThis?.window?.document?.addEventListener('mousedown', onAnyInput)
|
|
||||||
globalThis?.window?.document?.addEventListener('scroll', onAnyInput)
|
|
||||||
globalThis?.window?.document?.addEventListener('touchstart', onAnyInput)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IDLE) {
|
|
||||||
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a listener to execute code and play the stream
|
|
||||||
* on initial stream setup.
|
|
||||||
*/
|
|
||||||
engineCommandManager.addEventListener(
|
|
||||||
EngineCommandManagerEvents.SceneReady,
|
|
||||||
executeCodeAndPlayStream
|
|
||||||
)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
engineCommandManager.removeEventListener(
|
|
||||||
EngineCommandManagerEvents.SceneReady,
|
|
||||||
executeCodeAndPlayStream
|
|
||||||
)
|
|
||||||
globalThis?.window?.document?.removeEventListener('paste', handlePaste, {
|
|
||||||
capture: true,
|
|
||||||
})
|
|
||||||
if (IDLE) {
|
|
||||||
clearTimeout(timeoutIdIdleA)
|
|
||||||
clearTimeout(timeoutIdIdleB)
|
|
||||||
|
|
||||||
globalThis?.window?.document?.removeEventListener(
|
|
||||||
'visibilitychange',
|
|
||||||
onVisibilityChange
|
|
||||||
)
|
|
||||||
globalThis?.window?.document?.removeEventListener('keydown', onAnyInput)
|
|
||||||
globalThis?.window?.document?.removeEventListener(
|
|
||||||
'mousemove',
|
|
||||||
onAnyInput
|
|
||||||
)
|
|
||||||
globalThis?.window?.document?.removeEventListener(
|
|
||||||
'mousedown',
|
|
||||||
onAnyInput
|
|
||||||
)
|
|
||||||
globalThis?.window?.document?.removeEventListener('scroll', onAnyInput)
|
|
||||||
globalThis?.window?.document?.removeEventListener(
|
|
||||||
'touchstart',
|
|
||||||
onAnyInput
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [IDLE, streamState])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Play the vid
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (!kclManager.isExecuting) {
|
|
||||||
setTimeout(() => {
|
|
||||||
// execute in the next event loop
|
|
||||||
videoRef.current?.play().catch((e) => {
|
|
||||||
console.warn('Video playing was prevented', e, videoRef.current)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [kclManager.isExecuting])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
typeof window === 'undefined' ||
|
|
||||||
typeof RTCPeerConnection === 'undefined'
|
|
||||||
)
|
|
||||||
return
|
|
||||||
if (!videoRef.current) return
|
|
||||||
if (!mediaStream) return
|
|
||||||
|
|
||||||
// The browser complains if we try to load a new stream without pausing first.
|
|
||||||
// Do not immediately play the stream!
|
|
||||||
try {
|
|
||||||
videoRef.current.srcObject = mediaStream
|
|
||||||
videoRef.current.pause()
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Attempted to pause stream while play was still loading', e)
|
|
||||||
}
|
|
||||||
|
|
||||||
send({
|
|
||||||
type: 'Set context',
|
|
||||||
data: {
|
|
||||||
videoElement: videoRef.current,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
setIsLoading(false)
|
|
||||||
}, [mediaStream])
|
|
||||||
|
|
||||||
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
|
|
||||||
// If we've got no stream or connection, don't do anything
|
|
||||||
if (!isNetworkOkay) return
|
|
||||||
if (!videoRef.current) return
|
|
||||||
// If we're in sketch mode, don't send a engine-side select event
|
|
||||||
if (state.matches('Sketch')) return
|
|
||||||
if (state.matches({ idle: 'showPlanes' })) return
|
|
||||||
// If we're mousing up from a camera drag, don't send a select event
|
|
||||||
if (sceneInfra.camControls.wasDragging === true) return
|
|
||||||
|
|
||||||
if (btnName(e.nativeEvent).left) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
sendSelectEventToEngine(e, videoRef.current)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 z-0"
|
|
||||||
id="stream"
|
|
||||||
data-testid="stream"
|
|
||||||
onClick={handleMouseUp}
|
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
|
||||||
onContextMenuCapture={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
<video
|
|
||||||
ref={videoRef}
|
|
||||||
muted
|
|
||||||
autoPlay
|
|
||||||
controls={false}
|
|
||||||
onPlay={() => setIsLoading(false)}
|
|
||||||
className="w-full cursor-pointer h-full"
|
|
||||||
disablePictureInPicture
|
|
||||||
id="video-stream"
|
|
||||||
/>
|
|
||||||
<ClientSideScene
|
|
||||||
cameraControls={settings.context.modeling.mouseControls.current}
|
|
||||||
/>
|
|
||||||
{(streamState === StreamState.Paused ||
|
|
||||||
streamState === StreamState.Resuming) && (
|
|
||||||
<div className="text-center absolute inset-0">
|
|
||||||
<div
|
|
||||||
className="flex flex-col items-center justify-center h-screen"
|
|
||||||
data-testid="paused"
|
|
||||||
>
|
|
||||||
<div className="border-primary border p-2 rounded-sm">
|
|
||||||
<svg
|
|
||||||
width="8"
|
|
||||||
height="12"
|
|
||||||
viewBox="0 0 8 12"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M2 12V0H0V12H2ZM8 12V0H6V12H8Z"
|
|
||||||
fill="var(--primary)"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<p className="text-base mt-2 text-primary bold">
|
|
||||||
{streamState === StreamState.Paused && 'Paused'}
|
|
||||||
{streamState === StreamState.Resuming && 'Resuming'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{(!isNetworkOkay || isLoading) && (
|
|
||||||
<div className="text-center absolute inset-0">
|
|
||||||
<Loading>
|
|
||||||
{!isNetworkOkay && !isLoading ? (
|
|
||||||
<span data-testid="loading-stream">Stream disconnected...</span>
|
|
||||||
) : (
|
|
||||||
!isLoading && (
|
|
||||||
<span data-testid="loading-stream">Loading stream...</span>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</Loading>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
81
src/editor/manager.test.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { editorManager } from 'lib/singletons'
|
||||||
|
import { Diagnostic } from '@codemirror/lint'
|
||||||
|
|
||||||
|
describe('EditorManager Class', () => {
|
||||||
|
describe('makeUniqueDiagnostics', () => {
|
||||||
|
it('should filter out duplicated diagnostics', () => {
|
||||||
|
const duplicatedDiagnostics: Diagnostic[] = [
|
||||||
|
{
|
||||||
|
from: 2,
|
||||||
|
to: 10,
|
||||||
|
severity: 'hint',
|
||||||
|
message: 'my cool message',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 2,
|
||||||
|
to: 10,
|
||||||
|
severity: 'hint',
|
||||||
|
message: 'my cool message',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 2,
|
||||||
|
to: 10,
|
||||||
|
severity: 'hint',
|
||||||
|
message: 'my cool message',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const expected: Diagnostic[] = [
|
||||||
|
{
|
||||||
|
from: 2,
|
||||||
|
to: 10,
|
||||||
|
severity: 'hint',
|
||||||
|
message: 'my cool message',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const actual = editorManager.makeUniqueDiagnostics(duplicatedDiagnostics)
|
||||||
|
expect(actual).toStrictEqual(expected)
|
||||||
|
})
|
||||||
|
it('should filter out duplicated diagnostic and keep some original ones', () => {
|
||||||
|
const duplicatedDiagnostics: Diagnostic[] = [
|
||||||
|
{
|
||||||
|
from: 0,
|
||||||
|
to: 10,
|
||||||
|
severity: 'hint',
|
||||||
|
message: 'my cool message',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 0,
|
||||||
|
to: 10,
|
||||||
|
severity: 'hint',
|
||||||
|
message: 'my cool message',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 88,
|
||||||
|
to: 99,
|
||||||
|
severity: 'hint',
|
||||||
|
message: 'my super cool message',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const expected: Diagnostic[] = [
|
||||||
|
{
|
||||||
|
from: 0,
|
||||||
|
to: 10,
|
||||||
|
severity: 'hint',
|
||||||
|
message: 'my cool message',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 88,
|
||||||
|
to: 99,
|
||||||
|
severity: 'hint',
|
||||||
|
message: 'my super cool message',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const actual = editorManager.makeUniqueDiagnostics(duplicatedDiagnostics)
|
||||||
|
expect(actual).toStrictEqual(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -24,10 +24,6 @@ export const modelingMachineEvent = modelingMachineAnnotation.of(true)
|
|||||||
const setDiagnosticsAnnotation = Annotation.define<boolean>()
|
const setDiagnosticsAnnotation = Annotation.define<boolean>()
|
||||||
export const setDiagnosticsEvent = setDiagnosticsAnnotation.of(true)
|
export const setDiagnosticsEvent = setDiagnosticsAnnotation.of(true)
|
||||||
|
|
||||||
function diagnosticIsEqual(d1: Diagnostic, d2: Diagnostic): boolean {
|
|
||||||
return d1.from === d2.from && d1.to === d2.to && d1.message === d2.message
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class EditorManager {
|
export default class EditorManager {
|
||||||
private _editorView: EditorView | null = null
|
private _editorView: EditorView | null = null
|
||||||
private _copilotEnabled: boolean = true
|
private _copilotEnabled: boolean = true
|
||||||
@ -72,9 +68,10 @@ export default class EditorManager {
|
|||||||
// we cannot use <>.constructor.name since it will get destroyed
|
// we cannot use <>.constructor.name since it will get destroyed
|
||||||
// when packaging the application.
|
// when packaging the application.
|
||||||
const isTreeHighlightPlugin =
|
const isTreeHighlightPlugin =
|
||||||
e.value.hasOwnProperty('tree') &&
|
e?.value &&
|
||||||
e.value.hasOwnProperty('decoratedTo') &&
|
e.value?.hasOwnProperty('tree') &&
|
||||||
e.value.hasOwnProperty('decorations')
|
e.value?.hasOwnProperty('decoratedTo') &&
|
||||||
|
e.value?.hasOwnProperty('decorations')
|
||||||
|
|
||||||
if (isTreeHighlightPlugin) {
|
if (isTreeHighlightPlugin) {
|
||||||
let originalUpdate = e.value.update
|
let originalUpdate = e.value.update
|
||||||
@ -161,20 +158,29 @@ export default class EditorManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an array of Diagnostics remove any duplicates by hashing a key
|
||||||
|
* in the format of from + ' ' + to + ' ' + message.
|
||||||
|
*/
|
||||||
|
makeUniqueDiagnostics(duplicatedDiagnostics: Diagnostic[]): Diagnostic[] {
|
||||||
|
const uniqueDiagnostics: Diagnostic[] = []
|
||||||
|
const seenDiagnostic: { [key: string]: boolean } = {}
|
||||||
|
|
||||||
|
duplicatedDiagnostics.forEach((diagnostic: Diagnostic) => {
|
||||||
|
const hash = `${diagnostic.from} ${diagnostic.to} ${diagnostic.message}`
|
||||||
|
if (!seenDiagnostic[hash]) {
|
||||||
|
uniqueDiagnostics.push(diagnostic)
|
||||||
|
seenDiagnostic[hash] = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return uniqueDiagnostics
|
||||||
|
}
|
||||||
|
|
||||||
setDiagnostics(diagnostics: Diagnostic[]): void {
|
setDiagnostics(diagnostics: Diagnostic[]): void {
|
||||||
if (!this._editorView) return
|
if (!this._editorView) return
|
||||||
// Clear out any existing diagnostics that are the same.
|
// Clear out any existing diagnostics that are the same.
|
||||||
for (const diagnostic of diagnostics) {
|
diagnostics = this.makeUniqueDiagnostics(diagnostics)
|
||||||
for (const otherDiagnostic of diagnostics) {
|
|
||||||
if (diagnosticIsEqual(diagnostic, otherDiagnostic)) {
|
|
||||||
diagnostics = diagnostics.filter(
|
|
||||||
(d) => !diagnosticIsEqual(d, diagnostic)
|
|
||||||
)
|
|
||||||
diagnostics.push(diagnostic)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._editorView.dispatch({
|
this._editorView.dispatch({
|
||||||
effects: [setDiagnosticsEffect.of(diagnostics)],
|
effects: [setDiagnosticsEffect.of(diagnostics)],
|
||||||
|
237
src/hooks/useEngineStreamContext.ts
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
import { makeDefaultPlanes, modifyGrid } from 'lang/wasm'
|
||||||
|
import { MutableRefObject } from 'react'
|
||||||
|
import { setup, assign } from 'xstate'
|
||||||
|
import { createActorContext } from '@xstate/react'
|
||||||
|
import { kclManager, sceneInfra, engineCommandManager } from 'lib/singletons'
|
||||||
|
import { trap } from 'lib/trap'
|
||||||
|
|
||||||
|
export enum EngineStreamState {
|
||||||
|
Off = 'off',
|
||||||
|
On = 'on',
|
||||||
|
Playing = 'playing',
|
||||||
|
Paused = 'paused',
|
||||||
|
Resuming = 'resuming',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EngineStreamTransition {
|
||||||
|
SetMediaStream = 'set-context',
|
||||||
|
Play = 'play',
|
||||||
|
Resume = 'resume',
|
||||||
|
Pause = 'pause',
|
||||||
|
StartOrReconfigureEngine = 'start-or-reconfigure-engine',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EngineStreamContext {
|
||||||
|
pool: string | null
|
||||||
|
authToken: string | null
|
||||||
|
mediaStream: MediaStream | null
|
||||||
|
videoRef: MutableRefObject<HTMLVideoElement | null>
|
||||||
|
canvasRef: MutableRefObject<HTMLCanvasElement | null>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDimensions(streamWidth: number, streamHeight: number) {
|
||||||
|
const factorOf = 4
|
||||||
|
const maxResolution = 2160
|
||||||
|
const ratio = Math.min(
|
||||||
|
Math.min(maxResolution / streamWidth, maxResolution / streamHeight),
|
||||||
|
1.0
|
||||||
|
)
|
||||||
|
const quadWidth = Math.round((streamWidth * ratio) / factorOf) * factorOf
|
||||||
|
const quadHeight = Math.round((streamHeight * ratio) / factorOf) * factorOf
|
||||||
|
return { width: quadWidth, height: quadHeight }
|
||||||
|
}
|
||||||
|
|
||||||
|
const engineStreamMachine = setup({
|
||||||
|
types: {
|
||||||
|
context: {} as EngineStreamContext,
|
||||||
|
input: {} as EngineStreamContext,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
[EngineStreamTransition.Play]({ context }, params: { zoomToFit: boolean }) {
|
||||||
|
const canvas = context.canvasRef.current
|
||||||
|
if (!canvas) return false
|
||||||
|
|
||||||
|
const video = context.videoRef.current
|
||||||
|
if (!video) return false
|
||||||
|
|
||||||
|
const mediaStream = context.mediaStream
|
||||||
|
if (!mediaStream) return false
|
||||||
|
|
||||||
|
video.style.display = 'block'
|
||||||
|
canvas.style.display = 'none'
|
||||||
|
|
||||||
|
video.srcObject = mediaStream
|
||||||
|
void sceneInfra.camControls
|
||||||
|
.restoreCameraPosition()
|
||||||
|
.then(() => video.play())
|
||||||
|
.catch((e) => {
|
||||||
|
console.warn('Video playing was prevented', e, video)
|
||||||
|
})
|
||||||
|
.then(() => kclManager.executeCode(params.zoomToFit))
|
||||||
|
.catch(trap)
|
||||||
|
},
|
||||||
|
[EngineStreamTransition.Pause]({ context }) {
|
||||||
|
const video = context.videoRef.current
|
||||||
|
if (!video) return
|
||||||
|
|
||||||
|
video.pause()
|
||||||
|
|
||||||
|
const canvas = context.canvasRef.current
|
||||||
|
if (!canvas) return
|
||||||
|
|
||||||
|
canvas.width = video.videoWidth
|
||||||
|
canvas.height = video.videoHeight
|
||||||
|
canvas.style.width = video.videoWidth + 'px'
|
||||||
|
canvas.style.height = video.videoHeight + 'px'
|
||||||
|
canvas.style.display = 'block'
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (!ctx) return
|
||||||
|
|
||||||
|
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
|
// Make sure we're on the next frame for no flickering between canvas
|
||||||
|
// and the video elements.
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
video.style.display = 'none'
|
||||||
|
|
||||||
|
// Destroy the media stream only. We will re-establish it. We could
|
||||||
|
// leave everything at pausing, preventing video decoders from running
|
||||||
|
// but we can do even better by significantly reducing network
|
||||||
|
// cards also.
|
||||||
|
context.mediaStream?.getVideoTracks()[0].stop()
|
||||||
|
video.srcObject = null
|
||||||
|
|
||||||
|
sceneInfra.camControls.old = {
|
||||||
|
camera: sceneInfra.camControls.camera.clone(),
|
||||||
|
target: sceneInfra.camControls.target.clone(),
|
||||||
|
}
|
||||||
|
|
||||||
|
engineCommandManager.tearDown({ idleMode: true })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async [EngineStreamTransition.StartOrReconfigureEngine]({
|
||||||
|
context,
|
||||||
|
event,
|
||||||
|
}) {
|
||||||
|
if (!context.authToken) return
|
||||||
|
|
||||||
|
const video = context.videoRef.current
|
||||||
|
if (!video) return
|
||||||
|
|
||||||
|
const { width, height } = getDimensions(
|
||||||
|
window.innerWidth,
|
||||||
|
window.innerHeight
|
||||||
|
)
|
||||||
|
|
||||||
|
video.width = width
|
||||||
|
video.height = height
|
||||||
|
|
||||||
|
const settingsNext = {
|
||||||
|
// override the pool param (?pool=) to request a specific engine instance
|
||||||
|
// from a particular pool.
|
||||||
|
pool: context.pool,
|
||||||
|
...event.settings,
|
||||||
|
}
|
||||||
|
|
||||||
|
engineCommandManager.settings = settingsNext
|
||||||
|
|
||||||
|
engineCommandManager.start({
|
||||||
|
setMediaStream: event.onMediaStream,
|
||||||
|
setIsStreamReady: (isStreamReady) =>
|
||||||
|
event.setAppState({ isStreamReady }),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
token: context.authToken,
|
||||||
|
settings: settingsNext,
|
||||||
|
makeDefaultPlanes: () => {
|
||||||
|
return makeDefaultPlanes(kclManager.engineCommandManager)
|
||||||
|
},
|
||||||
|
modifyGrid: (hidden: boolean) => {
|
||||||
|
return modifyGrid(kclManager.engineCommandManager, hidden)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
event.modelingMachineActorSend({
|
||||||
|
type: 'Set context',
|
||||||
|
data: {
|
||||||
|
streamDimensions: {
|
||||||
|
streamWidth: width,
|
||||||
|
streamHeight: height,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async [EngineStreamTransition.Resume]({ context, event }) {
|
||||||
|
// engineCommandManager.engineConnection?.reattachMediaStream()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
context: (initial) => initial.input,
|
||||||
|
initial: EngineStreamState.Off,
|
||||||
|
states: {
|
||||||
|
[EngineStreamState.Off]: {
|
||||||
|
on: {
|
||||||
|
[EngineStreamTransition.StartOrReconfigureEngine]: {
|
||||||
|
target: EngineStreamState.On,
|
||||||
|
actions: [{ type: EngineStreamTransition.StartOrReconfigureEngine }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[EngineStreamState.On]: {
|
||||||
|
on: {
|
||||||
|
[EngineStreamTransition.SetMediaStream]: {
|
||||||
|
target: EngineStreamState.On,
|
||||||
|
actions: [
|
||||||
|
assign({ mediaStream: ({ context, event }) => event.mediaStream }),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
[EngineStreamTransition.Play]: {
|
||||||
|
target: EngineStreamState.Playing,
|
||||||
|
actions: [
|
||||||
|
{ type: EngineStreamTransition.Play, params: { zoomToFit: true } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[EngineStreamState.Playing]: {
|
||||||
|
on: {
|
||||||
|
[EngineStreamTransition.StartOrReconfigureEngine]: {
|
||||||
|
target: EngineStreamState.Playing,
|
||||||
|
reenter: true,
|
||||||
|
actions: [{ type: EngineStreamTransition.StartOrReconfigureEngine }],
|
||||||
|
},
|
||||||
|
[EngineStreamTransition.Pause]: {
|
||||||
|
target: EngineStreamState.Paused,
|
||||||
|
actions: [{ type: EngineStreamTransition.Pause }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[EngineStreamState.Paused]: {
|
||||||
|
on: {
|
||||||
|
[EngineStreamTransition.StartOrReconfigureEngine]: {
|
||||||
|
target: EngineStreamState.Resuming,
|
||||||
|
actions: [{ type: EngineStreamTransition.StartOrReconfigureEngine }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[EngineStreamState.Resuming]: {
|
||||||
|
on: {
|
||||||
|
[EngineStreamTransition.SetMediaStream]: {
|
||||||
|
target: EngineStreamState.Resuming,
|
||||||
|
actions: [
|
||||||
|
assign({ mediaStream: ({ context, event }) => event.mediaStream }),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
[EngineStreamTransition.Play]: {
|
||||||
|
target: EngineStreamState.Playing,
|
||||||
|
actions: [
|
||||||
|
{ type: EngineStreamTransition.Play, params: { zoomToFit: false } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default createActorContext(engineStreamMachine)
|
@ -2,13 +2,13 @@ import {
|
|||||||
SetVarNameModal,
|
SetVarNameModal,
|
||||||
createSetVarNameModal,
|
createSetVarNameModal,
|
||||||
} from 'components/SetVarNameModal'
|
} from 'components/SetVarNameModal'
|
||||||
import { editorManager, kclManager } from 'lib/singletons'
|
import { editorManager, kclManager, codeManager } from 'lib/singletons'
|
||||||
import { reportRejection, trap } from 'lib/trap'
|
import { reportRejection, trap, err } from 'lib/trap'
|
||||||
import { moveValueIntoNewVariable } from 'lang/modifyAst'
|
import { moveValueIntoNewVariable } from 'lang/modifyAst'
|
||||||
import { isNodeSafeToReplace } from 'lang/queryAst'
|
import { isNodeSafeToReplace } from 'lang/queryAst'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useModelingContext } from './useModelingContext'
|
import { useModelingContext } from './useModelingContext'
|
||||||
import { PathToNode, SourceRange } from 'lang/wasm'
|
import { PathToNode, SourceRange, recast } from 'lang/wasm'
|
||||||
import { useKclContext } from 'lang/KclProvider'
|
import { useKclContext } from 'lang/KclProvider'
|
||||||
import { toSync } from 'lib/utils'
|
import { toSync } from 'lib/utils'
|
||||||
|
|
||||||
@ -57,6 +57,11 @@ export function useConvertToVariable(range?: SourceRange) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
await kclManager.updateAst(_modifiedAst, true)
|
await kclManager.updateAst(_modifiedAst, true)
|
||||||
|
|
||||||
|
const newCode = recast(_modifiedAst)
|
||||||
|
if (err(newCode)) return
|
||||||
|
codeManager.updateCodeEditor(newCode)
|
||||||
|
|
||||||
return pathToReplacedNode
|
return pathToReplacedNode
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('error', e)
|
console.log('error', e)
|
||||||
|
@ -2,7 +2,7 @@ import { executeAst, lintAst } from 'lang/langHelpers'
|
|||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
import { KCLError, kclErrorsToDiagnostics } from './errors'
|
import { KCLError, kclErrorsToDiagnostics } from './errors'
|
||||||
import { uuidv4 } from 'lib/utils'
|
import { uuidv4 } from 'lib/utils'
|
||||||
import { EngineCommandManager } from './std/engineConnection'
|
import { EngineCommandManager, CommandLogType } from './std/engineConnection'
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
|
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
|
||||||
|
|
||||||
@ -125,7 +125,7 @@ export class KclManager {
|
|||||||
if (this.lints.length > 0) {
|
if (this.lints.length > 0) {
|
||||||
diagnostics = diagnostics.concat(this.lints)
|
diagnostics = diagnostics.concat(this.lints)
|
||||||
}
|
}
|
||||||
editorManager.setDiagnostics(diagnostics)
|
editorManager?.setDiagnostics(diagnostics)
|
||||||
}
|
}
|
||||||
|
|
||||||
addKclErrors(kclErrors: KCLError[]) {
|
addKclErrors(kclErrors: KCLError[]) {
|
||||||
@ -290,15 +290,9 @@ export class KclManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.engineCommandManager.sendSceneCommand({
|
await sceneInfra.camControls.centerModelRelativeToPanes({
|
||||||
type: 'modeling_cmd_req',
|
zoomToFit: true,
|
||||||
cmd_id: uuidv4(),
|
zoomObjectId,
|
||||||
cmd: {
|
|
||||||
type: 'zoom_to_fit',
|
|
||||||
object_ids: zoomObjectId ? [zoomObjectId] : [], // leave empty to zoom to all objects
|
|
||||||
padding: 0.1, // padding around the objects
|
|
||||||
animated: false, // don't animate the zoom for now
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -328,7 +322,7 @@ export class KclManager {
|
|||||||
this.ast = { ...ast }
|
this.ast = { ...ast }
|
||||||
this._executeCallback()
|
this._executeCallback()
|
||||||
this.engineCommandManager.addCommandLog({
|
this.engineCommandManager.addCommandLog({
|
||||||
type: 'execution-done',
|
type: CommandLogType.ExecutionDone,
|
||||||
data: null,
|
data: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -357,9 +351,6 @@ export class KclManager {
|
|||||||
this.clearAst()
|
this.clearAst()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
codeManager.updateCodeEditor(newCode)
|
|
||||||
// Write the file to disk.
|
|
||||||
await codeManager.writeToFile()
|
|
||||||
this._ast = { ...newAst }
|
this._ast = { ...newAst }
|
||||||
|
|
||||||
const { logs, errors, execState } = await executeAst({
|
const { logs, errors, execState } = await executeAst({
|
||||||
@ -497,11 +488,6 @@ export class KclManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (execute) {
|
if (execute) {
|
||||||
// Call execute on the set ast.
|
|
||||||
// Update the code state and editor.
|
|
||||||
codeManager.updateCodeEditor(newCode)
|
|
||||||
// Write the file to disk.
|
|
||||||
await codeManager.writeToFile()
|
|
||||||
await this.executeAst({
|
await this.executeAst({
|
||||||
ast: astWithUpdatedSource,
|
ast: astWithUpdatedSource,
|
||||||
zoomToFit: optionalParams?.zoomToFit,
|
zoomToFit: optionalParams?.zoomToFit,
|
||||||
|
@ -18,8 +18,7 @@ const mySketch001 = startSketchOn('XY')
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const sketch001 = execState.memory.get('mySketch001')
|
const sketch001 = execState.memory.get('mySketch001')
|
||||||
expect(sketch001).toEqual({
|
expect(sketch001).toEqual({
|
||||||
type: 'UserVal',
|
type: 'Sketch',
|
||||||
__meta: [{ sourceRange: [46, 71, 0] }],
|
|
||||||
value: {
|
value: {
|
||||||
type: 'Sketch',
|
type: 'Sketch',
|
||||||
on: expect.any(Object),
|
on: expect.any(Object),
|
||||||
|
@ -7,6 +7,8 @@ import toast from 'react-hot-toast'
|
|||||||
import { editorManager } from 'lib/singletons'
|
import { editorManager } from 'lib/singletons'
|
||||||
import { Annotation, Transaction } from '@codemirror/state'
|
import { Annotation, Transaction } from '@codemirror/state'
|
||||||
import { KeyBinding } from '@codemirror/view'
|
import { KeyBinding } from '@codemirror/view'
|
||||||
|
import { recast, Program } from 'lang/wasm'
|
||||||
|
import { err } from 'lib/trap'
|
||||||
|
|
||||||
const PERSIST_CODE_KEY = 'persistCode'
|
const PERSIST_CODE_KEY = 'persistCode'
|
||||||
|
|
||||||
@ -147,6 +149,13 @@ export default class CodeManager {
|
|||||||
safeLSSetItem(PERSIST_CODE_KEY, this.code)
|
safeLSSetItem(PERSIST_CODE_KEY, this.code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateEditorWithAstAndWriteToFile(ast: Program) {
|
||||||
|
const newCode = recast(ast)
|
||||||
|
if (err(newCode)) return
|
||||||
|
this.updateCodeStateEditor(newCode)
|
||||||
|
await this.writeToFile()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeLSGetItem(key: string) {
|
function safeLSGetItem(key: string) {
|
||||||
|
@ -58,7 +58,13 @@ const newVar = myVar + 1`
|
|||||||
`
|
`
|
||||||
const mem = await exe(code)
|
const mem = await exe(code)
|
||||||
// geo is three js buffer geometry and is very bloated to have in tests
|
// geo is three js buffer geometry and is very bloated to have in tests
|
||||||
const minusGeo = mem.get('mySketch')?.value?.paths
|
const sk = mem.get('mySketch')
|
||||||
|
expect(sk?.type).toEqual('Sketch')
|
||||||
|
if (sk?.type !== 'Sketch') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const minusGeo = sk?.value?.paths
|
||||||
expect(minusGeo).toEqual([
|
expect(minusGeo).toEqual([
|
||||||
{
|
{
|
||||||
type: 'ToPoint',
|
type: 'ToPoint',
|
||||||
@ -150,7 +156,7 @@ const newVar = myVar + 1`
|
|||||||
].join('\n')
|
].join('\n')
|
||||||
const mem = await exe(code)
|
const mem = await exe(code)
|
||||||
expect(mem.get('mySk1')).toEqual({
|
expect(mem.get('mySk1')).toEqual({
|
||||||
type: 'UserVal',
|
type: 'Sketch',
|
||||||
value: {
|
value: {
|
||||||
type: 'Sketch',
|
type: 'Sketch',
|
||||||
on: expect.any(Object),
|
on: expect.any(Object),
|
||||||
@ -215,7 +221,6 @@ const newVar = myVar + 1`
|
|||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
__meta: [{ sourceRange: [39, 63, 0] }],
|
__meta: [{ sourceRange: [39, 63, 0] }],
|
||||||
},
|
},
|
||||||
__meta: [{ sourceRange: [39, 63, 0] }],
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('execute array expression', async () => {
|
it('execute array expression', async () => {
|
||||||
@ -225,7 +230,7 @@ const newVar = myVar + 1`
|
|||||||
const mem = await exe(code)
|
const mem = await exe(code)
|
||||||
// TODO path to node is probably wrong here, zero indexes are not correct
|
// TODO path to node is probably wrong here, zero indexes are not correct
|
||||||
expect(mem.get('three')).toEqual({
|
expect(mem.get('three')).toEqual({
|
||||||
type: 'UserVal',
|
type: 'Int',
|
||||||
value: 3,
|
value: 3,
|
||||||
__meta: [
|
__meta: [
|
||||||
{
|
{
|
||||||
@ -234,8 +239,17 @@ const newVar = myVar + 1`
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
expect(mem.get('yo')).toEqual({
|
expect(mem.get('yo')).toEqual({
|
||||||
type: 'UserVal',
|
type: 'Array',
|
||||||
value: [1, '2', 3, 9],
|
value: [
|
||||||
|
{ type: 'Int', value: 1, __meta: [{ sourceRange: [28, 29, 0] }] },
|
||||||
|
{ type: 'String', value: '2', __meta: [{ sourceRange: [31, 34, 0] }] },
|
||||||
|
{ type: 'Int', value: 3, __meta: [{ sourceRange: [14, 15, 0] }] },
|
||||||
|
{
|
||||||
|
type: 'Number',
|
||||||
|
value: 9,
|
||||||
|
__meta: [{ sourceRange: [43, 44, 0] }, { sourceRange: [47, 48, 0] }],
|
||||||
|
},
|
||||||
|
],
|
||||||
__meta: [
|
__meta: [
|
||||||
{
|
{
|
||||||
sourceRange: [27, 49, 0],
|
sourceRange: [27, 49, 0],
|
||||||
@ -253,8 +267,25 @@ const newVar = myVar + 1`
|
|||||||
].join('\n')
|
].join('\n')
|
||||||
const mem = await exe(code)
|
const mem = await exe(code)
|
||||||
expect(mem.get('yo')).toEqual({
|
expect(mem.get('yo')).toEqual({
|
||||||
type: 'UserVal',
|
type: 'Object',
|
||||||
value: { aStr: 'str', anum: 2, identifier: 3, binExp: 9 },
|
value: {
|
||||||
|
aStr: {
|
||||||
|
type: 'String',
|
||||||
|
value: 'str',
|
||||||
|
__meta: [{ sourceRange: [34, 39, 0] }],
|
||||||
|
},
|
||||||
|
anum: { type: 'Int', value: 2, __meta: [{ sourceRange: [47, 48, 0] }] },
|
||||||
|
identifier: {
|
||||||
|
type: 'Int',
|
||||||
|
value: 3,
|
||||||
|
__meta: [{ sourceRange: [14, 15, 0] }],
|
||||||
|
},
|
||||||
|
binExp: {
|
||||||
|
type: 'Number',
|
||||||
|
value: 9,
|
||||||
|
__meta: [{ sourceRange: [77, 78, 0] }, { sourceRange: [81, 82, 0] }],
|
||||||
|
},
|
||||||
|
},
|
||||||
__meta: [
|
__meta: [
|
||||||
{
|
{
|
||||||
sourceRange: [27, 83, 0],
|
sourceRange: [27, 83, 0],
|
||||||
@ -268,11 +299,11 @@ const newVar = myVar + 1`
|
|||||||
)
|
)
|
||||||
const mem = await exe(code)
|
const mem = await exe(code)
|
||||||
expect(mem.get('myVar')).toEqual({
|
expect(mem.get('myVar')).toEqual({
|
||||||
type: 'UserVal',
|
type: 'String',
|
||||||
value: '123',
|
value: '123',
|
||||||
__meta: [
|
__meta: [
|
||||||
{
|
{
|
||||||
sourceRange: [41, 50, 0],
|
sourceRange: [19, 24, 0],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@ -356,7 +387,26 @@ describe('testing math operators', () => {
|
|||||||
it('with unaryExpression in ArrayExpression', async () => {
|
it('with unaryExpression in ArrayExpression', async () => {
|
||||||
const code = 'const myVar = [1,-legLen(5, 4)]'
|
const code = 'const myVar = [1,-legLen(5, 4)]'
|
||||||
const mem = await exe(code)
|
const mem = await exe(code)
|
||||||
expect(mem.get('myVar')?.value).toEqual([1, -3])
|
expect(mem.get('myVar')?.value).toEqual([
|
||||||
|
{
|
||||||
|
__meta: [
|
||||||
|
{
|
||||||
|
sourceRange: [15, 16, 0],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'Int',
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__meta: [
|
||||||
|
{
|
||||||
|
sourceRange: [17, 30, 0],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'Number',
|
||||||
|
value: -3,
|
||||||
|
},
|
||||||
|
])
|
||||||
})
|
})
|
||||||
it('with unaryExpression in ArrayExpression in CallExpression, checking nothing funny happens when used in a sketch', async () => {
|
it('with unaryExpression in ArrayExpression in CallExpression, checking nothing funny happens when used in a sketch', async () => {
|
||||||
const code = [
|
const code = [
|
||||||
|
@ -55,18 +55,13 @@ describe('Test KCL Samples from public Github repository', () => {
|
|||||||
})
|
})
|
||||||
// Run through all of the files in the manifest json. This will allow us to be automatically updated
|
// Run through all of the files in the manifest json. This will allow us to be automatically updated
|
||||||
// with the latest changes in github. We won't be hard coding the filenames
|
// with the latest changes in github. We won't be hard coding the filenames
|
||||||
it(
|
files.forEach((file: KclSampleFile) => {
|
||||||
'should run through all the files',
|
it(`should parse ${file.filename} without errors`, async () => {
|
||||||
async () => {
|
const code = await getKclSampleCodeFromGithub(file.filename)
|
||||||
for (let i = 0; i < files.length; i++) {
|
const parsed = parse(code)
|
||||||
const file: KclSampleFile = files[i]
|
assert(!(parsed instanceof Error))
|
||||||
const code = await getKclSampleCodeFromGithub(file.filename)
|
}, 1000)
|
||||||
const parsed = parse(code)
|
})
|
||||||
assert(!(parsed instanceof Error))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
files.length * 1000
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('when performing enginelessExecutor', () => {
|
describe('when performing enginelessExecutor', () => {
|
||||||
|
@ -35,7 +35,12 @@ import {
|
|||||||
ArtifactGraph,
|
ArtifactGraph,
|
||||||
getSweepFromSuspectedPath,
|
getSweepFromSuspectedPath,
|
||||||
} from 'lang/std/artifactGraph'
|
} from 'lang/std/artifactGraph'
|
||||||
import { kclManager, engineCommandManager, editorManager } from 'lib/singletons'
|
import {
|
||||||
|
kclManager,
|
||||||
|
engineCommandManager,
|
||||||
|
editorManager,
|
||||||
|
codeManager,
|
||||||
|
} from 'lib/singletons'
|
||||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
|
|
||||||
// Apply Fillet To Selection
|
// Apply Fillet To Selection
|
||||||
@ -253,6 +258,9 @@ async function updateAstAndFocus(
|
|||||||
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
|
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
|
||||||
focusPath: pathToFilletNode,
|
focusPath: pathToFilletNode,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
|
||||||
|
|
||||||
if (updatedAst?.selections) {
|
if (updatedAst?.selections) {
|
||||||
editorManager.selectRange(updatedAst?.selections)
|
editorManager.selectRange(updatedAst?.selections)
|
||||||
}
|
}
|
||||||
|
@ -633,7 +633,7 @@ export function expandSweep(
|
|||||||
if (err(path)) return path
|
if (err(path)) return path
|
||||||
return {
|
return {
|
||||||
type: 'sweep',
|
type: 'sweep',
|
||||||
subType: 'extrusion',
|
subType: sweep.subType,
|
||||||
surfaces: Array.from(surfs.values()),
|
surfaces: Array.from(surfs.values()),
|
||||||
edges: Array.from(edges.values()),
|
edges: Array.from(edges.values()),
|
||||||
path,
|
path,
|
||||||
|
@ -406,13 +406,14 @@ class EngineConnection extends EventTarget {
|
|||||||
default:
|
default:
|
||||||
if (this.isConnecting()) break
|
if (this.isConnecting()) break
|
||||||
// Means we never could do an initial connection. Reconnect everything.
|
// Means we never could do an initial connection. Reconnect everything.
|
||||||
if (!this.pingPongSpan.ping) this.connect().catch(reportRejection)
|
if (!this.pingPongSpan.ping)
|
||||||
|
this.connect({ reconnect: false }).catch(reportRejection)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}, pingIntervalMs)
|
}, pingIntervalMs)
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.connect()
|
this.connect({ reconnect: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
// SHOULD ONLY BE USED FOR VITESTS
|
// SHOULD ONLY BE USED FOR VITESTS
|
||||||
@ -523,7 +524,9 @@ class EngineConnection extends EventTarget {
|
|||||||
this.idleMode = opts?.idleMode ?? false
|
this.idleMode = opts?.idleMode ?? false
|
||||||
clearInterval(this.pingIntervalId)
|
clearInterval(this.pingIntervalId)
|
||||||
|
|
||||||
if (opts?.idleMode) {
|
this.disconnectAll()
|
||||||
|
|
||||||
|
if (this.idleMode) {
|
||||||
this.state = {
|
this.state = {
|
||||||
type: EngineConnectionStateType.Disconnecting,
|
type: EngineConnectionStateType.Disconnecting,
|
||||||
value: {
|
value: {
|
||||||
@ -542,8 +545,6 @@ class EngineConnection extends EventTarget {
|
|||||||
type: DisconnectingType.Quit,
|
type: DisconnectingType.Quit,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
this.disconnectAll()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -553,7 +554,7 @@ class EngineConnection extends EventTarget {
|
|||||||
* This will attempt the full handshake, and retry if the connection
|
* This will attempt the full handshake, and retry if the connection
|
||||||
* did not establish.
|
* did not establish.
|
||||||
*/
|
*/
|
||||||
connect(reconnecting?: boolean): Promise<void> {
|
connect(args: { reconnect: boolean }): Promise<void> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (this.isConnecting() || this.isReady()) {
|
if (this.isConnecting() || this.isReady()) {
|
||||||
return
|
return
|
||||||
@ -1165,7 +1166,7 @@ class EngineConnection extends EventTarget {
|
|||||||
this.websocket.addEventListener('message', this.onWebSocketMessage)
|
this.websocket.addEventListener('message', this.onWebSocketMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reconnecting) {
|
if (args.reconnect) {
|
||||||
createWebSocketConnection()
|
createWebSocketConnection()
|
||||||
} else {
|
} else {
|
||||||
this.onNetworkStatusReady = () => {
|
this.onNetworkStatusReady = () => {
|
||||||
@ -1178,6 +1179,32 @@ class EngineConnection extends EventTarget {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async reattachMediaStream() {
|
||||||
|
return this.pc
|
||||||
|
?.createOffer({ iceRestart: true })
|
||||||
|
.then((offer: RTCSessionDescriptionInit) => {
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Connecting,
|
||||||
|
value: {
|
||||||
|
type: ConnectingType.SetLocalDescription,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return this.pc?.setLocalDescription(offer).then(() => {
|
||||||
|
this.send({
|
||||||
|
type: 'sdp_offer',
|
||||||
|
offer: offer as Models['RtcSessionDescription_type'],
|
||||||
|
})
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Connecting,
|
||||||
|
value: {
|
||||||
|
type: ConnectingType.OfferedSdp,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Do not change this back to an object or any, we should only be sending the
|
// Do not change this back to an object or any, we should only be sending the
|
||||||
// WebSocketRequest type!
|
// WebSocketRequest type!
|
||||||
unreliableSend(message: Models['WebSocketRequest_type']) {
|
unreliableSend(message: Models['WebSocketRequest_type']) {
|
||||||
@ -1229,8 +1256,17 @@ class EngineConnection extends EventTarget {
|
|||||||
this.websocket?.readyState === 3
|
this.websocket?.readyState === 3
|
||||||
|
|
||||||
if (closedPc && closedUDC && closedWS) {
|
if (closedPc && closedUDC && closedWS) {
|
||||||
// Do not notify the rest of the program that we have cut off anything.
|
if (!this.idleMode) {
|
||||||
this.state = { type: EngineConnectionStateType.Disconnected }
|
// Do not notify the rest of the program that we have cut off anything.
|
||||||
|
this.state = { type: EngineConnectionStateType.Disconnected }
|
||||||
|
} else {
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Disconnecting,
|
||||||
|
value: {
|
||||||
|
type: DisconnectingType.Pause,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1255,27 +1291,40 @@ export interface Subscription<T extends ModelTypes> {
|
|||||||
) => void
|
) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CommandLogType {
|
||||||
|
SendModeling = 'send-modeling',
|
||||||
|
SendScene = 'send-scene',
|
||||||
|
ReceiveReliable = 'receive-reliable',
|
||||||
|
ExecutionDone = 'execution-done',
|
||||||
|
ExportDone = 'export-done',
|
||||||
|
SetDefaultSystemProperties = 'set_default_system_properties',
|
||||||
|
}
|
||||||
|
|
||||||
export type CommandLog =
|
export type CommandLog =
|
||||||
| {
|
| {
|
||||||
type: 'send-modeling'
|
type: CommandLogType.SendModeling
|
||||||
data: EngineCommand
|
data: EngineCommand
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'send-scene'
|
type: CommandLogType.SendScene
|
||||||
data: EngineCommand
|
data: EngineCommand
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'receive-reliable'
|
type: CommandLogType.ReceiveReliable
|
||||||
data: OkWebSocketResponseData
|
data: OkWebSocketResponseData
|
||||||
id: string
|
id: string
|
||||||
cmd_type?: string
|
cmd_type?: string
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'execution-done'
|
type: CommandLogType.ExecutionDone
|
||||||
data: null
|
data: null
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'export-done'
|
type: CommandLogType.ExportDone
|
||||||
|
data: null
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: CommandLogType.SetDefaultSystemProperties
|
||||||
data: null
|
data: null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1686,7 +1735,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
message.request_id
|
message.request_id
|
||||||
) {
|
) {
|
||||||
this.addCommandLog({
|
this.addCommandLog({
|
||||||
type: 'receive-reliable',
|
type: CommandLogType.ReceiveReliable,
|
||||||
data: message.resp,
|
data: message.resp,
|
||||||
id: message?.request_id || '',
|
id: message?.request_id || '',
|
||||||
cmd_type: pending?.command?.cmd?.type,
|
cmd_type: pending?.command?.cmd?.type,
|
||||||
@ -1720,7 +1769,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
if (!command) return
|
if (!command) return
|
||||||
if (command.type === 'modeling_cmd_req')
|
if (command.type === 'modeling_cmd_req')
|
||||||
this.addCommandLog({
|
this.addCommandLog({
|
||||||
type: 'receive-reliable',
|
type: CommandLogType.ReceiveReliable,
|
||||||
data: {
|
data: {
|
||||||
type: 'modeling',
|
type: 'modeling',
|
||||||
data: {
|
data: {
|
||||||
@ -1762,7 +1811,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.engineConnection?.connect()
|
this.engineConnection?.connect({ reconnect: false })
|
||||||
}
|
}
|
||||||
this.engineConnection.addEventListener(
|
this.engineConnection.addEventListener(
|
||||||
EngineConnectionEvents.ConnectionStarted,
|
EngineConnectionEvents.ConnectionStarted,
|
||||||
@ -1824,6 +1873,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
)
|
)
|
||||||
|
|
||||||
this.engineConnection?.tearDown(opts)
|
this.engineConnection?.tearDown(opts)
|
||||||
|
this.engineConnection = undefined
|
||||||
|
|
||||||
// Our window.tearDown assignment causes this case to happen which is
|
// Our window.tearDown assignment causes this case to happen which is
|
||||||
// only really for tests.
|
// only really for tests.
|
||||||
@ -1831,6 +1881,8 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
} else if (this.engineCommandManager?.engineConnection) {
|
} else if (this.engineCommandManager?.engineConnection) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.engineCommandManager?.engineConnection?.tearDown(opts)
|
this.engineCommandManager?.engineConnection?.tearDown(opts)
|
||||||
|
// @ts-ignore
|
||||||
|
this.engineCommandManager.engineConnection = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async startNewSession() {
|
async startNewSession() {
|
||||||
@ -1929,7 +1981,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
) {
|
) {
|
||||||
// highlight_set_entity, mouse_move and camera_drag_move are sent over the unreliable channel and are too noisy
|
// highlight_set_entity, mouse_move and camera_drag_move are sent over the unreliable channel and are too noisy
|
||||||
this.addCommandLog({
|
this.addCommandLog({
|
||||||
type: 'send-scene',
|
type: CommandLogType.SendScene,
|
||||||
data: command,
|
data: command,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1988,7 +2040,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
toastId,
|
toastId,
|
||||||
resolve: (passThrough) => {
|
resolve: (passThrough) => {
|
||||||
this.addCommandLog({
|
this.addCommandLog({
|
||||||
type: 'export-done',
|
type: CommandLogType.ExportDone,
|
||||||
data: null,
|
data: null,
|
||||||
})
|
})
|
||||||
resolve(passThrough)
|
resolve(passThrough)
|
||||||
|
@ -336,7 +336,7 @@ export class ProgramMemory {
|
|||||||
*/
|
*/
|
||||||
hasSketchOrSolid(): boolean {
|
hasSketchOrSolid(): boolean {
|
||||||
for (const node of this.visibleEntries().values()) {
|
for (const node of this.visibleEntries().values()) {
|
||||||
if (node.type === 'Solid' || node.value?.type === 'Sketch') {
|
if (node.type === 'Solid' || node.type === 'Sketch') {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -145,12 +145,6 @@ export const fileLoader: LoaderFunction = async (
|
|||||||
? await getProjectInfo(projectPath)
|
? await getProjectInfo(projectPath)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
console.log('maybeProjectInfo', {
|
|
||||||
maybeProjectInfo,
|
|
||||||
defaultProjectData,
|
|
||||||
projectPathData,
|
|
||||||
})
|
|
||||||
|
|
||||||
const projectData: IndexLoaderData = {
|
const projectData: IndexLoaderData = {
|
||||||
code,
|
code,
|
||||||
project: maybeProjectInfo ?? defaultProjectData,
|
project: maybeProjectInfo ?? defaultProjectData,
|
||||||
|
@ -118,6 +118,8 @@ export class Setting<T = unknown> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MS_IN_MINUTE = 1000 * 60
|
||||||
|
|
||||||
export function createSettings() {
|
export function createSettings() {
|
||||||
return {
|
return {
|
||||||
/** Settings that affect the behavior of the entire app,
|
/** Settings that affect the behavior of the entire app,
|
||||||
@ -181,13 +183,58 @@ export function createSettings() {
|
|||||||
/**
|
/**
|
||||||
* Stream resource saving behavior toggle
|
* Stream resource saving behavior toggle
|
||||||
*/
|
*/
|
||||||
streamIdleMode: new Setting<boolean>({
|
streamIdleMode: new Setting<number | undefined>({
|
||||||
defaultValue: false,
|
defaultValue: undefined,
|
||||||
description: 'Toggle stream idling, saving bandwidth and battery',
|
description: 'Toggle stream idling, saving bandwidth and battery',
|
||||||
validate: (v) => typeof v === 'boolean',
|
validate: (v) =>
|
||||||
commandConfig: {
|
v === undefined ||
|
||||||
inputType: 'boolean',
|
(typeof v === 'number' &&
|
||||||
},
|
v >= 1 * MS_IN_MINUTE &&
|
||||||
|
v <= 60 * MS_IN_MINUTE),
|
||||||
|
Component: ({ value, updateValue }) => (
|
||||||
|
<div className="flex item-center gap-4 px-2 m-0 py-0">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={value !== undefined}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateValue(
|
||||||
|
!e.currentTarget.checked ? undefined : 5 * MS_IN_MINUTE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="block w-4 h-4"
|
||||||
|
/>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col grow">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
onChange={(e) =>
|
||||||
|
updateValue(Number(e.currentTarget.value) * MS_IN_MINUTE)
|
||||||
|
}
|
||||||
|
disabled={value === undefined}
|
||||||
|
value={
|
||||||
|
value !== null && value !== undefined
|
||||||
|
? value / MS_IN_MINUTE
|
||||||
|
: 5
|
||||||
|
}
|
||||||
|
min={1}
|
||||||
|
max={60}
|
||||||
|
step={1}
|
||||||
|
className="block flex-1"
|
||||||
|
/>
|
||||||
|
{value !== undefined && value !== null && (
|
||||||
|
<div>
|
||||||
|
{value / MS_IN_MINUTE === 60
|
||||||
|
? '1 hour'
|
||||||
|
: value / MS_IN_MINUTE === 1
|
||||||
|
? '1 minute'
|
||||||
|
: value / MS_IN_MINUTE + ' minutes'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
onboardingStatus: new Setting<string>({
|
onboardingStatus: new Setting<string>({
|
||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
|
@ -24,6 +24,10 @@ import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration
|
|||||||
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||||
import { DeepPartial } from 'lib/types'
|
import { DeepPartial } from 'lib/types'
|
||||||
|
|
||||||
|
type OmitNull<T> = T extends null ? undefined : T
|
||||||
|
const toUndefinedIfNull = (a: any): OmitNull<any> =>
|
||||||
|
a === null ? undefined : a
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert from a rust settings struct into the JS settings struct.
|
* Convert from a rust settings struct into the JS settings struct.
|
||||||
* We do this because the JS settings type has all the fancy shit
|
* We do this because the JS settings type has all the fancy shit
|
||||||
@ -40,7 +44,9 @@ export function configurationToSettingsPayload(
|
|||||||
: undefined,
|
: undefined,
|
||||||
onboardingStatus: configuration?.settings?.app?.onboarding_status,
|
onboardingStatus: configuration?.settings?.app?.onboarding_status,
|
||||||
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
|
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
|
||||||
streamIdleMode: configuration?.settings?.app?.stream_idle_mode,
|
streamIdleMode: toUndefinedIfNull(
|
||||||
|
configuration?.settings?.app?.stream_idle_mode
|
||||||
|
),
|
||||||
projectDirectory: configuration?.settings?.project?.directory,
|
projectDirectory: configuration?.settings?.project?.directory,
|
||||||
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
|
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
|
||||||
},
|
},
|
||||||
@ -79,7 +85,9 @@ export function projectConfigurationToSettingsPayload(
|
|||||||
: undefined,
|
: undefined,
|
||||||
onboardingStatus: configuration?.settings?.app?.onboarding_status,
|
onboardingStatus: configuration?.settings?.app?.onboarding_status,
|
||||||
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
|
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
|
||||||
streamIdleMode: configuration?.settings?.app?.stream_idle_mode,
|
streamIdleMode: toUndefinedIfNull(
|
||||||
|
configuration?.settings?.app?.stream_idle_mode
|
||||||
|
),
|
||||||
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
|
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
|
||||||
},
|
},
|
||||||
modeling: {
|
modeling: {
|
||||||
|
@ -10,8 +10,14 @@ export const codeManager = new CodeManager()
|
|||||||
|
|
||||||
export const engineCommandManager = new EngineCommandManager()
|
export const engineCommandManager = new EngineCommandManager()
|
||||||
|
|
||||||
// Accessible for tests mostly
|
declare global {
|
||||||
// @ts-ignore
|
interface Window {
|
||||||
|
tearDown: typeof engineCommandManager.tearDown
|
||||||
|
sceneInfra: typeof sceneInfra
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accessible for tests
|
||||||
window.tearDown = engineCommandManager.tearDown
|
window.tearDown = engineCommandManager.tearDown
|
||||||
|
|
||||||
// This needs to be after codeManager is created.
|
// This needs to be after codeManager is created.
|
||||||
@ -21,7 +27,9 @@ engineCommandManager.kclManager = kclManager
|
|||||||
engineCommandManager.getAstCb = () => kclManager.ast
|
engineCommandManager.getAstCb = () => kclManager.ast
|
||||||
|
|
||||||
export const sceneInfra = new SceneInfra(engineCommandManager)
|
export const sceneInfra = new SceneInfra(engineCommandManager)
|
||||||
engineCommandManager.camControlsCameraChange = sceneInfra.onCameraChange
|
|
||||||
|
// Accessible for tests
|
||||||
|
window.sceneInfra = sceneInfra
|
||||||
|
|
||||||
export const sceneEntitiesManager = new SceneEntities(engineCommandManager)
|
export const sceneEntitiesManager = new SceneEntities(engineCommandManager)
|
||||||
|
|
||||||
|
3
src/lib/timings.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// 0.25s is the average visual reaction time for humans so we'll go a bit less
|
||||||
|
// so those exception people don't see.
|
||||||
|
export const REASONABLE_TIME_TO_REFRESH_STREAM_SIZE = 100
|
@ -91,7 +91,7 @@ export function useCalculateKclExpression({
|
|||||||
const _programMem: ProgramMemory = ProgramMemory.empty()
|
const _programMem: ProgramMemory = ProgramMemory.empty()
|
||||||
for (const { key, value } of availableVarInfo.variables) {
|
for (const { key, value } of availableVarInfo.variables) {
|
||||||
const error = _programMem.set(key, {
|
const error = _programMem.set(key, {
|
||||||
type: 'UserVal',
|
type: 'String',
|
||||||
value,
|
value,
|
||||||
__meta: [],
|
__meta: [],
|
||||||
})
|
})
|
||||||
@ -115,6 +115,7 @@ export function useCalculateKclExpression({
|
|||||||
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
|
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
|
||||||
init && setValueNode(init)
|
init && setValueNode(init)
|
||||||
}
|
}
|
||||||
|
if (!value) return
|
||||||
execAstAndSetResult().catch(() => {
|
execAstAndSetResult().catch(() => {
|
||||||
setCalcResult('NAN')
|
setCalcResult('NAN')
|
||||||
setValueNode(null)
|
setValueNode(null)
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
sceneEntitiesManager,
|
sceneEntitiesManager,
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
editorManager,
|
editorManager,
|
||||||
|
codeManager,
|
||||||
} from 'lib/singletons'
|
} from 'lib/singletons'
|
||||||
import {
|
import {
|
||||||
horzVertInfo,
|
horzVertInfo,
|
||||||
@ -531,8 +532,10 @@ export const modelingMachine = setup({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
'hide default planes': () => {
|
||||||
'hide default planes': () => kclManager.hidePlanes(),
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
kclManager.hidePlanes()
|
||||||
|
},
|
||||||
'reset sketch metadata': assign({
|
'reset sketch metadata': assign({
|
||||||
sketchDetails: null,
|
sketchDetails: null,
|
||||||
sketchEnginePathId: '',
|
sketchEnginePathId: '',
|
||||||
@ -595,7 +598,6 @@ export const modelingMachine = setup({
|
|||||||
if (trap(extrudeSketchRes)) return
|
if (trap(extrudeSketchRes)) return
|
||||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketchRes
|
const { modifiedAst, pathToExtrudeArg } = extrudeSketchRes
|
||||||
|
|
||||||
store.videoElement?.pause()
|
|
||||||
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
|
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
|
||||||
focusPath: [pathToExtrudeArg],
|
focusPath: [pathToExtrudeArg],
|
||||||
zoomToFit: true,
|
zoomToFit: true,
|
||||||
@ -604,11 +606,9 @@ export const modelingMachine = setup({
|
|||||||
type: 'path',
|
type: 'path',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (!engineCommandManager.engineConnection?.idleMode) {
|
|
||||||
store.videoElement?.play().catch((e) => {
|
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
|
||||||
console.warn('Video playing was prevented', e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (updatedAst?.selections) {
|
if (updatedAst?.selections) {
|
||||||
editorManager.selectRange(updatedAst?.selections)
|
editorManager.selectRange(updatedAst?.selections)
|
||||||
}
|
}
|
||||||
@ -642,7 +642,6 @@ export const modelingMachine = setup({
|
|||||||
if (trap(revolveSketchRes)) return
|
if (trap(revolveSketchRes)) return
|
||||||
const { modifiedAst, pathToRevolveArg } = revolveSketchRes
|
const { modifiedAst, pathToRevolveArg } = revolveSketchRes
|
||||||
|
|
||||||
store.videoElement?.pause()
|
|
||||||
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
|
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
|
||||||
focusPath: [pathToRevolveArg],
|
focusPath: [pathToRevolveArg],
|
||||||
zoomToFit: true,
|
zoomToFit: true,
|
||||||
@ -651,11 +650,9 @@ export const modelingMachine = setup({
|
|||||||
type: 'path',
|
type: 'path',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (!engineCommandManager.engineConnection?.idleMode) {
|
|
||||||
store.videoElement?.play().catch((e) => {
|
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
|
||||||
console.warn('Video playing was prevented', e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (updatedAst?.selections) {
|
if (updatedAst?.selections) {
|
||||||
editorManager.selectRange(updatedAst?.selections)
|
editorManager.selectRange(updatedAst?.selections)
|
||||||
}
|
}
|
||||||
@ -685,6 +682,7 @@ export const modelingMachine = setup({
|
|||||||
}
|
}
|
||||||
|
|
||||||
await kclManager.updateAst(modifiedAst, true)
|
await kclManager.updateAst(modifiedAst, true)
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(modifiedAst)
|
||||||
})().catch(reportRejection)
|
})().catch(reportRejection)
|
||||||
},
|
},
|
||||||
'AST fillet': ({ event }) => {
|
'AST fillet': ({ event }) => {
|
||||||
@ -702,6 +700,9 @@ export const modelingMachine = setup({
|
|||||||
radius
|
radius
|
||||||
)
|
)
|
||||||
if (err(applyFilletToSelectionResult)) return applyFilletToSelectionResult
|
if (err(applyFilletToSelectionResult)) return applyFilletToSelectionResult
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||||||
},
|
},
|
||||||
'set selection filter to curves only': () => {
|
'set selection filter to curves only': () => {
|
||||||
;(async () => {
|
;(async () => {
|
||||||
@ -758,25 +759,35 @@ export const modelingMachine = setup({
|
|||||||
'remove sketch grid': () => sceneEntitiesManager.removeSketchGrid(),
|
'remove sketch grid': () => sceneEntitiesManager.removeSketchGrid(),
|
||||||
'set up draft line': ({ context: { sketchDetails } }) => {
|
'set up draft line': ({ context: { sketchDetails } }) => {
|
||||||
if (!sketchDetails) return
|
if (!sketchDetails) return
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
sceneEntitiesManager.setUpDraftSegment(
|
sceneEntitiesManager
|
||||||
sketchDetails.sketchPathToNode,
|
.setupDraftSegment(
|
||||||
sketchDetails.zAxis,
|
sketchDetails.sketchPathToNode,
|
||||||
sketchDetails.yAxis,
|
sketchDetails.zAxis,
|
||||||
sketchDetails.origin,
|
sketchDetails.yAxis,
|
||||||
'line'
|
sketchDetails.origin,
|
||||||
)
|
'line'
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
'set up draft arc': ({ context: { sketchDetails } }) => {
|
'set up draft arc': ({ context: { sketchDetails } }) => {
|
||||||
if (!sketchDetails) return
|
if (!sketchDetails) return
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
sceneEntitiesManager.setUpDraftSegment(
|
sceneEntitiesManager
|
||||||
sketchDetails.sketchPathToNode,
|
.setupDraftSegment(
|
||||||
sketchDetails.zAxis,
|
sketchDetails.sketchPathToNode,
|
||||||
sketchDetails.yAxis,
|
sketchDetails.zAxis,
|
||||||
sketchDetails.origin,
|
sketchDetails.yAxis,
|
||||||
'tangentialArcTo'
|
sketchDetails.origin,
|
||||||
)
|
'tangentialArcTo'
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
'listen for rectangle origin': ({ context: { sketchDetails } }) => {
|
'listen for rectangle origin': ({ context: { sketchDetails } }) => {
|
||||||
if (!sketchDetails) return
|
if (!sketchDetails) return
|
||||||
@ -834,38 +845,53 @@ export const modelingMachine = setup({
|
|||||||
'set up draft rectangle': ({ context: { sketchDetails }, event }) => {
|
'set up draft rectangle': ({ context: { sketchDetails }, event }) => {
|
||||||
if (event.type !== 'Add rectangle origin') return
|
if (event.type !== 'Add rectangle origin') return
|
||||||
if (!sketchDetails || !event.data) return
|
if (!sketchDetails || !event.data) return
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
sceneEntitiesManager.setupDraftRectangle(
|
sceneEntitiesManager
|
||||||
sketchDetails.sketchPathToNode,
|
.setupDraftRectangle(
|
||||||
sketchDetails.zAxis,
|
sketchDetails.sketchPathToNode,
|
||||||
sketchDetails.yAxis,
|
sketchDetails.zAxis,
|
||||||
sketchDetails.origin,
|
sketchDetails.yAxis,
|
||||||
event.data
|
sketchDetails.origin,
|
||||||
)
|
event.data
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
'set up draft circle': ({ context: { sketchDetails }, event }) => {
|
'set up draft circle': ({ context: { sketchDetails }, event }) => {
|
||||||
if (event.type !== 'Add circle origin') return
|
if (event.type !== 'Add circle origin') return
|
||||||
if (!sketchDetails || !event.data) return
|
if (!sketchDetails || !event.data) return
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
sceneEntitiesManager.setupDraftCircle(
|
sceneEntitiesManager
|
||||||
sketchDetails.sketchPathToNode,
|
.setupDraftCircle(
|
||||||
sketchDetails.zAxis,
|
sketchDetails.sketchPathToNode,
|
||||||
sketchDetails.yAxis,
|
sketchDetails.zAxis,
|
||||||
sketchDetails.origin,
|
sketchDetails.yAxis,
|
||||||
event.data
|
sketchDetails.origin,
|
||||||
)
|
event.data
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
'set up draft line without teardown': ({ context: { sketchDetails } }) => {
|
'set up draft line without teardown': ({ context: { sketchDetails } }) => {
|
||||||
if (!sketchDetails) return
|
if (!sketchDetails) return
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
sceneEntitiesManager.setUpDraftSegment(
|
sceneEntitiesManager
|
||||||
sketchDetails.sketchPathToNode,
|
.setupDraftSegment(
|
||||||
sketchDetails.zAxis,
|
sketchDetails.sketchPathToNode,
|
||||||
sketchDetails.yAxis,
|
sketchDetails.zAxis,
|
||||||
sketchDetails.origin,
|
sketchDetails.yAxis,
|
||||||
'line',
|
sketchDetails.origin,
|
||||||
false
|
'line',
|
||||||
)
|
false
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
'show default planes': () => {
|
'show default planes': () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
@ -882,12 +908,17 @@ export const modelingMachine = setup({
|
|||||||
'add axis n grid': ({ context: { sketchDetails } }) => {
|
'add axis n grid': ({ context: { sketchDetails } }) => {
|
||||||
if (!sketchDetails) return
|
if (!sketchDetails) return
|
||||||
if (localStorage.getItem('disableAxis')) return
|
if (localStorage.getItem('disableAxis')) return
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
sceneEntitiesManager.createSketchAxis(
|
sceneEntitiesManager.createSketchAxis(
|
||||||
sketchDetails.sketchPathToNode || [],
|
sketchDetails.sketchPathToNode || [],
|
||||||
sketchDetails.zAxis,
|
sketchDetails.zAxis,
|
||||||
sketchDetails.yAxis,
|
sketchDetails.yAxis,
|
||||||
sketchDetails.origin
|
sketchDetails.origin
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||||||
},
|
},
|
||||||
'reset client scene mouse handlers': () => {
|
'reset client scene mouse handlers': () => {
|
||||||
// when not in sketch mode we don't need any mouse listeners
|
// when not in sketch mode we don't need any mouse listeners
|
||||||
@ -916,10 +947,13 @@ export const modelingMachine = setup({
|
|||||||
'Delete segment': ({ context: { sketchDetails }, event }) => {
|
'Delete segment': ({ context: { sketchDetails }, event }) => {
|
||||||
if (event.type !== 'Delete segment') return
|
if (event.type !== 'Delete segment') return
|
||||||
if (!sketchDetails || !event.data) return
|
if (!sketchDetails || !event.data) return
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
deleteSegment({
|
deleteSegment({
|
||||||
pathToNode: event.data,
|
pathToNode: event.data,
|
||||||
sketchDetails,
|
sketchDetails,
|
||||||
|
}).then(() => {
|
||||||
|
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
'Reset Segment Overlays': () => sceneEntitiesManager.resetOverlays(),
|
'Reset Segment Overlays': () => sceneEntitiesManager.resetOverlays(),
|
||||||
@ -984,6 +1018,9 @@ export const modelingMachine = setup({
|
|||||||
)
|
)
|
||||||
if (trap(updatedAst, { suppress: true })) return
|
if (trap(updatedAst, { suppress: true })) return
|
||||||
if (!updatedAst) return
|
if (!updatedAst) return
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectionType: 'completeSelection',
|
selectionType: 'completeSelection',
|
||||||
selection: updateSelections(
|
selection: updateSelections(
|
||||||
@ -1018,6 +1055,7 @@ export const modelingMachine = setup({
|
|||||||
)
|
)
|
||||||
if (trap(updatedAst, { suppress: true })) return
|
if (trap(updatedAst, { suppress: true })) return
|
||||||
if (!updatedAst) return
|
if (!updatedAst) return
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
|
||||||
return {
|
return {
|
||||||
selectionType: 'completeSelection',
|
selectionType: 'completeSelection',
|
||||||
selection: updateSelections(
|
selection: updateSelections(
|
||||||
@ -1052,6 +1090,7 @@ export const modelingMachine = setup({
|
|||||||
)
|
)
|
||||||
if (trap(updatedAst, { suppress: true })) return
|
if (trap(updatedAst, { suppress: true })) return
|
||||||
if (!updatedAst) return
|
if (!updatedAst) return
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
|
||||||
return {
|
return {
|
||||||
selectionType: 'completeSelection',
|
selectionType: 'completeSelection',
|
||||||
selection: updateSelections(
|
selection: updateSelections(
|
||||||
@ -1084,6 +1123,7 @@ export const modelingMachine = setup({
|
|||||||
)
|
)
|
||||||
if (trap(updatedAst, { suppress: true })) return
|
if (trap(updatedAst, { suppress: true })) return
|
||||||
if (!updatedAst) return
|
if (!updatedAst) return
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
|
||||||
const updatedSelectionRanges = updateSelections(
|
const updatedSelectionRanges = updateSelections(
|
||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -1117,6 +1157,7 @@ export const modelingMachine = setup({
|
|||||||
)
|
)
|
||||||
if (trap(updatedAst, { suppress: true })) return
|
if (trap(updatedAst, { suppress: true })) return
|
||||||
if (!updatedAst) return
|
if (!updatedAst) return
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
|
||||||
const updatedSelectionRanges = updateSelections(
|
const updatedSelectionRanges = updateSelections(
|
||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -1150,6 +1191,7 @@ export const modelingMachine = setup({
|
|||||||
)
|
)
|
||||||
if (trap(updatedAst, { suppress: true })) return
|
if (trap(updatedAst, { suppress: true })) return
|
||||||
if (!updatedAst) return
|
if (!updatedAst) return
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
|
||||||
const updatedSelectionRanges = updateSelections(
|
const updatedSelectionRanges = updateSelections(
|
||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -1183,6 +1225,7 @@ export const modelingMachine = setup({
|
|||||||
)
|
)
|
||||||
if (trap(updatedAst, { suppress: true })) return
|
if (trap(updatedAst, { suppress: true })) return
|
||||||
if (!updatedAst) return
|
if (!updatedAst) return
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
|
||||||
const updatedSelectionRanges = updateSelections(
|
const updatedSelectionRanges = updateSelections(
|
||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -1220,6 +1263,8 @@ export const modelingMachine = setup({
|
|||||||
)
|
)
|
||||||
if (trap(updatedAst, { suppress: true })) return
|
if (trap(updatedAst, { suppress: true })) return
|
||||||
if (!updatedAst) return
|
if (!updatedAst) return
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
|
||||||
|
|
||||||
const updatedSelectionRanges = updateSelections(
|
const updatedSelectionRanges = updateSelections(
|
||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -1252,6 +1297,7 @@ export const modelingMachine = setup({
|
|||||||
)
|
)
|
||||||
if (trap(updatedAst, { suppress: true })) return
|
if (trap(updatedAst, { suppress: true })) return
|
||||||
if (!updatedAst) return
|
if (!updatedAst) return
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
|
||||||
const updatedSelectionRanges = updateSelections(
|
const updatedSelectionRanges = updateSelections(
|
||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -1556,7 +1602,7 @@ export const modelingMachine = setup({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
entry: 'setup client side sketch segments',
|
entry: ['setup client side sketch segments'],
|
||||||
},
|
},
|
||||||
|
|
||||||
'Await horizontal distance info': {
|
'Await horizontal distance info': {
|
||||||
@ -1801,7 +1847,7 @@ export const modelingMachine = setup({
|
|||||||
onError: 'SketchIdle',
|
onError: 'SketchIdle',
|
||||||
onDone: {
|
onDone: {
|
||||||
target: 'SketchIdle',
|
target: 'SketchIdle',
|
||||||
actions: ['Set selection'],
|
actions: 'Set selection',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
2
src/wasm-lib/Cargo.lock
generated
@ -737,7 +737,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive-docs"
|
name = "derive-docs"
|
||||||
version = "0.1.29"
|
version = "0.1.30"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"Inflector",
|
"Inflector",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "derive-docs"
|
name = "derive-docs"
|
||||||
description = "A tool for generating documentation from Rust derive macros"
|
description = "A tool for generating documentation from Rust derive macros"
|
||||||
version = "0.1.29"
|
version = "0.1.30"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/KittyCAD/modeling-app"
|
repository = "https://github.com/KittyCAD/modeling-app"
|
||||||
|
@ -27,7 +27,7 @@ pub use crate::ast::types::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
docs::StdLibFn,
|
docs::StdLibFn,
|
||||||
errors::KclError,
|
errors::KclError,
|
||||||
executor::{ExecState, ExecutorContext, KclValue, Metadata, SourceRange, TagIdentifier, UserVal},
|
executor::{ExecState, ExecutorContext, KclValue, Metadata, SourceRange, TagIdentifier},
|
||||||
parser::PIPE_OPERATOR,
|
parser::PIPE_OPERATOR,
|
||||||
std::kcl_stdlib::KclStdLibFn,
|
std::kcl_stdlib::KclStdLibFn,
|
||||||
};
|
};
|
||||||
@ -59,6 +59,14 @@ pub struct Node<T> {
|
|||||||
pub module_id: ModuleId,
|
pub module_id: ModuleId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> Node<T> {
|
||||||
|
pub fn metadata(&self) -> Metadata {
|
||||||
|
Metadata {
|
||||||
|
source_range: SourceRange([self.start, self.end, self.module_id.0 as usize]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T: JsonSchema> schemars::JsonSchema for Node<T> {
|
impl<T: JsonSchema> schemars::JsonSchema for Node<T> {
|
||||||
fn schema_name() -> String {
|
fn schema_name() -> String {
|
||||||
T::schema_name()
|
T::schema_name()
|
||||||
@ -1708,34 +1716,26 @@ impl Literal {
|
|||||||
|
|
||||||
impl From<Node<Literal>> for KclValue {
|
impl From<Node<Literal>> for KclValue {
|
||||||
fn from(literal: Node<Literal>) -> Self {
|
fn from(literal: Node<Literal>) -> Self {
|
||||||
KclValue::UserVal(UserVal {
|
let meta = vec![literal.metadata()];
|
||||||
value: JValue::from(literal.value.clone()),
|
match literal.inner.value {
|
||||||
meta: vec![Metadata {
|
LiteralValue::IInteger(value) => KclValue::Int { value, meta },
|
||||||
source_range: literal.into(),
|
LiteralValue::Fractional(value) => KclValue::Number { value, meta },
|
||||||
}],
|
LiteralValue::String(value) => KclValue::String { value, meta },
|
||||||
})
|
LiteralValue::Bool(value) => KclValue::Bool { value, meta },
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&Node<Literal>> for KclValue {
|
impl From<&Node<Literal>> for KclValue {
|
||||||
fn from(literal: &Node<Literal>) -> Self {
|
fn from(literal: &Node<Literal>) -> Self {
|
||||||
KclValue::UserVal(UserVal {
|
Self::from(literal.to_owned())
|
||||||
value: JValue::from(literal.value.clone()),
|
|
||||||
meta: vec![Metadata {
|
|
||||||
source_range: literal.into(),
|
|
||||||
}],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&BoxNode<Literal>> for KclValue {
|
impl From<&BoxNode<Literal>> for KclValue {
|
||||||
fn from(literal: &BoxNode<Literal>) -> Self {
|
fn from(literal: &BoxNode<Literal>) -> Self {
|
||||||
KclValue::UserVal(UserVal {
|
let b: &Node<Literal> = literal;
|
||||||
value: JValue::from(literal.value.clone()),
|
Self::from(b)
|
||||||
meta: vec![Metadata {
|
|
||||||
source_range: literal.into(),
|
|
||||||
}],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3010,17 +3010,6 @@ impl ConstraintLevels {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn human_friendly_type(j: &JValue) -> &'static str {
|
|
||||||
match j {
|
|
||||||
JValue::Null => "null",
|
|
||||||
JValue::Bool(_) => "boolean (true/false value)",
|
|
||||||
JValue::Number(_) => "number",
|
|
||||||
JValue::String(_) => "string (text)",
|
|
||||||
JValue::Array(_) => "array (list)",
|
|
||||||
JValue::Object(_) => "object",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
@ -1,18 +1,21 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
human_friendly_type, ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart,
|
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, CallExpression, Expr,
|
||||||
CallExpression, Expr, IfExpression, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Node,
|
IfExpression, KclNone, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Node, ObjectExpression,
|
||||||
ObjectExpression, TagDeclarator, UnaryExpression, UnaryOperator,
|
TagDeclarator, UnaryExpression, UnaryOperator,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
errors::{KclError, KclErrorDetails},
|
errors::{KclError, KclErrorDetails},
|
||||||
executor::{
|
executor::{
|
||||||
BodyType, ExecState, ExecutorContext, KclValue, Metadata, Sketch, SourceRange, StatementKind, TagEngineInfo,
|
BodyType, ExecState, ExecutorContext, KclValue, Metadata, SourceRange, StatementKind, TagEngineInfo,
|
||||||
TagIdentifier, UserVal,
|
TagIdentifier,
|
||||||
},
|
},
|
||||||
std::FunctionKind,
|
std::FunctionKind,
|
||||||
};
|
};
|
||||||
use async_recursion::async_recursion;
|
use async_recursion::async_recursion;
|
||||||
use serde_json::Value as JValue;
|
|
||||||
|
const FLOAT_TO_INT_MAX_DELTA: f64 = 0.01;
|
||||||
|
|
||||||
impl BinaryPart {
|
impl BinaryPart {
|
||||||
#[async_recursion]
|
#[async_recursion]
|
||||||
@ -42,26 +45,19 @@ impl Node<MemberExpression> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let array_json = array.get_json_value()?;
|
let KclValue::Array { value: array, meta: _ } = array else {
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
if let serde_json::Value::Array(array) = array_json {
|
|
||||||
if let Some(value) = array.get(index) {
|
|
||||||
Ok(KclValue::UserVal(UserVal {
|
|
||||||
value: value.clone(),
|
|
||||||
meta: vec![Metadata {
|
|
||||||
source_range: self.into(),
|
|
||||||
}],
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
Err(KclError::UndefinedValue(KclErrorDetails {
|
|
||||||
message: format!("index {} not found in array", index),
|
|
||||||
source_ranges: vec![self.clone().into()],
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(KclError::Semantic(KclErrorDetails {
|
|
||||||
message: format!("MemberExpression array is not an array: {:?}", array),
|
message: format!("MemberExpression array is not an array: {:?}", array),
|
||||||
source_ranges: vec![self.clone().into()],
|
source_ranges: vec![self.clone().into()],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(value) = array.get(index) {
|
||||||
|
Ok(value.to_owned())
|
||||||
|
} else {
|
||||||
|
Err(KclError::UndefinedValue(KclErrorDetails {
|
||||||
|
message: format!("index {} not found in array", index),
|
||||||
|
source_ranges: vec![self.clone().into()],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,18 +73,11 @@ impl Node<MemberExpression> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let object_json = object.get_json_value()?;
|
|
||||||
|
|
||||||
// Check the property and object match -- e.g. ints for arrays, strs for objects.
|
// Check the property and object match -- e.g. ints for arrays, strs for objects.
|
||||||
match (object_json, property) {
|
match (object, property) {
|
||||||
(JValue::Object(map), Property::String(property)) => {
|
(KclValue::Object { value: map, meta: _ }, Property::String(property)) => {
|
||||||
if let Some(value) = map.get(&property) {
|
if let Some(value) = map.get(&property) {
|
||||||
Ok(KclValue::UserVal(UserVal {
|
Ok(value.to_owned())
|
||||||
value: value.clone(),
|
|
||||||
meta: vec![Metadata {
|
|
||||||
source_range: self.into(),
|
|
||||||
}],
|
|
||||||
}))
|
|
||||||
} else {
|
} else {
|
||||||
Err(KclError::UndefinedValue(KclErrorDetails {
|
Err(KclError::UndefinedValue(KclErrorDetails {
|
||||||
message: format!("Property '{property}' not found in object"),
|
message: format!("Property '{property}' not found in object"),
|
||||||
@ -96,22 +85,20 @@ impl Node<MemberExpression> {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(JValue::Object(_), p) => Err(KclError::Semantic(KclErrorDetails {
|
(KclValue::Object { .. }, p) => {
|
||||||
message: format!(
|
let t = p.type_name();
|
||||||
"Only strings can be used as the property of an object, but you're using a {}",
|
let article = article_for(t);
|
||||||
p.type_name()
|
Err(KclError::Semantic(KclErrorDetails {
|
||||||
),
|
message: format!(
|
||||||
source_ranges: vec![self.clone().into()],
|
"Only strings can be used as the property of an object, but you're using {article} {t}",
|
||||||
})),
|
),
|
||||||
(JValue::Array(arr), Property::Number(index)) => {
|
source_ranges: vec![self.clone().into()],
|
||||||
let value_of_arr: Option<&JValue> = arr.get(index);
|
}))
|
||||||
|
}
|
||||||
|
(KclValue::Array { value: arr, meta: _ }, Property::Number(index)) => {
|
||||||
|
let value_of_arr = arr.get(index);
|
||||||
if let Some(value) = value_of_arr {
|
if let Some(value) = value_of_arr {
|
||||||
Ok(KclValue::UserVal(UserVal {
|
Ok(value.to_owned())
|
||||||
value: value.clone(),
|
|
||||||
meta: vec![Metadata {
|
|
||||||
source_range: self.into(),
|
|
||||||
}],
|
|
||||||
}))
|
|
||||||
} else {
|
} else {
|
||||||
Err(KclError::UndefinedValue(KclErrorDetails {
|
Err(KclError::UndefinedValue(KclErrorDetails {
|
||||||
message: format!("The array doesn't have any item at index {index}"),
|
message: format!("The array doesn't have any item at index {index}"),
|
||||||
@ -119,17 +106,36 @@ impl Node<MemberExpression> {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(JValue::Array(_), p) => Err(KclError::Semantic(KclErrorDetails {
|
(KclValue::Array { .. }, p) => {
|
||||||
message: format!(
|
let t = p.type_name();
|
||||||
"Only integers >= 0 can be used as the index of an array, but you're using a {}",
|
let article = article_for(t);
|
||||||
p.type_name()
|
|
||||||
),
|
|
||||||
source_ranges: vec![self.clone().into()],
|
|
||||||
})),
|
|
||||||
(being_indexed, _) => {
|
|
||||||
let t = human_friendly_type(&being_indexed);
|
|
||||||
Err(KclError::Semantic(KclErrorDetails {
|
Err(KclError::Semantic(KclErrorDetails {
|
||||||
message: format!("Only arrays and objects can be indexed, but you're trying to index a {t}"),
|
message: format!(
|
||||||
|
"Only integers >= 0 can be used as the index of an array, but you're using {article} {t}",
|
||||||
|
),
|
||||||
|
source_ranges: vec![self.clone().into()],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
(KclValue::Solid(solid), Property::String(prop)) if prop == "sketch" => Ok(KclValue::Sketch {
|
||||||
|
value: Box::new(solid.sketch),
|
||||||
|
}),
|
||||||
|
(KclValue::Sketch { value: sk }, Property::String(prop)) if prop == "tags" => Ok(KclValue::Object {
|
||||||
|
meta: vec![Metadata {
|
||||||
|
source_range: SourceRange::from(self.clone()),
|
||||||
|
}],
|
||||||
|
value: sk
|
||||||
|
.tags
|
||||||
|
.iter()
|
||||||
|
.map(|(k, tag)| (k.to_owned(), KclValue::TagIdentifier(Box::new(tag.to_owned()))))
|
||||||
|
.collect(),
|
||||||
|
}),
|
||||||
|
(being_indexed, _) => {
|
||||||
|
let t = being_indexed.human_friendly_type();
|
||||||
|
let article = article_for(t);
|
||||||
|
Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!(
|
||||||
|
"Only arrays and objects can be indexed, but you're trying to index {article} {t}"
|
||||||
|
),
|
||||||
source_ranges: vec![self.clone().into()],
|
source_ranges: vec![self.clone().into()],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -140,81 +146,134 @@ impl Node<MemberExpression> {
|
|||||||
impl Node<BinaryExpression> {
|
impl Node<BinaryExpression> {
|
||||||
#[async_recursion]
|
#[async_recursion]
|
||||||
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
||||||
let left_json_value = self.left.get_result(exec_state, ctx).await?.get_json_value()?;
|
let left_value = self.left.get_result(exec_state, ctx).await?;
|
||||||
let right_json_value = self.right.get_result(exec_state, ctx).await?.get_json_value()?;
|
let right_value = self.right.get_result(exec_state, ctx).await?;
|
||||||
|
let mut meta = left_value.metadata();
|
||||||
|
meta.extend(right_value.metadata());
|
||||||
|
|
||||||
// First check if we are doing string concatenation.
|
// First check if we are doing string concatenation.
|
||||||
if self.operator == BinaryOperator::Add {
|
if self.operator == BinaryOperator::Add {
|
||||||
if let (Some(left), Some(right)) = (
|
if let (KclValue::String { value: left, meta: _ }, KclValue::String { value: right, meta: _ }) =
|
||||||
parse_json_value_as_string(&left_json_value),
|
(&left_value, &right_value)
|
||||||
parse_json_value_as_string(&right_json_value),
|
{
|
||||||
) {
|
return Ok(KclValue::String {
|
||||||
let value = serde_json::Value::String(format!("{}{}", left, right));
|
value: format!("{}{}", left, right),
|
||||||
return Ok(KclValue::UserVal(UserVal {
|
meta,
|
||||||
value,
|
});
|
||||||
meta: vec![Metadata {
|
|
||||||
source_range: self.into(),
|
|
||||||
}],
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let left = parse_json_number_as_f64(&left_json_value, self.left.clone().into())?;
|
let left = parse_number_as_f64(&left_value, self.left.clone().into())?;
|
||||||
let right = parse_json_number_as_f64(&right_json_value, self.right.clone().into())?;
|
let right = parse_number_as_f64(&right_value, self.right.clone().into())?;
|
||||||
|
|
||||||
let value: serde_json::Value = match self.operator {
|
let value = match self.operator {
|
||||||
BinaryOperator::Add => (left + right).into(),
|
BinaryOperator::Add => KclValue::Number {
|
||||||
BinaryOperator::Sub => (left - right).into(),
|
value: left + right,
|
||||||
BinaryOperator::Mul => (left * right).into(),
|
meta,
|
||||||
BinaryOperator::Div => (left / right).into(),
|
},
|
||||||
BinaryOperator::Mod => (left % right).into(),
|
BinaryOperator::Sub => KclValue::Number {
|
||||||
BinaryOperator::Pow => (left.powf(right)).into(),
|
value: left - right,
|
||||||
BinaryOperator::Eq => (left == right).into(),
|
meta,
|
||||||
BinaryOperator::Neq => (left != right).into(),
|
},
|
||||||
BinaryOperator::Gt => (left > right).into(),
|
BinaryOperator::Mul => KclValue::Number {
|
||||||
BinaryOperator::Gte => (left >= right).into(),
|
value: left * right,
|
||||||
BinaryOperator::Lt => (left < right).into(),
|
meta,
|
||||||
BinaryOperator::Lte => (left <= right).into(),
|
},
|
||||||
|
BinaryOperator::Div => KclValue::Number {
|
||||||
|
value: left / right,
|
||||||
|
meta,
|
||||||
|
},
|
||||||
|
BinaryOperator::Mod => KclValue::Number {
|
||||||
|
value: left % right,
|
||||||
|
meta,
|
||||||
|
},
|
||||||
|
BinaryOperator::Pow => KclValue::Number {
|
||||||
|
value: left.powf(right),
|
||||||
|
meta,
|
||||||
|
},
|
||||||
|
BinaryOperator::Neq => KclValue::Bool {
|
||||||
|
value: left != right,
|
||||||
|
meta,
|
||||||
|
},
|
||||||
|
BinaryOperator::Gt => KclValue::Bool {
|
||||||
|
value: left > right,
|
||||||
|
meta,
|
||||||
|
},
|
||||||
|
BinaryOperator::Gte => KclValue::Bool {
|
||||||
|
value: left >= right,
|
||||||
|
meta,
|
||||||
|
},
|
||||||
|
BinaryOperator::Lt => KclValue::Bool {
|
||||||
|
value: left < right,
|
||||||
|
meta,
|
||||||
|
},
|
||||||
|
BinaryOperator::Lte => KclValue::Bool {
|
||||||
|
value: left <= right,
|
||||||
|
meta,
|
||||||
|
},
|
||||||
|
BinaryOperator::Eq => KclValue::Bool {
|
||||||
|
value: left == right,
|
||||||
|
meta,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(KclValue::UserVal(UserVal {
|
Ok(value)
|
||||||
value,
|
|
||||||
meta: vec![Metadata {
|
|
||||||
source_range: self.into(),
|
|
||||||
}],
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Node<UnaryExpression> {
|
impl Node<UnaryExpression> {
|
||||||
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
||||||
if self.operator == UnaryOperator::Not {
|
if self.operator == UnaryOperator::Not {
|
||||||
let value = self.argument.get_result(exec_state, ctx).await?.get_json_value()?;
|
let value = self.argument.get_result(exec_state, ctx).await?;
|
||||||
let Some(bool_value) = json_as_bool(&value) else {
|
let KclValue::Bool {
|
||||||
|
value: bool_value,
|
||||||
|
meta: _,
|
||||||
|
} = value
|
||||||
|
else {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
message: format!("Cannot apply unary operator ! to non-boolean value: {}", value),
|
message: format!(
|
||||||
|
"Cannot apply unary operator ! to non-boolean value: {}",
|
||||||
|
value.human_friendly_type()
|
||||||
|
),
|
||||||
source_ranges: vec![self.into()],
|
source_ranges: vec![self.into()],
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
let negated = !bool_value;
|
let meta = vec![Metadata {
|
||||||
return Ok(KclValue::UserVal(UserVal {
|
source_range: self.into(),
|
||||||
value: serde_json::Value::Bool(negated),
|
}];
|
||||||
meta: vec![Metadata {
|
let negated = KclValue::Bool {
|
||||||
source_range: self.into(),
|
value: !bool_value,
|
||||||
}],
|
meta,
|
||||||
}));
|
};
|
||||||
|
|
||||||
|
return Ok(negated);
|
||||||
}
|
}
|
||||||
|
|
||||||
let num = parse_json_number_as_f64(
|
let value = &self.argument.get_result(exec_state, ctx).await?;
|
||||||
&self.argument.get_result(exec_state, ctx).await?.get_json_value()?,
|
match value {
|
||||||
self.into(),
|
KclValue::Number { value, meta: _ } => {
|
||||||
)?;
|
let meta = vec![Metadata {
|
||||||
Ok(KclValue::UserVal(UserVal {
|
source_range: self.into(),
|
||||||
value: (-(num)).into(),
|
}];
|
||||||
meta: vec![Metadata {
|
Ok(KclValue::Number { value: -value, meta })
|
||||||
source_range: self.into(),
|
}
|
||||||
}],
|
KclValue::Int { value, meta: _ } => {
|
||||||
}))
|
let meta = vec![Metadata {
|
||||||
|
source_range: self.into(),
|
||||||
|
}];
|
||||||
|
Ok(KclValue::Number {
|
||||||
|
value: (-value) as f64,
|
||||||
|
meta,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!(
|
||||||
|
"You can only negate numbers, but this is a {}",
|
||||||
|
value.human_friendly_type()
|
||||||
|
),
|
||||||
|
source_ranges: vec![self.into()],
|
||||||
|
})),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -325,13 +384,10 @@ impl Node<CallExpression> {
|
|||||||
// 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
|
||||||
// and it works.
|
// and it works.
|
||||||
match result {
|
match result {
|
||||||
KclValue::UserVal(ref mut uval) => {
|
KclValue::Sketch { value: ref mut sketch } => {
|
||||||
uval.mutate(|sketch: &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(&tag.value, tag.clone())?;
|
}
|
||||||
}
|
|
||||||
Ok::<_, KclError>(())
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
KclValue::Solid(ref mut solid) => {
|
KclValue::Solid(ref mut solid) => {
|
||||||
for value in &solid.value {
|
for value in &solid.value {
|
||||||
@ -425,10 +481,10 @@ impl Node<CallExpression> {
|
|||||||
} else {
|
} else {
|
||||||
fn_memory.add(
|
fn_memory.add(
|
||||||
¶m.identifier.name,
|
¶m.identifier.name,
|
||||||
KclValue::UserVal(UserVal {
|
KclValue::KclNone {
|
||||||
value: serde_json::value::Value::Null,
|
value: KclNone::new(),
|
||||||
meta: Default::default(),
|
meta: vec![self.into()],
|
||||||
}),
|
},
|
||||||
param.identifier.clone().into(),
|
param.identifier.clone().into(),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
@ -531,15 +587,13 @@ impl Node<ArrayExpression> {
|
|||||||
.execute_expr(element, exec_state, &metadata, StatementKind::Expression)
|
.execute_expr(element, exec_state, &metadata, StatementKind::Expression)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
results.push(value.get_json_value()?);
|
results.push(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(KclValue::UserVal(UserVal {
|
Ok(KclValue::Array {
|
||||||
value: results.into(),
|
value: results,
|
||||||
meta: vec![Metadata {
|
meta: vec![self.into()],
|
||||||
source_range: self.into(),
|
})
|
||||||
}],
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -549,15 +603,19 @@ impl Node<ArrayRangeExpression> {
|
|||||||
let metadata = Metadata::from(&self.start_element);
|
let metadata = Metadata::from(&self.start_element);
|
||||||
let start = ctx
|
let start = ctx
|
||||||
.execute_expr(&self.start_element, exec_state, &metadata, StatementKind::Expression)
|
.execute_expr(&self.start_element, exec_state, &metadata, StatementKind::Expression)
|
||||||
.await?
|
.await?;
|
||||||
.get_json_value()?;
|
let start = start.as_int().ok_or(KclError::Semantic(KclErrorDetails {
|
||||||
let start = parse_json_number_as_i64(&start, (&self.start_element).into())?;
|
source_ranges: vec![self.into()],
|
||||||
|
message: format!("Expected int but found {}", start.human_friendly_type()),
|
||||||
|
}))?;
|
||||||
let metadata = Metadata::from(&self.end_element);
|
let metadata = Metadata::from(&self.end_element);
|
||||||
let end = ctx
|
let end = ctx
|
||||||
.execute_expr(&self.end_element, exec_state, &metadata, StatementKind::Expression)
|
.execute_expr(&self.end_element, exec_state, &metadata, StatementKind::Expression)
|
||||||
.await?
|
.await?;
|
||||||
.get_json_value()?;
|
let end = end.as_int().ok_or(KclError::Semantic(KclErrorDetails {
|
||||||
let end = parse_json_number_as_i64(&end, (&self.end_element).into())?;
|
source_ranges: vec![self.into()],
|
||||||
|
message: format!("Expected int but found {}", end.human_friendly_type()),
|
||||||
|
}))?;
|
||||||
|
|
||||||
if end < start {
|
if end < start {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
@ -567,94 +625,76 @@ impl Node<ArrayRangeExpression> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let range: Vec<_> = if self.end_inclusive {
|
let range: Vec<_> = if self.end_inclusive {
|
||||||
(start..=end).map(JValue::from).collect()
|
(start..=end).collect()
|
||||||
} else {
|
} else {
|
||||||
(start..end).map(JValue::from).collect()
|
(start..end).collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(KclValue::UserVal(UserVal {
|
let meta = vec![Metadata {
|
||||||
value: range.into(),
|
source_range: self.into(),
|
||||||
meta: vec![Metadata {
|
}];
|
||||||
source_range: self.into(),
|
Ok(KclValue::Array {
|
||||||
}],
|
value: range
|
||||||
}))
|
.into_iter()
|
||||||
|
.map(|num| KclValue::Int {
|
||||||
|
value: num,
|
||||||
|
meta: meta.clone(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
meta,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Node<ObjectExpression> {
|
impl Node<ObjectExpression> {
|
||||||
#[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 mut object = serde_json::Map::new();
|
let mut object = HashMap::with_capacity(self.properties.len());
|
||||||
for property in &self.properties {
|
for property in &self.properties {
|
||||||
let metadata = Metadata::from(&property.value);
|
let metadata = Metadata::from(&property.value);
|
||||||
let result = ctx
|
let result = ctx
|
||||||
.execute_expr(&property.value, exec_state, &metadata, StatementKind::Expression)
|
.execute_expr(&property.value, exec_state, &metadata, StatementKind::Expression)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
object.insert(property.key.name.clone(), result.get_json_value()?);
|
object.insert(property.key.name.clone(), result);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(KclValue::UserVal(UserVal {
|
Ok(KclValue::Object {
|
||||||
value: object.into(),
|
value: object,
|
||||||
meta: vec![Metadata {
|
meta: vec![Metadata {
|
||||||
source_range: self.into(),
|
source_range: self.into(),
|
||||||
}],
|
}],
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_json_number_as_i64(j: &serde_json::Value, source_range: SourceRange) -> Result<i64, KclError> {
|
|
||||||
if let serde_json::Value::Number(n) = &j {
|
|
||||||
n.as_i64().ok_or_else(|| {
|
|
||||||
KclError::Syntax(KclErrorDetails {
|
|
||||||
source_ranges: vec![source_range],
|
|
||||||
message: format!("Invalid integer: {}", j),
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn article_for(s: &str) -> &'static str {
|
||||||
|
if s.starts_with(['a', 'e', 'i', 'o', 'u']) {
|
||||||
|
"an"
|
||||||
} else {
|
} else {
|
||||||
Err(KclError::Syntax(KclErrorDetails {
|
"a"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_number_as_f64(v: &KclValue, source_range: SourceRange) -> Result<f64, KclError> {
|
||||||
|
if let KclValue::Number { value: n, .. } = &v {
|
||||||
|
Ok(*n)
|
||||||
|
} else if let KclValue::Int { value: n, .. } = &v {
|
||||||
|
Ok(*n as f64)
|
||||||
|
} else {
|
||||||
|
let actual_type = v.human_friendly_type();
|
||||||
|
let article = if actual_type.starts_with(['a', 'e', 'i', 'o', 'u']) {
|
||||||
|
"an"
|
||||||
|
} else {
|
||||||
|
"a"
|
||||||
|
};
|
||||||
|
Err(KclError::Semantic(KclErrorDetails {
|
||||||
source_ranges: vec![source_range],
|
source_ranges: vec![source_range],
|
||||||
message: format!("Invalid integer: {}", j),
|
message: format!("Expected a number, but found {article} {actual_type}",),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_json_number_as_f64(j: &serde_json::Value, source_range: SourceRange) -> Result<f64, KclError> {
|
|
||||||
if let serde_json::Value::Number(n) = &j {
|
|
||||||
n.as_f64().ok_or_else(|| {
|
|
||||||
KclError::Syntax(KclErrorDetails {
|
|
||||||
source_ranges: vec![source_range],
|
|
||||||
message: format!("Invalid number: {}", j),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(KclError::Syntax(KclErrorDetails {
|
|
||||||
source_ranges: vec![source_range],
|
|
||||||
message: format!("Invalid number: {}", j),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_json_value_as_string(j: &serde_json::Value) -> Option<String> {
|
|
||||||
if let serde_json::Value::String(n) = &j {
|
|
||||||
Some(n.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// JSON value as bool. If it isn't a bool, returns None.
|
|
||||||
pub fn json_as_bool(j: &serde_json::Value) -> Option<bool> {
|
|
||||||
match j {
|
|
||||||
JValue::Null => None,
|
|
||||||
JValue::Bool(b) => Some(*b),
|
|
||||||
JValue::Number(_) => None,
|
|
||||||
JValue::String(_) => None,
|
|
||||||
JValue::Array(_) => None,
|
|
||||||
JValue::Object(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Node<IfExpression> {
|
impl Node<IfExpression> {
|
||||||
#[async_recursion]
|
#[async_recursion]
|
||||||
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
||||||
@ -724,15 +764,7 @@ impl Property {
|
|||||||
} else {
|
} else {
|
||||||
// Actually evaluate memory to compute the property.
|
// Actually evaluate memory to compute the property.
|
||||||
let prop = exec_state.memory.get(name, property_src)?;
|
let prop = exec_state.memory.get(name, property_src)?;
|
||||||
let KclValue::UserVal(prop) = prop else {
|
jvalue_to_prop(prop, property_sr, name)
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
|
||||||
source_ranges: property_sr,
|
|
||||||
message: format!(
|
|
||||||
"{name} is not a valid property/index, you can only use a string or int (>= 0) here",
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
jvalue_to_prop(&prop.value, property_sr, name)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LiteralIdentifier::Literal(literal) => {
|
LiteralIdentifier::Literal(literal) => {
|
||||||
@ -759,35 +791,37 @@ impl Property {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn jvalue_to_prop(value: &JValue, property_sr: Vec<SourceRange>, name: &str) -> Result<Property, KclError> {
|
fn jvalue_to_prop(value: &KclValue, property_sr: Vec<SourceRange>, name: &str) -> Result<Property, KclError> {
|
||||||
let make_err = |message: String| {
|
let make_err = |message: String| {
|
||||||
Err::<Property, _>(KclError::Semantic(KclErrorDetails {
|
Err::<Property, _>(KclError::Semantic(KclErrorDetails {
|
||||||
source_ranges: property_sr,
|
source_ranges: property_sr,
|
||||||
message,
|
message,
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
const MUST_BE_POSINT: &str = "indices must be whole positive numbers";
|
|
||||||
const TRY_INT: &str = "try using the int() function to make this a whole number";
|
|
||||||
match value {
|
match value {
|
||||||
JValue::Number(ref num) => {
|
KclValue::Int { value:num, meta: _ } => {
|
||||||
let maybe_uint = num.as_u64().and_then(|x| usize::try_from(x).ok());
|
let maybe_int: Result<usize, _> = (*num).try_into();
|
||||||
if let Some(uint) = maybe_uint {
|
if let Ok(uint) = maybe_int {
|
||||||
Ok(Property::Number(uint))
|
Ok(Property::Number(uint))
|
||||||
} else if let Some(iint) = num.as_i64() {
|
}
|
||||||
make_err(format!("'{iint}' is not a valid index, {MUST_BE_POSINT}"))
|
else {
|
||||||
} else if let Some(fnum) = num.as_f64() {
|
make_err(format!("'{num}' is negative, so you can't index an array with it"))
|
||||||
if fnum < 0.0 {
|
|
||||||
make_err(format!("'{fnum}' is not a valid index, {MUST_BE_POSINT}"))
|
|
||||||
} else if fnum.fract() == 0.0 {
|
|
||||||
make_err(format!("'{fnum:.1}' is stored as a fractional number but indices must be whole numbers, {TRY_INT}"))
|
|
||||||
} else {
|
|
||||||
make_err(format!("'{fnum}' is not a valid index, {MUST_BE_POSINT}, {TRY_INT}"))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
make_err(format!("'{num}' is not a valid index, {MUST_BE_POSINT}"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
JValue::String(ref x) => Ok(Property::String(x.to_owned())),
|
KclValue::Number{value: num, meta:_} => {
|
||||||
|
let num = *num;
|
||||||
|
if num < 0.0 {
|
||||||
|
return make_err(format!("'{num}' is negative, so you can't index an array with it"))
|
||||||
|
}
|
||||||
|
let nearest_int = num.round();
|
||||||
|
let delta = num-nearest_int;
|
||||||
|
if delta < FLOAT_TO_INT_MAX_DELTA {
|
||||||
|
Ok(Property::Number(nearest_int as usize))
|
||||||
|
} else {
|
||||||
|
make_err(format!("'{num}' is not an integer, so you can't index an array with it"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KclValue::String{value: x, meta:_} => Ok(Property::String(x.to_owned())),
|
||||||
_ => {
|
_ => {
|
||||||
make_err(format!("{name} is not a valid property/index, you can only use a string to get the property of an object, or an int (>= 0) to get an item in an array"))
|
make_err(format!("{name} is not a valid property/index, you can only use a string to get the property of an object, or an int (>= 0) to get an item in an array"))
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,7 @@ use databake::*;
|
|||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{ast::types::ConstraintLevel, executor::KclValue};
|
||||||
ast::types::ConstraintLevel,
|
|
||||||
executor::{KclValue, UserVal},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::Node;
|
use super::Node;
|
||||||
|
|
||||||
@ -16,7 +13,7 @@ const KCL_NONE_ID: &str = "KCL_NONE_ID";
|
|||||||
/// KCL value for an optional parameter which was not given an argument.
|
/// KCL value for an optional parameter which was not given an argument.
|
||||||
/// (remember, parameters are in the function declaration,
|
/// (remember, parameters are in the function declaration,
|
||||||
/// arguments are in the function call/application).
|
/// arguments are in the function call/application).
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake, Default)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake, Default, Copy)]
|
||||||
#[databake(path = kcl_lib::ast::types)]
|
#[databake(path = kcl_lib::ast::types)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
@ -58,19 +55,12 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&KclNone> for UserVal {
|
|
||||||
fn from(none: &KclNone) -> Self {
|
|
||||||
UserVal {
|
|
||||||
value: serde_json::to_value(none).expect("can always serialize a None"),
|
|
||||||
meta: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&KclNone> for KclValue {
|
impl From<&KclNone> for KclValue {
|
||||||
fn from(none: &KclNone) -> Self {
|
fn from(none: &KclNone) -> Self {
|
||||||
let val = UserVal::from(none);
|
KclValue::KclNone {
|
||||||
KclValue::UserVal(val)
|
value: *none,
|
||||||
|
meta: Default::default(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -790,6 +790,7 @@ fn test_generate_stdlib_json_schema() {
|
|||||||
// If this test fails and you've modified the AST or something else which affects the json repr
|
// If this test fails and you've modified the AST or something else which affects the json repr
|
||||||
// of stdlib functions, you should rerun the test with `EXPECTORATE=overwrite` to create new
|
// of stdlib functions, you should rerun the test with `EXPECTORATE=overwrite` to create new
|
||||||
// test data, then check `/docs/kcl/std.json` to ensure the changes are expected.
|
// test data, then check `/docs/kcl/std.json` to ensure the changes are expected.
|
||||||
|
// Alternatively, run `just redo-kcl-stdlib-docs` (make sure to have just installed).
|
||||||
let stdlib = StdLib::new();
|
let stdlib = StdLib::new();
|
||||||
let combined = stdlib.combined();
|
let combined = stdlib.combined();
|
||||||
|
|
||||||
|
@ -19,7 +19,6 @@ use kittycad_modeling_cmds::length_unit::LengthUnit;
|
|||||||
use parse_display::{Display, FromStr};
|
use parse_display::{Display, FromStr};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value as JValue;
|
|
||||||
use tower_lsp::lsp_types::{Position as LspPosition, Range as LspRange};
|
use tower_lsp::lsp_types::{Position as LspPosition, Range as LspRange};
|
||||||
|
|
||||||
type Point2D = kcmc::shared::Point2d<f64>;
|
type Point2D = kcmc::shared::Point2d<f64>;
|
||||||
@ -27,8 +26,8 @@ type Point3D = kcmc::shared::Point3d<f64>;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ast::types::{
|
ast::types::{
|
||||||
human_friendly_type, BodyItem, Expr, FunctionExpression, ItemVisibility, KclNone, ModuleId, Node, NodeRef,
|
BodyItem, Expr, FunctionExpression, ItemVisibility, KclNone, ModuleId, Node, NodeRef, Program, TagDeclarator,
|
||||||
Program, TagDeclarator, TagNode,
|
TagNode,
|
||||||
},
|
},
|
||||||
engine::{EngineManager, ExecutionKind},
|
engine::{EngineManager, ExecutionKind},
|
||||||
errors::{KclError, KclErrorDetails},
|
errors::{KclError, KclErrorDetails},
|
||||||
@ -201,33 +200,18 @@ impl Environment {
|
|||||||
Self {
|
Self {
|
||||||
// Prelude
|
// Prelude
|
||||||
bindings: HashMap::from([
|
bindings: HashMap::from([
|
||||||
(
|
("ZERO".to_string(), KclValue::from_number(0.0, Default::default())),
|
||||||
"ZERO".to_string(),
|
|
||||||
KclValue::UserVal(UserVal {
|
|
||||||
value: serde_json::Value::Number(serde_json::value::Number::from(0)),
|
|
||||||
meta: Default::default(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
"QUARTER_TURN".to_string(),
|
"QUARTER_TURN".to_string(),
|
||||||
KclValue::UserVal(UserVal {
|
KclValue::from_number(90.0, Default::default()),
|
||||||
value: serde_json::Value::Number(serde_json::value::Number::from(90)),
|
|
||||||
meta: Default::default(),
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"HALF_TURN".to_string(),
|
"HALF_TURN".to_string(),
|
||||||
KclValue::UserVal(UserVal {
|
KclValue::from_number(180.0, Default::default()),
|
||||||
value: serde_json::Value::Number(serde_json::value::Number::from(180)),
|
|
||||||
meta: Default::default(),
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"THREE_QUARTER_TURN".to_string(),
|
"THREE_QUARTER_TURN".to_string(),
|
||||||
KclValue::UserVal(UserVal {
|
KclValue::from_number(270.0, Default::default()),
|
||||||
value: serde_json::Value::Number(serde_json::value::Number::from(270)),
|
|
||||||
meta: Default::default(),
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
parent: None,
|
parent: None,
|
||||||
@ -264,22 +248,15 @@ impl Environment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (_, val) in self.bindings.iter_mut() {
|
for (_, val) in self.bindings.iter_mut() {
|
||||||
let KclValue::UserVal(v) = val else { continue };
|
let KclValue::Sketch { value } = val else { continue };
|
||||||
let meta = v.meta.clone();
|
let mut sketch = value.to_owned();
|
||||||
let maybe_sg: Result<Sketch, _> = serde_json::from_value(v.value.clone());
|
|
||||||
let Ok(mut sketch) = maybe_sg else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
if sketch.original_id == sg.original_id {
|
if sketch.original_id == sg.original_id {
|
||||||
for tag in sg.tags.iter() {
|
for tag in sg.tags.iter() {
|
||||||
sketch.tags.insert(tag.0.clone(), tag.1.clone());
|
sketch.tags.insert(tag.0.clone(), tag.1.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*val = KclValue::UserVal(UserVal {
|
*val = KclValue::Sketch { value: sketch };
|
||||||
meta,
|
|
||||||
value: serde_json::to_value(sketch).expect("can always turn Sketch into JSON"),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -360,12 +337,52 @@ impl IdGenerator {
|
|||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
pub enum KclValue {
|
pub enum KclValue {
|
||||||
UserVal(UserVal),
|
Uuid {
|
||||||
|
value: ::uuid::Uuid,
|
||||||
|
#[serde(rename = "__meta")]
|
||||||
|
meta: Vec<Metadata>,
|
||||||
|
},
|
||||||
|
Bool {
|
||||||
|
value: bool,
|
||||||
|
#[serde(rename = "__meta")]
|
||||||
|
meta: Vec<Metadata>,
|
||||||
|
},
|
||||||
|
Number {
|
||||||
|
value: f64,
|
||||||
|
#[serde(rename = "__meta")]
|
||||||
|
meta: Vec<Metadata>,
|
||||||
|
},
|
||||||
|
Int {
|
||||||
|
value: i64,
|
||||||
|
#[serde(rename = "__meta")]
|
||||||
|
meta: Vec<Metadata>,
|
||||||
|
},
|
||||||
|
String {
|
||||||
|
value: String,
|
||||||
|
#[serde(rename = "__meta")]
|
||||||
|
meta: Vec<Metadata>,
|
||||||
|
},
|
||||||
|
Array {
|
||||||
|
value: Vec<KclValue>,
|
||||||
|
#[serde(rename = "__meta")]
|
||||||
|
meta: Vec<Metadata>,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
value: HashMap<String, KclValue>,
|
||||||
|
#[serde(rename = "__meta")]
|
||||||
|
meta: Vec<Metadata>,
|
||||||
|
},
|
||||||
TagIdentifier(Box<TagIdentifier>),
|
TagIdentifier(Box<TagIdentifier>),
|
||||||
TagDeclarator(crate::ast::types::BoxNode<TagDeclarator>),
|
TagDeclarator(crate::ast::types::BoxNode<TagDeclarator>),
|
||||||
Plane(Box<Plane>),
|
Plane(Box<Plane>),
|
||||||
Face(Box<Face>),
|
Face(Box<Face>),
|
||||||
|
|
||||||
|
Sketch {
|
||||||
|
value: Box<Sketch>,
|
||||||
|
},
|
||||||
|
Sketches {
|
||||||
|
value: Vec<Box<Sketch>>,
|
||||||
|
},
|
||||||
Solid(Box<Solid>),
|
Solid(Box<Solid>),
|
||||||
Solids {
|
Solids {
|
||||||
value: Vec<Box<Solid>>,
|
value: Vec<Box<Solid>>,
|
||||||
@ -380,31 +397,55 @@ pub enum KclValue {
|
|||||||
#[serde(rename = "__meta")]
|
#[serde(rename = "__meta")]
|
||||||
meta: Vec<Metadata>,
|
meta: Vec<Metadata>,
|
||||||
},
|
},
|
||||||
|
KclNone {
|
||||||
|
value: KclNone,
|
||||||
|
#[serde(rename = "__meta")]
|
||||||
|
meta: Vec<Metadata>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KclValue {
|
impl KclValue {
|
||||||
pub(crate) fn new_user_val<T: Serialize>(meta: Vec<Metadata>, val: T) -> Self {
|
pub(crate) fn metadata(&self) -> Vec<Metadata> {
|
||||||
Self::UserVal(UserVal::new(meta, val))
|
match self {
|
||||||
|
KclValue::Uuid { value: _, meta } => meta.clone(),
|
||||||
|
KclValue::Bool { value: _, meta } => meta.clone(),
|
||||||
|
KclValue::Number { value: _, meta } => meta.clone(),
|
||||||
|
KclValue::Int { value: _, meta } => meta.clone(),
|
||||||
|
KclValue::String { value: _, meta } => meta.clone(),
|
||||||
|
KclValue::Array { value: _, meta } => meta.clone(),
|
||||||
|
KclValue::Object { value: _, meta } => meta.clone(),
|
||||||
|
KclValue::TagIdentifier(x) => x.meta.clone(),
|
||||||
|
KclValue::TagDeclarator(x) => vec![x.metadata()],
|
||||||
|
KclValue::Plane(x) => x.meta.clone(),
|
||||||
|
KclValue::Face(x) => x.meta.clone(),
|
||||||
|
KclValue::Sketch { value } => value.meta.clone(),
|
||||||
|
KclValue::Sketches { value } => value.iter().flat_map(|sketch| &sketch.meta).copied().collect(),
|
||||||
|
KclValue::Solid(x) => x.meta.clone(),
|
||||||
|
KclValue::Solids { value } => value.iter().flat_map(|sketch| &sketch.meta).copied().collect(),
|
||||||
|
KclValue::ImportedGeometry(x) => x.meta.clone(),
|
||||||
|
KclValue::Function { meta, .. } => meta.clone(),
|
||||||
|
KclValue::KclNone { meta, .. } => meta.clone(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_solid_set(&self) -> Result<SolidSet> {
|
pub(crate) fn get_solid_set(&self) -> Result<SolidSet> {
|
||||||
match self {
|
match self {
|
||||||
KclValue::Solid(e) => Ok(SolidSet::Solid(e.clone())),
|
KclValue::Solid(e) => Ok(SolidSet::Solid(e.clone())),
|
||||||
KclValue::Solids { value } => Ok(SolidSet::Solids(value.clone())),
|
KclValue::Solids { value } => Ok(SolidSet::Solids(value.clone())),
|
||||||
KclValue::UserVal(value) => {
|
KclValue::Array { value, .. } => {
|
||||||
let value = value.value.clone();
|
let solids: Vec<_> = value
|
||||||
match value {
|
.iter()
|
||||||
JValue::Null | JValue::Bool(_) | JValue::Number(_) | JValue::String(_) => Err(anyhow::anyhow!(
|
.enumerate()
|
||||||
"Failed to deserialize solid set from JSON {}",
|
.map(|(i, v)| {
|
||||||
human_friendly_type(&value)
|
v.as_solid().map(|v| v.to_owned()).map(Box::new).ok_or_else(|| {
|
||||||
)),
|
anyhow::anyhow!(
|
||||||
JValue::Array(_) => serde_json::from_value::<Vec<Box<Solid>>>(value)
|
"expected this array to only contain solids, but element {i} was actually {}",
|
||||||
.map(SolidSet::from)
|
v.human_friendly_type()
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to deserialize array of solids from JSON: {}", e)),
|
)
|
||||||
JValue::Object(_) => serde_json::from_value::<Box<Solid>>(value)
|
})
|
||||||
.map(SolidSet::from)
|
})
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to deserialize solid from JSON: {}", e)),
|
.collect::<Result<_, _>>()?;
|
||||||
}
|
Ok(SolidSet::Solids(solids))
|
||||||
}
|
}
|
||||||
_ => anyhow::bail!("Not a solid or solids: {:?}", self),
|
_ => anyhow::bail!("Not a solid or solids: {:?}", self),
|
||||||
}
|
}
|
||||||
@ -414,43 +455,44 @@ impl KclValue {
|
|||||||
/// on for program logic.
|
/// on for program logic.
|
||||||
pub(crate) fn human_friendly_type(&self) -> &'static str {
|
pub(crate) fn human_friendly_type(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
KclValue::UserVal(u) => human_friendly_type(&u.value),
|
KclValue::Uuid { .. } => "Unique ID (uuid)",
|
||||||
KclValue::TagDeclarator(_) => "TagDeclarator",
|
KclValue::TagDeclarator(_) => "TagDeclarator",
|
||||||
KclValue::TagIdentifier(_) => "TagIdentifier",
|
KclValue::TagIdentifier(_) => "TagIdentifier",
|
||||||
KclValue::Solid(_) => "Solid",
|
KclValue::Solid(_) => "Solid",
|
||||||
KclValue::Solids { .. } => "Solids",
|
KclValue::Solids { .. } => "Solids",
|
||||||
|
KclValue::Sketch { .. } => "Sketch",
|
||||||
|
KclValue::Sketches { .. } => "Sketches",
|
||||||
KclValue::ImportedGeometry(_) => "ImportedGeometry",
|
KclValue::ImportedGeometry(_) => "ImportedGeometry",
|
||||||
KclValue::Function { .. } => "Function",
|
KclValue::Function { .. } => "Function",
|
||||||
KclValue::Plane(_) => "Plane",
|
KclValue::Plane(_) => "Plane",
|
||||||
KclValue::Face(_) => "Face",
|
KclValue::Face(_) => "Face",
|
||||||
|
KclValue::Bool { .. } => "boolean (true/false value)",
|
||||||
|
KclValue::Number { .. } => "number",
|
||||||
|
KclValue::Int { .. } => "integer",
|
||||||
|
KclValue::String { .. } => "string (text)",
|
||||||
|
KclValue::Array { .. } => "array (list)",
|
||||||
|
KclValue::Object { .. } => "object",
|
||||||
|
KclValue::KclNone { .. } => "None",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_function(&self) -> bool {
|
pub(crate) fn is_function(&self) -> bool {
|
||||||
match self {
|
matches!(self, KclValue::Function { .. })
|
||||||
KclValue::UserVal(..)
|
|
||||||
| KclValue::TagIdentifier(..)
|
|
||||||
| KclValue::TagDeclarator(..)
|
|
||||||
| KclValue::Plane(..)
|
|
||||||
| KclValue::Face(..)
|
|
||||||
| KclValue::Solid(..)
|
|
||||||
| KclValue::Solids { .. }
|
|
||||||
| KclValue::ImportedGeometry(..) => false,
|
|
||||||
KclValue::Function { .. } => true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<SketchSet> for KclValue {
|
impl From<SketchSet> for KclValue {
|
||||||
fn from(sg: SketchSet) -> Self {
|
fn from(sg: SketchSet) -> Self {
|
||||||
KclValue::UserVal(UserVal::new(sg.meta(), sg))
|
match sg {
|
||||||
|
SketchSet::Sketch(value) => KclValue::Sketch { value },
|
||||||
|
SketchSet::Sketches(value) => KclValue::Sketches { value },
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Vec<Box<Sketch>>> for KclValue {
|
impl From<Vec<Box<Sketch>>> for KclValue {
|
||||||
fn from(sg: Vec<Box<Sketch>>) -> Self {
|
fn from(sg: Vec<Box<Sketch>>) -> Self {
|
||||||
let meta = sg.iter().flat_map(|sg| sg.meta.clone()).collect();
|
KclValue::Sketches { value: sg }
|
||||||
KclValue::UserVal(UserVal::new(meta, sg))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -815,52 +857,6 @@ pub enum PlaneType {
|
|||||||
Custom,
|
Custom,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
|
||||||
#[ts(export)]
|
|
||||||
#[serde(tag = "type", rename_all = "camelCase")]
|
|
||||||
pub struct UserVal {
|
|
||||||
#[ts(type = "any")]
|
|
||||||
pub value: serde_json::Value,
|
|
||||||
#[serde(rename = "__meta")]
|
|
||||||
pub meta: Vec<Metadata>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserVal {
|
|
||||||
pub fn new<T: serde::Serialize>(meta: Vec<Metadata>, val: T) -> Self {
|
|
||||||
Self {
|
|
||||||
meta,
|
|
||||||
value: serde_json::to_value(val).expect("all KCL values should be compatible with JSON"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If the UserVal matches the type `T`, return it.
|
|
||||||
pub fn get<T: serde::de::DeserializeOwned>(&self) -> Option<(T, Vec<Metadata>)> {
|
|
||||||
let meta = self.meta.clone();
|
|
||||||
// TODO: This clone might cause performance problems, it'll happen a lot.
|
|
||||||
let res: Result<T, _> = serde_json::from_value(self.value.clone());
|
|
||||||
if let Ok(t) = res {
|
|
||||||
Some((t, meta))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If the UserVal matches the type `T`, then mutate it via the given closure.
|
|
||||||
/// If the closure returns Err, the mutation won't be applied.
|
|
||||||
pub fn mutate<T, F, E>(&mut self, mutate: F) -> Result<(), E>
|
|
||||||
where
|
|
||||||
T: serde::de::DeserializeOwned + Serialize,
|
|
||||||
F: FnOnce(&mut T) -> Result<(), E>,
|
|
||||||
{
|
|
||||||
let Some((mut val, meta)) = self.get::<T>() else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
mutate(&mut val)?;
|
|
||||||
*self = Self::new(meta, val);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(tag = "type", rename_all = "camelCase")]
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
@ -922,108 +918,177 @@ pub type MemoryFunction =
|
|||||||
impl From<KclValue> for Vec<SourceRange> {
|
impl From<KclValue> for Vec<SourceRange> {
|
||||||
fn from(item: KclValue) -> Self {
|
fn from(item: KclValue) -> Self {
|
||||||
match item {
|
match item {
|
||||||
KclValue::UserVal(u) => u.meta.iter().map(|m| m.source_range).collect(),
|
KclValue::TagDeclarator(t) => vec![SourceRange([t.start, t.end, t.module_id.0 as usize])],
|
||||||
KclValue::TagDeclarator(t) => vec![(&t).into()],
|
KclValue::TagIdentifier(t) => to_vec_sr(&t.meta),
|
||||||
KclValue::TagIdentifier(t) => t.meta.iter().map(|m| m.source_range).collect(),
|
KclValue::Solid(e) => to_vec_sr(&e.meta),
|
||||||
KclValue::Solid(e) => e.meta.iter().map(|m| m.source_range).collect(),
|
KclValue::Solids { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(),
|
||||||
KclValue::Solids { value } => value
|
KclValue::Sketch { value } => to_vec_sr(&value.meta),
|
||||||
.iter()
|
KclValue::Sketches { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(),
|
||||||
.flat_map(|eg| eg.meta.iter().map(|m| m.source_range))
|
KclValue::ImportedGeometry(i) => to_vec_sr(&i.meta),
|
||||||
.collect(),
|
KclValue::Function { meta, .. } => to_vec_sr(&meta),
|
||||||
KclValue::ImportedGeometry(i) => i.meta.iter().map(|m| m.source_range).collect(),
|
KclValue::Plane(p) => to_vec_sr(&p.meta),
|
||||||
KclValue::Function { meta, .. } => meta.iter().map(|m| m.source_range).collect(),
|
KclValue::Face(f) => to_vec_sr(&f.meta),
|
||||||
KclValue::Plane(p) => p.meta.iter().map(|m| m.source_range).collect(),
|
KclValue::Bool { meta, .. } => to_vec_sr(&meta),
|
||||||
KclValue::Face(f) => f.meta.iter().map(|m| m.source_range).collect(),
|
KclValue::Number { meta, .. } => to_vec_sr(&meta),
|
||||||
|
KclValue::Int { meta, .. } => to_vec_sr(&meta),
|
||||||
|
KclValue::String { meta, .. } => to_vec_sr(&meta),
|
||||||
|
KclValue::Array { meta, .. } => to_vec_sr(&meta),
|
||||||
|
KclValue::Object { meta, .. } => to_vec_sr(&meta),
|
||||||
|
KclValue::Uuid { meta, .. } => to_vec_sr(&meta),
|
||||||
|
KclValue::KclNone { meta, .. } => to_vec_sr(&meta),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn to_vec_sr(meta: &[Metadata]) -> Vec<SourceRange> {
|
||||||
|
meta.iter().map(|m| m.source_range).collect()
|
||||||
|
}
|
||||||
|
|
||||||
impl From<&KclValue> for Vec<SourceRange> {
|
impl From<&KclValue> for Vec<SourceRange> {
|
||||||
fn from(item: &KclValue) -> Self {
|
fn from(item: &KclValue) -> Self {
|
||||||
match item {
|
match item {
|
||||||
KclValue::UserVal(u) => u.meta.iter().map(|m| m.source_range).collect(),
|
KclValue::TagDeclarator(t) => vec![SourceRange([t.start, t.end, t.module_id.0 as usize])],
|
||||||
KclValue::TagDeclarator(ref t) => vec![t.into()],
|
KclValue::TagIdentifier(t) => to_vec_sr(&t.meta),
|
||||||
KclValue::TagIdentifier(t) => t.meta.iter().map(|m| m.source_range).collect(),
|
KclValue::Solid(e) => to_vec_sr(&e.meta),
|
||||||
KclValue::Solid(e) => e.meta.iter().map(|m| m.source_range).collect(),
|
KclValue::Solids { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(),
|
||||||
KclValue::Solids { value } => value
|
KclValue::Sketch { value } => to_vec_sr(&value.meta),
|
||||||
.iter()
|
KclValue::Sketches { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(),
|
||||||
.flat_map(|eg| eg.meta.iter().map(|m| m.source_range))
|
KclValue::ImportedGeometry(i) => to_vec_sr(&i.meta),
|
||||||
.collect(),
|
KclValue::Function { meta, .. } => to_vec_sr(meta),
|
||||||
KclValue::ImportedGeometry(i) => i.meta.iter().map(|m| m.source_range).collect(),
|
KclValue::Plane(p) => to_vec_sr(&p.meta),
|
||||||
KclValue::Function { meta, .. } => meta.iter().map(|m| m.source_range).collect(),
|
KclValue::Face(f) => to_vec_sr(&f.meta),
|
||||||
KclValue::Plane(p) => p.meta.iter().map(|m| m.source_range).collect(),
|
KclValue::Bool { meta, .. } => to_vec_sr(meta),
|
||||||
KclValue::Face(f) => f.meta.iter().map(|m| m.source_range).collect(),
|
KclValue::Number { meta, .. } => to_vec_sr(meta),
|
||||||
|
KclValue::Int { meta, .. } => to_vec_sr(meta),
|
||||||
|
KclValue::String { meta, .. } => to_vec_sr(meta),
|
||||||
|
KclValue::Uuid { meta, .. } => to_vec_sr(meta),
|
||||||
|
KclValue::Array { meta, .. } => to_vec_sr(meta),
|
||||||
|
KclValue::Object { meta, .. } => to_vec_sr(meta),
|
||||||
|
KclValue::KclNone { meta, .. } => to_vec_sr(meta),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KclValue {
|
impl KclValue {
|
||||||
pub fn get_json_value(&self) -> Result<serde_json::Value, KclError> {
|
/// Put the number into a KCL value.
|
||||||
if let KclValue::UserVal(user_val) = self {
|
pub fn from_number(f: f64, meta: Vec<Metadata>) -> Self {
|
||||||
Ok(user_val.value.clone())
|
Self::Number { value: f, meta }
|
||||||
} else {
|
}
|
||||||
serde_json::to_value(self).map_err(|err| {
|
|
||||||
KclError::Semantic(KclErrorDetails {
|
/// Put the point into a KCL value.
|
||||||
message: format!("Cannot convert memory item to json value: {:?}", err),
|
pub fn from_point2d(p: [f64; 2], meta: Vec<Metadata>) -> Self {
|
||||||
source_ranges: self.clone().into(),
|
Self::Array {
|
||||||
})
|
value: vec![
|
||||||
})
|
Self::Number {
|
||||||
|
value: p[0],
|
||||||
|
meta: meta.clone(),
|
||||||
|
},
|
||||||
|
Self::Number {
|
||||||
|
value: p[1],
|
||||||
|
meta: meta.clone(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
meta,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a JSON value and deserialize it into some concrete type.
|
pub(crate) fn as_usize(&self) -> Option<usize> {
|
||||||
pub fn get_json<T: serde::de::DeserializeOwned>(&self) -> Result<T, KclError> {
|
match self {
|
||||||
let json = self.get_json_value()?;
|
KclValue::Int { value, .. } => Some(*value as usize),
|
||||||
|
_ => None,
|
||||||
serde_json::from_value(json).map_err(|e| {
|
|
||||||
KclError::Type(KclErrorDetails {
|
|
||||||
message: format!("Failed to deserialize struct from JSON: {}", e),
|
|
||||||
source_ranges: self.clone().into(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a JSON value and deserialize it into some concrete type.
|
|
||||||
/// If it's a KCL None, return None. Otherwise return Some.
|
|
||||||
pub fn get_json_opt<T: serde::de::DeserializeOwned>(&self) -> Result<Option<T>, KclError> {
|
|
||||||
let json = self.get_json_value()?;
|
|
||||||
if let JValue::Object(ref o) = json {
|
|
||||||
if let Some(JValue::String(s)) = o.get("type") {
|
|
||||||
if s == "KclNone" {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
serde_json::from_value(json)
|
|
||||||
.map_err(|e| {
|
|
||||||
KclError::Type(KclErrorDetails {
|
|
||||||
message: format!("Failed to deserialize struct from JSON: {}", e),
|
|
||||||
source_ranges: self.clone().into(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.map(Some)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_user_val(&self) -> Option<&UserVal> {
|
pub fn as_int(&self) -> Option<i64> {
|
||||||
if let KclValue::UserVal(x) = self {
|
if let KclValue::Int { value, meta: _ } = &self {
|
||||||
Some(x)
|
Some(*value)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If this value is of type u32, return it.
|
pub fn as_object(&self) -> Option<&HashMap<String, KclValue>> {
|
||||||
|
if let KclValue::Object { value, meta: _ } = &self {
|
||||||
|
Some(value)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_str(&self) -> Option<&str> {
|
||||||
|
if let KclValue::String { value, meta: _ } = &self {
|
||||||
|
Some(value)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_array(&self) -> Option<&[KclValue]> {
|
||||||
|
if let KclValue::Array { value, meta: _ } = &self {
|
||||||
|
Some(value)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_point2d(&self) -> Option<[f64; 2]> {
|
||||||
|
let arr = self.as_array()?;
|
||||||
|
if arr.len() != 2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let x = arr[0].as_f64()?;
|
||||||
|
let y = arr[1].as_f64()?;
|
||||||
|
Some([x, y])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_uuid(&self) -> Option<uuid::Uuid> {
|
||||||
|
if let KclValue::Uuid { value, meta: _ } = &self {
|
||||||
|
Some(*value)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_solid(&self) -> Option<&Solid> {
|
||||||
|
if let KclValue::Solid(value) = &self {
|
||||||
|
Some(value)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_f64(&self) -> Option<f64> {
|
||||||
|
if let KclValue::Number { value, meta: _ } = &self {
|
||||||
|
Some(*value)
|
||||||
|
} else if let KclValue::Int { value, meta: _ } = &self {
|
||||||
|
Some(*value as f64)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_bool(&self) -> Option<bool> {
|
||||||
|
if let KclValue::Bool { value, meta: _ } = &self {
|
||||||
|
Some(*value)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If this value fits in a u32, return it.
|
||||||
pub fn get_u32(&self, source_ranges: Vec<SourceRange>) -> Result<u32, KclError> {
|
pub fn get_u32(&self, source_ranges: Vec<SourceRange>) -> Result<u32, KclError> {
|
||||||
let err = KclError::Semantic(KclErrorDetails {
|
let u = self.as_int().and_then(|n| u64::try_from(n).ok()).ok_or_else(|| {
|
||||||
message: "Expected an integer >= 0".to_owned(),
|
KclError::Semantic(KclErrorDetails {
|
||||||
source_ranges,
|
message: "Expected an integer >= 0".to_owned(),
|
||||||
});
|
source_ranges: source_ranges.clone(),
|
||||||
self.as_user_val()
|
})
|
||||||
.and_then(|uv| uv.value.as_number())
|
})?;
|
||||||
.and_then(|n| n.as_u64())
|
u32::try_from(u).map_err(|_| {
|
||||||
.and_then(|n| u32::try_from(n).ok())
|
KclError::Semantic(KclErrorDetails {
|
||||||
.ok_or(err)
|
message: "Number was too big".to_owned(),
|
||||||
|
source_ranges,
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If this value is of type function, return it.
|
/// If this value is of type function, return it.
|
||||||
@ -1048,16 +1113,6 @@ impl KclValue {
|
|||||||
pub fn get_tag_identifier(&self) -> Result<TagIdentifier, KclError> {
|
pub fn get_tag_identifier(&self) -> Result<TagIdentifier, KclError> {
|
||||||
match self {
|
match self {
|
||||||
KclValue::TagIdentifier(t) => Ok(*t.clone()),
|
KclValue::TagIdentifier(t) => Ok(*t.clone()),
|
||||||
KclValue::UserVal(_) => {
|
|
||||||
if let Some(identifier) = self.get_json_opt::<TagIdentifier>()? {
|
|
||||||
Ok(identifier)
|
|
||||||
} else {
|
|
||||||
Err(KclError::Semantic(KclErrorDetails {
|
|
||||||
message: format!("Not a tag identifier: {:?}", self),
|
|
||||||
source_ranges: self.clone().into(),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Err(KclError::Semantic(KclErrorDetails {
|
_ => Err(KclError::Semantic(KclErrorDetails {
|
||||||
message: format!("Not a tag identifier: {:?}", self),
|
message: format!("Not a tag identifier: {:?}", self),
|
||||||
source_ranges: self.clone().into(),
|
source_ranges: self.clone().into(),
|
||||||
@ -1089,19 +1144,13 @@ impl KclValue {
|
|||||||
|
|
||||||
/// If this KCL value is a bool, retrieve it.
|
/// If this KCL value is a bool, retrieve it.
|
||||||
pub fn get_bool(&self) -> Result<bool, KclError> {
|
pub fn get_bool(&self) -> Result<bool, KclError> {
|
||||||
let Self::UserVal(uv) = self else {
|
let Self::Bool { value: b, .. } = self else {
|
||||||
return Err(KclError::Type(KclErrorDetails {
|
return Err(KclError::Type(KclErrorDetails {
|
||||||
source_ranges: self.into(),
|
source_ranges: self.into(),
|
||||||
message: format!("Expected bool, found {}", self.human_friendly_type()),
|
message: format!("Expected bool, found {}", self.human_friendly_type()),
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
let JValue::Bool(b) = uv.value else {
|
Ok(*b)
|
||||||
return Err(KclError::Type(KclErrorDetails {
|
|
||||||
source_ranges: self.into(),
|
|
||||||
message: format!("Expected bool, found {}", human_friendly_type(&uv.value)),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
Ok(b)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If this memory item is a function, call it with the given arguments, return its val as Ok.
|
/// If this memory item is a function, call it with the given arguments, return its val as Ok.
|
||||||
@ -1555,7 +1604,7 @@ impl From<Point3d> for kittycad_modeling_cmds::shared::Point3d<LengthUnit> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Metadata.
|
/// Metadata.
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq, Copy)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Metadata {
|
pub struct Metadata {
|
||||||
@ -1563,6 +1612,12 @@ pub struct Metadata {
|
|||||||
pub source_range: SourceRange,
|
pub source_range: SourceRange,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Metadata> for Vec<SourceRange> {
|
||||||
|
fn from(meta: Metadata) -> Self {
|
||||||
|
vec![meta.source_range]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<SourceRange> for Metadata {
|
impl From<SourceRange> for Metadata {
|
||||||
fn from(source_range: SourceRange) -> Self {
|
fn from(source_range: SourceRange) -> Self {
|
||||||
Self { source_range }
|
Self { source_range }
|
||||||
@ -2655,74 +2710,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience function to get a JSON value from memory and unwrap.
|
/// Convenience function to get a JSON value from memory and unwrap.
|
||||||
fn mem_get_json(memory: &ProgramMemory, name: &str) -> serde_json::Value {
|
fn mem_get_json(memory: &ProgramMemory, name: &str) -> KclValue {
|
||||||
memory
|
memory.get(name, SourceRange::default()).unwrap().to_owned()
|
||||||
.get(name, SourceRange::default())
|
|
||||||
.unwrap()
|
|
||||||
.get_json_value()
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
|
||||||
async fn test_execute_assign_two_variables() {
|
|
||||||
let ast = r#"const myVar = 5
|
|
||||||
const newVar = myVar + 1"#;
|
|
||||||
let memory = parse_execute(ast).await.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
serde_json::json!(5),
|
|
||||||
memory
|
|
||||||
.get("myVar", SourceRange::default())
|
|
||||||
.unwrap()
|
|
||||||
.get_json_value()
|
|
||||||
.unwrap()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
serde_json::json!(6.0),
|
|
||||||
memory
|
|
||||||
.get("newVar", SourceRange::default())
|
|
||||||
.unwrap()
|
|
||||||
.get_json_value()
|
|
||||||
.unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
|
||||||
async fn test_execute_angled_line_that_intersects() {
|
|
||||||
let ast_fn = |offset: &str| -> String {
|
|
||||||
format!(
|
|
||||||
r#"const part001 = startSketchOn('XY')
|
|
||||||
|> startProfileAt([0, 0], %)
|
|
||||||
|> lineTo([2, 2], %, $yo)
|
|
||||||
|> lineTo([3, 1], %)
|
|
||||||
|> angledLineThatIntersects({{
|
|
||||||
angle: 180,
|
|
||||||
intersectTag: yo,
|
|
||||||
offset: {},
|
|
||||||
}}, %, $yo2)
|
|
||||||
const intersect = segEndX(yo2)"#,
|
|
||||||
offset
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
let memory = parse_execute(&ast_fn("-1")).await.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
serde_json::json!(1.0 + 2.0f64.sqrt()),
|
|
||||||
memory
|
|
||||||
.get("intersect", SourceRange::default())
|
|
||||||
.unwrap()
|
|
||||||
.get_json_value()
|
|
||||||
.unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
let memory = parse_execute(&ast_fn("0")).await.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
serde_json::json!(1.0000000000000002),
|
|
||||||
memory
|
|
||||||
.get("intersect", SourceRange::default())
|
|
||||||
.unwrap()
|
|
||||||
.get_json_value()
|
|
||||||
.unwrap()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
@ -3120,200 +3109,41 @@ let shape = layer() |> patternTransform(10, transform, %)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
// ADAM: Move some of these into simulation tests.
|
||||||
async fn test_execute_function_with_parameter_redefined_outside() {
|
|
||||||
let ast = r#"
|
|
||||||
fn myIdentity = (x) => {
|
|
||||||
return x
|
|
||||||
}
|
|
||||||
|
|
||||||
const x = 33
|
|
||||||
|
|
||||||
const two = myIdentity(2)"#;
|
|
||||||
|
|
||||||
let memory = parse_execute(ast).await.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
serde_json::json!(2),
|
|
||||||
memory
|
|
||||||
.get("two", SourceRange::default())
|
|
||||||
.unwrap()
|
|
||||||
.get_json_value()
|
|
||||||
.unwrap()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
serde_json::json!(33),
|
|
||||||
memory
|
|
||||||
.get("x", SourceRange::default())
|
|
||||||
.unwrap()
|
|
||||||
.get_json_value()
|
|
||||||
.unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
|
||||||
async fn test_execute_function_referencing_variable_in_parent_scope() {
|
|
||||||
let ast = r#"
|
|
||||||
const x = 22
|
|
||||||
const y = 3
|
|
||||||
|
|
||||||
fn add = (x) => {
|
|
||||||
return x + y
|
|
||||||
}
|
|
||||||
|
|
||||||
const answer = add(2)"#;
|
|
||||||
|
|
||||||
let memory = parse_execute(ast).await.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
serde_json::json!(5.0),
|
|
||||||
memory
|
|
||||||
.get("answer", SourceRange::default())
|
|
||||||
.unwrap()
|
|
||||||
.get_json_value()
|
|
||||||
.unwrap()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
serde_json::json!(22),
|
|
||||||
memory
|
|
||||||
.get("x", SourceRange::default())
|
|
||||||
.unwrap()
|
|
||||||
.get_json_value()
|
|
||||||
.unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
|
||||||
async fn test_execute_function_redefining_variable_in_parent_scope() {
|
|
||||||
let ast = r#"
|
|
||||||
const x = 1
|
|
||||||
|
|
||||||
fn foo = () => {
|
|
||||||
const x = 2
|
|
||||||
return x
|
|
||||||
}
|
|
||||||
|
|
||||||
const answer = foo()"#;
|
|
||||||
|
|
||||||
let memory = parse_execute(ast).await.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
serde_json::json!(2),
|
|
||||||
memory
|
|
||||||
.get("answer", SourceRange::default())
|
|
||||||
.unwrap()
|
|
||||||
.get_json_value()
|
|
||||||
.unwrap()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
serde_json::json!(1),
|
|
||||||
memory
|
|
||||||
.get("x", SourceRange::default())
|
|
||||||
.unwrap()
|
|
||||||
.get_json_value()
|
|
||||||
.unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
|
||||||
async fn test_execute_pattern_transform_function_redefining_variable_in_parent_scope() {
|
|
||||||
let ast = r#"
|
|
||||||
const scale = 100
|
|
||||||
fn transform = (replicaId) => {
|
|
||||||
// Redefine same variable as in parent scope.
|
|
||||||
const scale = 2
|
|
||||||
return {
|
|
||||||
translate: [0, 0, replicaId * 10],
|
|
||||||
scale: [scale, 1, 0],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layer = () => {
|
|
||||||
return startSketchOn("XY")
|
|
||||||
|> circle({ center: [0, 0], radius: 1 }, %, $tag1)
|
|
||||||
|> extrude(10, %)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The 10 layers are replicas of each other, with a transform applied to each.
|
|
||||||
let shape = layer() |> patternTransform(10, transform, %)"#;
|
|
||||||
|
|
||||||
let memory = parse_execute(ast).await.unwrap();
|
|
||||||
// TODO: Assert that scale 2 was used.
|
|
||||||
assert_eq!(
|
|
||||||
serde_json::json!(100),
|
|
||||||
memory
|
|
||||||
.get("scale", SourceRange::default())
|
|
||||||
.unwrap()
|
|
||||||
.get_json_value()
|
|
||||||
.unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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 memory = parse_execute(ast).await.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(5.0, mem_get_json(&memory, "myVar").as_f64().unwrap());
|
||||||
serde_json::json!(5.0),
|
|
||||||
memory
|
|
||||||
.get("myVar", SourceRange::default())
|
|
||||||
.unwrap()
|
|
||||||
.get_json_value()
|
|
||||||
.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 memory = parse_execute(ast).await.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(7.4, mem_get_json(&memory, "myVar").as_f64().unwrap());
|
||||||
serde_json::json!(7.4),
|
|
||||||
memory
|
|
||||||
.get("myVar", SourceRange::default())
|
|
||||||
.unwrap()
|
|
||||||
.get_json_value()
|
|
||||||
.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 memory = parse_execute(ast).await.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(1.0, mem_get_json(&memory, "myVar").as_f64().unwrap());
|
||||||
serde_json::json!(1.0),
|
|
||||||
memory
|
|
||||||
.get("myVar", SourceRange::default())
|
|
||||||
.unwrap()
|
|
||||||
.get_json_value()
|
|
||||||
.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 memory = parse_execute(ast).await.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(std::f64::consts::TAU, mem_get_json(&memory, "myVar").as_f64().unwrap());
|
||||||
serde_json::json!(std::f64::consts::TAU),
|
|
||||||
memory
|
|
||||||
.get("myVar", SourceRange::default())
|
|
||||||
.unwrap()
|
|
||||||
.get_json_value()
|
|
||||||
.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 memory = parse_execute(ast).await.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(7.4, mem_get_json(&memory, "thing").as_f64().unwrap());
|
||||||
serde_json::json!(7.4),
|
|
||||||
memory
|
|
||||||
.get("thing", SourceRange::default())
|
|
||||||
.unwrap()
|
|
||||||
.get_json_value()
|
|
||||||
.unwrap()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
@ -3353,10 +3183,10 @@ fn check = (x) => {
|
|||||||
check(false)
|
check(false)
|
||||||
"#;
|
"#;
|
||||||
let mem = parse_execute(ast).await.unwrap();
|
let mem = parse_execute(ast).await.unwrap();
|
||||||
assert_eq!(serde_json::json!(false), mem_get_json(&mem, "notTrue"));
|
assert_eq!(false, mem_get_json(&mem, "notTrue").as_bool().unwrap());
|
||||||
assert_eq!(serde_json::json!(true), mem_get_json(&mem, "notFalse"));
|
assert_eq!(true, mem_get_json(&mem, "notFalse").as_bool().unwrap());
|
||||||
assert_eq!(serde_json::json!(true), mem_get_json(&mem, "c"));
|
assert_eq!(true, mem_get_json(&mem, "c").as_bool().unwrap());
|
||||||
assert_eq!(serde_json::json!(false), mem_get_json(&mem, "d"));
|
assert_eq!(false, mem_get_json(&mem, "d").as_bool().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
@ -3369,7 +3199,7 @@ let notNull = !myNull
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_execute(code1).await.unwrap_err().downcast::<KclError>().unwrap(),
|
parse_execute(code1).await.unwrap_err().downcast::<KclError>().unwrap(),
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails {
|
||||||
message: "Cannot apply unary operator ! to non-boolean value: null".to_owned(),
|
message: "Cannot apply unary operator ! to non-boolean value: number".to_owned(),
|
||||||
source_ranges: vec![SourceRange([56, 63, 0])],
|
source_ranges: vec![SourceRange([56, 63, 0])],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -3378,7 +3208,7 @@ let notNull = !myNull
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_execute(code2).await.unwrap_err().downcast::<KclError>().unwrap(),
|
parse_execute(code2).await.unwrap_err().downcast::<KclError>().unwrap(),
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails {
|
||||||
message: "Cannot apply unary operator ! to non-boolean value: 0".to_owned(),
|
message: "Cannot apply unary operator ! to non-boolean value: integer".to_owned(),
|
||||||
source_ranges: vec![SourceRange([14, 16, 0])],
|
source_ranges: vec![SourceRange([14, 16, 0])],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -3389,7 +3219,7 @@ let notEmptyString = !""
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_execute(code3).await.unwrap_err().downcast::<KclError>().unwrap(),
|
parse_execute(code3).await.unwrap_err().downcast::<KclError>().unwrap(),
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails {
|
||||||
message: "Cannot apply unary operator ! to non-boolean value: \"\"".to_owned(),
|
message: "Cannot apply unary operator ! to non-boolean value: string (text)".to_owned(),
|
||||||
source_ranges: vec![SourceRange([22, 25, 0])],
|
source_ranges: vec![SourceRange([22, 25, 0])],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -3401,7 +3231,7 @@ let notMember = !obj.a
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_execute(code4).await.unwrap_err().downcast::<KclError>().unwrap(),
|
parse_execute(code4).await.unwrap_err().downcast::<KclError>().unwrap(),
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails {
|
||||||
message: "Cannot apply unary operator ! to non-boolean value: 1".to_owned(),
|
message: "Cannot apply unary operator ! to non-boolean value: integer".to_owned(),
|
||||||
source_ranges: vec![SourceRange([36, 42, 0])],
|
source_ranges: vec![SourceRange([36, 42, 0])],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -3412,7 +3242,7 @@ let notArray = !a";
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_execute(code5).await.unwrap_err().downcast::<KclError>().unwrap(),
|
parse_execute(code5).await.unwrap_err().downcast::<KclError>().unwrap(),
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails {
|
||||||
message: "Cannot apply unary operator ! to non-boolean value: []".to_owned(),
|
message: "Cannot apply unary operator ! to non-boolean value: array (list)".to_owned(),
|
||||||
source_ranges: vec![SourceRange([27, 29, 0])],
|
source_ranges: vec![SourceRange([27, 29, 0])],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -3423,7 +3253,7 @@ let notObject = !x";
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_execute(code6).await.unwrap_err().downcast::<KclError>().unwrap(),
|
parse_execute(code6).await.unwrap_err().downcast::<KclError>().unwrap(),
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails {
|
||||||
message: "Cannot apply unary operator ! to non-boolean value: {}".to_owned(),
|
message: "Cannot apply unary operator ! to non-boolean value: object".to_owned(),
|
||||||
source_ranges: vec![SourceRange([28, 30, 0])],
|
source_ranges: vec![SourceRange([28, 30, 0])],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -3451,7 +3281,7 @@ let notTagDeclarator = !myTagDeclarator";
|
|||||||
assert!(
|
assert!(
|
||||||
tag_declarator_err
|
tag_declarator_err
|
||||||
.message()
|
.message()
|
||||||
.starts_with("Cannot apply unary operator ! to non-boolean value: {\"type\":\"TagDeclarator\","),
|
.starts_with("Cannot apply unary operator ! to non-boolean value: TagDeclarator"),
|
||||||
"Actual error: {:?}",
|
"Actual error: {:?}",
|
||||||
tag_declarator_err
|
tag_declarator_err
|
||||||
);
|
);
|
||||||
@ -3465,7 +3295,7 @@ let notTagIdentifier = !myTag";
|
|||||||
assert!(
|
assert!(
|
||||||
tag_identifier_err
|
tag_identifier_err
|
||||||
.message()
|
.message()
|
||||||
.starts_with("Cannot apply unary operator ! to non-boolean value: {\"type\":\"TagIdentifier\","),
|
.starts_with("Cannot apply unary operator ! to non-boolean value: TagIdentifier"),
|
||||||
"Actual error: {:?}",
|
"Actual error: {:?}",
|
||||||
tag_identifier_err
|
tag_identifier_err
|
||||||
);
|
);
|
||||||
@ -3603,10 +3433,10 @@ let w = f() + f()
|
|||||||
fn test_assign_args_to_params() {
|
fn test_assign_args_to_params() {
|
||||||
// Set up a little framework for this test.
|
// Set up a little framework for this test.
|
||||||
fn mem(number: usize) -> KclValue {
|
fn mem(number: usize) -> KclValue {
|
||||||
KclValue::UserVal(UserVal {
|
KclValue::Int {
|
||||||
value: number.into(),
|
value: number as i64,
|
||||||
meta: Default::default(),
|
meta: Default::default(),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
fn ident(s: &'static str) -> Node<Identifier> {
|
fn ident(s: &'static str) -> Node<Identifier> {
|
||||||
Node::no_src(Identifier {
|
Node::no_src(Identifier {
|
||||||
|
@ -5,7 +5,7 @@ pub mod project;
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use parse_display::{Display, FromStr};
|
use parse_display::{Display, FromStr};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserializer, Deserialize, Serialize};
|
||||||
use validator::{Validate, ValidateRange};
|
use validator::{Validate, ValidateRange};
|
||||||
|
|
||||||
const DEFAULT_THEME_COLOR: f64 = 264.5;
|
const DEFAULT_THEME_COLOR: f64 = 264.5;
|
||||||
@ -119,12 +119,34 @@ pub struct AppSettings {
|
|||||||
/// This setting only applies to the web app. And is temporary until we have Linux support.
|
/// This setting only applies to the web app. And is temporary until we have Linux support.
|
||||||
#[serde(default, alias = "dismissWebBanner", skip_serializing_if = "is_default")]
|
#[serde(default, alias = "dismissWebBanner", skip_serializing_if = "is_default")]
|
||||||
pub dismiss_web_banner: bool,
|
pub dismiss_web_banner: bool,
|
||||||
/// When the user is idle, and this is true, the stream will be torn down.
|
/// When the user is idle, teardown the stream after some time.
|
||||||
#[serde(default, alias = "streamIdleMode", skip_serializing_if = "is_default")]
|
#[serde(default, deserialize_with = "deserialize_stream_idle_mode", alias = "streamIdleMode", skip_serializing_if = "is_default")]
|
||||||
stream_idle_mode: bool,
|
stream_idle_mode: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_stream_idle_mode<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum StreamIdleModeValue {
|
||||||
|
String(String),
|
||||||
|
Boolean(bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_TIMEOUT: u32 = 1000 * 60 * 5;
|
||||||
|
|
||||||
|
Ok(match StreamIdleModeValue::deserialize(deserializer) {
|
||||||
|
Ok(StreamIdleModeValue::String(value)) => Some(value.parse::<u32>().unwrap_or(DEFAULT_TIMEOUT)),
|
||||||
|
// The old type of this value. I'm willing to say no one used it but
|
||||||
|
// we can never guarantee it.
|
||||||
|
Ok(StreamIdleModeValue::Boolean(true)) => Some(DEFAULT_TIMEOUT),
|
||||||
|
Ok(StreamIdleModeValue::Boolean(false)) => None,
|
||||||
|
_ => None
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: When we remove backwards compatibility with the old settings file, we can remove this.
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
@ -582,7 +604,7 @@ textWrapping = true
|
|||||||
theme_color: None,
|
theme_color: None,
|
||||||
dismiss_web_banner: false,
|
dismiss_web_banner: false,
|
||||||
enable_ssao: None,
|
enable_ssao: None,
|
||||||
stream_idle_mode: false,
|
stream_idle_mode: None,
|
||||||
},
|
},
|
||||||
modeling: ModelingSettings {
|
modeling: ModelingSettings {
|
||||||
base_unit: UnitLength::In,
|
base_unit: UnitLength::In,
|
||||||
@ -643,7 +665,7 @@ includeSettings = false
|
|||||||
theme_color: None,
|
theme_color: None,
|
||||||
dismiss_web_banner: false,
|
dismiss_web_banner: false,
|
||||||
enable_ssao: None,
|
enable_ssao: None,
|
||||||
stream_idle_mode: false,
|
stream_idle_mode: None,
|
||||||
},
|
},
|
||||||
modeling: ModelingSettings {
|
modeling: ModelingSettings {
|
||||||
base_unit: UnitLength::Yd,
|
base_unit: UnitLength::Yd,
|
||||||
@ -709,7 +731,7 @@ defaultProjectName = "projects-$nnn"
|
|||||||
theme_color: None,
|
theme_color: None,
|
||||||
dismiss_web_banner: false,
|
dismiss_web_banner: false,
|
||||||
enable_ssao: None,
|
enable_ssao: None,
|
||||||
stream_idle_mode: false,
|
stream_idle_mode: None,
|
||||||
},
|
},
|
||||||
modeling: ModelingSettings {
|
modeling: ModelingSettings {
|
||||||
base_unit: UnitLength::Yd,
|
base_unit: UnitLength::Yd,
|
||||||
@ -787,7 +809,7 @@ projectDirectory = "/Users/macinatormax/Documents/kittycad-modeling-projects""#;
|
|||||||
theme_color: None,
|
theme_color: None,
|
||||||
dismiss_web_banner: false,
|
dismiss_web_banner: false,
|
||||||
enable_ssao: None,
|
enable_ssao: None,
|
||||||
stream_idle_mode: false,
|
stream_idle_mode: None,
|
||||||
},
|
},
|
||||||
modeling: ModelingSettings {
|
modeling: ModelingSettings {
|
||||||
base_unit: UnitLength::Mm,
|
base_unit: UnitLength::Mm,
|
||||||
|
@ -123,7 +123,7 @@ includeSettings = false
|
|||||||
theme_color: None,
|
theme_color: None,
|
||||||
dismiss_web_banner: false,
|
dismiss_web_banner: false,
|
||||||
enable_ssao: None,
|
enable_ssao: None,
|
||||||
stream_idle_mode: false,
|
stream_idle_mode: None,
|
||||||
},
|
},
|
||||||
modeling: ModelingSettings {
|
modeling: ModelingSettings {
|
||||||
base_unit: UnitLength::Yd,
|
base_unit: UnitLength::Yd,
|
||||||
|
@ -1,47 +1,25 @@
|
|||||||
use derive_docs::stdlib;
|
use derive_docs::stdlib;
|
||||||
use serde_json::Value as JValue;
|
|
||||||
|
|
||||||
use super::{args::FromArgs, Args, FnAsArg};
|
use super::{args::FromArgs, Args, FnAsArg};
|
||||||
use crate::{
|
use crate::{
|
||||||
errors::{KclError, KclErrorDetails},
|
errors::{KclError, KclErrorDetails},
|
||||||
executor::{ExecState, KclValue, SourceRange, UserVal},
|
executor::{ExecState, KclValue, SourceRange},
|
||||||
function_param::FunctionParam,
|
function_param::FunctionParam,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Apply a function to each element of an array.
|
/// Apply a function to each element of an array.
|
||||||
pub async fn map(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn map(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
let (array, f): (Vec<JValue>, FnAsArg<'_>) = FromArgs::from_args(&args, 0)?;
|
let (array, f): (Vec<KclValue>, FnAsArg<'_>) = FromArgs::from_args(&args, 0)?;
|
||||||
let array: Vec<KclValue> = array
|
let meta = vec![args.source_range.into()];
|
||||||
.into_iter()
|
|
||||||
.map(|jval| {
|
|
||||||
KclValue::UserVal(UserVal {
|
|
||||||
value: jval,
|
|
||||||
meta: vec![args.source_range.into()],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let map_fn = FunctionParam {
|
let map_fn = FunctionParam {
|
||||||
inner: f.func,
|
inner: f.func,
|
||||||
fn_expr: f.expr,
|
fn_expr: f.expr,
|
||||||
meta: vec![args.source_range.into()],
|
meta: meta.clone(),
|
||||||
ctx: args.ctx.clone(),
|
ctx: args.ctx.clone(),
|
||||||
memory: *f.memory,
|
memory: *f.memory,
|
||||||
};
|
};
|
||||||
let new_array = inner_map(array, map_fn, exec_state, &args).await?;
|
let new_array = inner_map(array, map_fn, exec_state, &args).await?;
|
||||||
let unwrapped = new_array
|
Ok(KclValue::Array { value: new_array, meta })
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.map(|k| match k {
|
|
||||||
KclValue::UserVal(user_val) => Ok(user_val.value),
|
|
||||||
_ => Err(()),
|
|
||||||
})
|
|
||||||
.collect::<Result<Vec<_>, _>>();
|
|
||||||
if let Ok(unwrapped) = unwrapped {
|
|
||||||
let uv = UserVal::new(vec![args.source_range.into()], unwrapped);
|
|
||||||
return Ok(KclValue::UserVal(uv));
|
|
||||||
}
|
|
||||||
let uv = UserVal::new(vec![args.source_range.into()], new_array);
|
|
||||||
Ok(KclValue::UserVal(uv))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply a function to every element of a list.
|
/// Apply a function to every element of a list.
|
||||||
@ -110,16 +88,7 @@ async fn call_map_closure<'a>(
|
|||||||
|
|
||||||
/// For each item in an array, update a value.
|
/// For each item in an array, update a value.
|
||||||
pub async fn reduce(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn reduce(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
let (array, start, f): (Vec<JValue>, KclValue, FnAsArg<'_>) = FromArgs::from_args(&args, 0)?;
|
let (array, start, f): (Vec<KclValue>, KclValue, FnAsArg<'_>) = FromArgs::from_args(&args, 0)?;
|
||||||
let array: Vec<KclValue> = array
|
|
||||||
.into_iter()
|
|
||||||
.map(|jval| {
|
|
||||||
KclValue::UserVal(UserVal {
|
|
||||||
value: jval,
|
|
||||||
meta: vec![args.source_range.into()],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let reduce_fn = FunctionParam {
|
let reduce_fn = FunctionParam {
|
||||||
inner: f.func,
|
inner: f.func,
|
||||||
fn_expr: f.expr,
|
fn_expr: f.expr,
|
||||||
@ -206,50 +175,26 @@ async fn call_reduce_closure<'a>(
|
|||||||
#[stdlib {
|
#[stdlib {
|
||||||
name = "push",
|
name = "push",
|
||||||
}]
|
}]
|
||||||
async fn inner_push(array: Vec<KclValue>, elem: KclValue, args: &Args) -> Result<KclValue, KclError> {
|
async fn inner_push(mut array: Vec<KclValue>, elem: KclValue, args: &Args) -> Result<KclValue, KclError> {
|
||||||
// Unwrap the KclValues to JValues for manipulation
|
// Unwrap the KclValues to JValues for manipulation
|
||||||
let mut unwrapped_array = array
|
array.push(elem);
|
||||||
.into_iter()
|
Ok(KclValue::Array {
|
||||||
.map(|k| match k {
|
value: array,
|
||||||
KclValue::UserVal(user_val) => Ok(user_val.value),
|
meta: vec![args.source_range.into()],
|
||||||
_ => Err(KclError::Semantic(KclErrorDetails {
|
})
|
||||||
message: "Expected a UserVal in array".to_string(),
|
|
||||||
source_ranges: vec![args.source_range],
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
|
||||||
|
|
||||||
// Unwrap the element
|
|
||||||
let unwrapped_elem = match elem {
|
|
||||||
KclValue::UserVal(user_val) => user_val.value,
|
|
||||||
_ => {
|
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
|
||||||
message: "Expected a UserVal as element".to_string(),
|
|
||||||
source_ranges: vec![args.source_range],
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Append the element to the array
|
|
||||||
unwrapped_array.push(unwrapped_elem);
|
|
||||||
|
|
||||||
// Wrap the new array into a UserVal with the source range metadata
|
|
||||||
let uv = UserVal::new(vec![args.source_range.into()], unwrapped_array);
|
|
||||||
|
|
||||||
// Return the new array wrapped as a KclValue::UserVal
|
|
||||||
Ok(KclValue::UserVal(uv))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn push(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn push(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
// Extract the array and the element from the arguments
|
// Extract the array and the element from the arguments
|
||||||
let (array_jvalues, elem): (Vec<JValue>, KclValue) = FromArgs::from_args(&args, 0)?;
|
let (val, elem): (KclValue, KclValue) = FromArgs::from_args(&args, 0)?;
|
||||||
|
|
||||||
// Convert the array of JValue into Vec<KclValue>
|
let meta = vec![args.source_range];
|
||||||
let array: Vec<KclValue> = array_jvalues
|
let KclValue::Array { value: array, meta: _ } = val else {
|
||||||
.into_iter()
|
let actual_type = val.human_friendly_type();
|
||||||
.map(|jval| KclValue::UserVal(UserVal::new(vec![args.source_range.into()], jval)))
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
.collect();
|
source_ranges: meta,
|
||||||
|
message: format!("You can't push to a value of type {actual_type}, only an array"),
|
||||||
// Call the inner_push function
|
}));
|
||||||
|
};
|
||||||
inner_push(array, elem, &args).await
|
inner_push(array, elem, &args).await
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ async fn _assert(value: bool, message: &str, args: &Args) -> Result<(), KclError
|
|||||||
pub async fn assert(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn assert(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
let (data, description): (bool, String) = args.get_data()?;
|
let (data, description): (bool, String) = args.get_data()?;
|
||||||
inner_assert(data, &description, &args).await?;
|
inner_assert(data, &description, &args).await?;
|
||||||
Ok(args.make_null_user_val())
|
Ok(args.make_user_val_from_f64(0.0)) // TODO: Add a new Void enum for fns that don't return anything.
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check a value at runtime, and raise an error if the argument provided
|
/// Check a value at runtime, and raise an error if the argument provided
|
||||||
@ -44,7 +44,7 @@ async fn inner_assert(data: bool, message: &str, args: &Args) -> Result<(), KclE
|
|||||||
pub async fn assert_lt(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn assert_lt(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
let (left, right, description): (f64, f64, String) = args.get_data()?;
|
let (left, right, description): (f64, f64, String) = args.get_data()?;
|
||||||
inner_assert_lt(left, right, &description, &args).await?;
|
inner_assert_lt(left, right, &description, &args).await?;
|
||||||
Ok(args.make_null_user_val())
|
Ok(args.make_user_val_from_f64(0.0)) // TODO: Add a new Void enum for fns that don't return anything.
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check that a numerical value is less than to another at runtime,
|
/// Check that a numerical value is less than to another at runtime,
|
||||||
@ -63,7 +63,7 @@ async fn inner_assert_lt(left: f64, right: f64, message: &str, args: &Args) -> R
|
|||||||
pub async fn assert_gt(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn assert_gt(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
let (left, right, description): (f64, f64, String) = args.get_data()?;
|
let (left, right, description): (f64, f64, String) = args.get_data()?;
|
||||||
inner_assert_gt(left, right, &description, &args).await?;
|
inner_assert_gt(left, right, &description, &args).await?;
|
||||||
Ok(args.make_null_user_val())
|
Ok(args.make_user_val_from_f64(0.0)) // TODO: Add a new Void enum for fns that don't return anything.
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check that a numerical value equals another at runtime,
|
/// Check that a numerical value equals another at runtime,
|
||||||
@ -96,7 +96,7 @@ async fn inner_assert_equal(left: f64, right: f64, epsilon: f64, message: &str,
|
|||||||
pub async fn assert_equal(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn assert_equal(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
let (left, right, epsilon, description): (f64, f64, f64, String) = args.get_data()?;
|
let (left, right, epsilon, description): (f64, f64, f64, String) = args.get_data()?;
|
||||||
inner_assert_equal(left, right, epsilon, &description, &args).await?;
|
inner_assert_equal(left, right, epsilon, &description, &args).await?;
|
||||||
Ok(args.make_null_user_val())
|
Ok(args.make_user_val_from_f64(0.0)) // TODO: Add a new Void enum for fns that don't return anything.
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check that a numerical value is greater than another at runtime,
|
/// Check that a numerical value is greater than another at runtime,
|
||||||
@ -115,7 +115,7 @@ async fn inner_assert_gt(left: f64, right: f64, message: &str, args: &Args) -> R
|
|||||||
pub async fn assert_lte(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn assert_lte(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
let (left, right, description): (f64, f64, String) = args.get_data()?;
|
let (left, right, description): (f64, f64, String) = args.get_data()?;
|
||||||
inner_assert_lte(left, right, &description, &args).await?;
|
inner_assert_lte(left, right, &description, &args).await?;
|
||||||
Ok(args.make_null_user_val())
|
Ok(args.make_user_val_from_f64(0.0)) // TODO: Add a new Void enum for fns that don't return anything.
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check that a numerical value is less than or equal to another at runtime,
|
/// Check that a numerical value is less than or equal to another at runtime,
|
||||||
@ -135,7 +135,7 @@ async fn inner_assert_lte(left: f64, right: f64, message: &str, args: &Args) ->
|
|||||||
pub async fn assert_gte(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn assert_gte(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
let (left, right, description): (f64, f64, String) = args.get_data()?;
|
let (left, right, description): (f64, f64, String) = args.get_data()?;
|
||||||
inner_assert_gte(left, right, &description, &args).await?;
|
inner_assert_gte(left, right, &description, &args).await?;
|
||||||
Ok(args.make_null_user_val())
|
Ok(args.make_user_val_from_f64(0.0)) // TODO: Add a new Void enum for fns that don't return anything.
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check that a numerical value is greater than or equal to another at runtime,
|
/// Check that a numerical value is greater than or equal to another at runtime,
|
||||||
|
@ -233,7 +233,7 @@ pub(crate) async fn do_post_extrude(
|
|||||||
tag: path.get_base().tag.clone(),
|
tag: path.get_base().tag.clone(),
|
||||||
geo_meta: GeoMeta {
|
geo_meta: GeoMeta {
|
||||||
id: path.get_base().geo_meta.id,
|
id: path.get_base().geo_meta.id,
|
||||||
metadata: path.get_base().geo_meta.metadata.clone(),
|
metadata: path.get_base().geo_meta.metadata,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
Some(extrude_surface)
|
Some(extrude_surface)
|
||||||
@ -244,7 +244,7 @@ pub(crate) async fn do_post_extrude(
|
|||||||
tag: path.get_base().tag.clone(),
|
tag: path.get_base().tag.clone(),
|
||||||
geo_meta: GeoMeta {
|
geo_meta: GeoMeta {
|
||||||
id: path.get_base().geo_meta.id,
|
id: path.get_base().geo_meta.id,
|
||||||
metadata: path.get_base().geo_meta.metadata.clone(),
|
metadata: path.get_base().geo_meta.metadata,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
Some(extrude_surface)
|
Some(extrude_surface)
|
||||||
@ -259,7 +259,7 @@ pub(crate) async fn do_post_extrude(
|
|||||||
tag: path.get_base().tag.clone(),
|
tag: path.get_base().tag.clone(),
|
||||||
geo_meta: GeoMeta {
|
geo_meta: GeoMeta {
|
||||||
id: path.get_base().geo_meta.id,
|
id: path.get_base().geo_meta.id,
|
||||||
metadata: path.get_base().geo_meta.metadata.clone(),
|
metadata: path.get_base().geo_meta.metadata,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
Some(extrude_surface)
|
Some(extrude_surface)
|
||||||
|
@ -14,7 +14,7 @@ use uuid::Uuid;
|
|||||||
use crate::{
|
use crate::{
|
||||||
ast::types::TagNode,
|
ast::types::TagNode,
|
||||||
errors::{KclError, KclErrorDetails},
|
errors::{KclError, KclErrorDetails},
|
||||||
executor::{EdgeCut, ExecState, ExtrudeSurface, FilletSurface, GeoMeta, KclValue, Solid, TagIdentifier, UserVal},
|
executor::{EdgeCut, ExecState, ExtrudeSurface, FilletSurface, GeoMeta, KclValue, Solid, TagIdentifier},
|
||||||
settings::types::UnitLength,
|
settings::types::UnitLength,
|
||||||
std::Args,
|
std::Args,
|
||||||
};
|
};
|
||||||
@ -186,15 +186,10 @@ pub async fn get_opposite_edge(exec_state: &mut ExecState, args: Args) -> Result
|
|||||||
let tag: TagIdentifier = args.get_data()?;
|
let tag: TagIdentifier = args.get_data()?;
|
||||||
|
|
||||||
let edge = inner_get_opposite_edge(tag, exec_state, args.clone()).await?;
|
let edge = inner_get_opposite_edge(tag, exec_state, args.clone()).await?;
|
||||||
Ok(KclValue::UserVal(UserVal {
|
Ok(KclValue::Uuid {
|
||||||
value: serde_json::to_value(edge).map_err(|e| {
|
value: edge,
|
||||||
KclError::Type(KclErrorDetails {
|
|
||||||
message: format!("Failed to convert Uuid to json: {}", e),
|
|
||||||
source_ranges: vec![args.source_range],
|
|
||||||
})
|
|
||||||
})?,
|
|
||||||
meta: vec![args.source_range.into()],
|
meta: vec![args.source_range.into()],
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the opposite edge to the edge given.
|
/// Get the opposite edge to the edge given.
|
||||||
@ -264,15 +259,10 @@ pub async fn get_next_adjacent_edge(exec_state: &mut ExecState, args: Args) -> R
|
|||||||
let tag: TagIdentifier = args.get_data()?;
|
let tag: TagIdentifier = args.get_data()?;
|
||||||
|
|
||||||
let edge = inner_get_next_adjacent_edge(tag, exec_state, args.clone()).await?;
|
let edge = inner_get_next_adjacent_edge(tag, exec_state, args.clone()).await?;
|
||||||
Ok(KclValue::UserVal(UserVal {
|
Ok(KclValue::Uuid {
|
||||||
value: serde_json::to_value(edge).map_err(|e| {
|
value: edge,
|
||||||
KclError::Type(KclErrorDetails {
|
|
||||||
message: format!("Failed to convert Uuid to json: {}", e),
|
|
||||||
source_ranges: vec![args.source_range],
|
|
||||||
})
|
|
||||||
})?,
|
|
||||||
meta: vec![args.source_range.into()],
|
meta: vec![args.source_range.into()],
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the next adjacent edge to the edge given.
|
/// Get the next adjacent edge to the edge given.
|
||||||
@ -354,15 +344,10 @@ pub async fn get_previous_adjacent_edge(exec_state: &mut ExecState, args: Args)
|
|||||||
let tag: TagIdentifier = args.get_data()?;
|
let tag: TagIdentifier = args.get_data()?;
|
||||||
|
|
||||||
let edge = inner_get_previous_adjacent_edge(tag, exec_state, args.clone()).await?;
|
let edge = inner_get_previous_adjacent_edge(tag, exec_state, args.clone()).await?;
|
||||||
Ok(KclValue::UserVal(UserVal {
|
Ok(KclValue::Uuid {
|
||||||
value: serde_json::to_value(edge).map_err(|e| {
|
value: edge,
|
||||||
KclError::Type(KclErrorDetails {
|
|
||||||
message: format!("Failed to convert Uuid to json: {}", e),
|
|
||||||
source_ranges: vec![args.source_range],
|
|
||||||
})
|
|
||||||
})?,
|
|
||||||
meta: vec![args.source_range.into()],
|
meta: vec![args.source_range.into()],
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the previous adjacent edge to the edge given.
|
/// Get the previous adjacent edge to the edge given.
|
||||||
|
@ -40,7 +40,7 @@ pub async fn cos(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
|
|||||||
let num = args.get_number()?;
|
let num = args.get_number()?;
|
||||||
let result = inner_cos(num)?;
|
let result = inner_cos(num)?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the cosine of a number (in radians).
|
/// Compute the cosine of a number (in radians).
|
||||||
@ -70,7 +70,7 @@ pub async fn sin(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
|
|||||||
let num = args.get_number()?;
|
let num = args.get_number()?;
|
||||||
let result = inner_sin(num)?;
|
let result = inner_sin(num)?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the sine of a number (in radians).
|
/// Compute the sine of a number (in radians).
|
||||||
@ -100,7 +100,7 @@ pub async fn tan(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
|
|||||||
let num = args.get_number()?;
|
let num = args.get_number()?;
|
||||||
let result = inner_tan(num)?;
|
let result = inner_tan(num)?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the tangent of a number (in radians).
|
/// Compute the tangent of a number (in radians).
|
||||||
@ -129,7 +129,7 @@ fn inner_tan(num: f64) -> Result<f64, KclError> {
|
|||||||
pub async fn pi(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn pi(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
let result = inner_pi()?;
|
let result = inner_pi()?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the value of `pi`. Archimedes’ constant (π).
|
/// Return the value of `pi`. Archimedes’ constant (π).
|
||||||
@ -155,7 +155,7 @@ pub async fn sqrt(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
|
|||||||
let num = args.get_number()?;
|
let num = args.get_number()?;
|
||||||
let result = inner_sqrt(num)?;
|
let result = inner_sqrt(num)?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the square root of a number.
|
/// Compute the square root of a number.
|
||||||
@ -185,7 +185,7 @@ pub async fn abs(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
|
|||||||
let num = args.get_number()?;
|
let num = args.get_number()?;
|
||||||
let result = inner_abs(num)?;
|
let result = inner_abs(num)?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the absolute value of a number.
|
/// Compute the absolute value of a number.
|
||||||
@ -222,7 +222,7 @@ pub async fn floor(_exec_state: &mut ExecState, args: Args) -> Result<KclValue,
|
|||||||
let num = args.get_number()?;
|
let num = args.get_number()?;
|
||||||
let result = inner_floor(num)?;
|
let result = inner_floor(num)?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the largest integer less than or equal to a number.
|
/// Compute the largest integer less than or equal to a number.
|
||||||
@ -250,7 +250,7 @@ pub async fn ceil(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
|
|||||||
let num = args.get_number()?;
|
let num = args.get_number()?;
|
||||||
let result = inner_ceil(num)?;
|
let result = inner_ceil(num)?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the smallest integer greater than or equal to a number.
|
/// Compute the smallest integer greater than or equal to a number.
|
||||||
@ -278,7 +278,7 @@ pub async fn min(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
|
|||||||
let nums = args.get_number_array()?;
|
let nums = args.get_number_array()?;
|
||||||
let result = inner_min(nums);
|
let result = inner_min(nums);
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the minimum of the given arguments.
|
/// Compute the minimum of the given arguments.
|
||||||
@ -315,7 +315,7 @@ pub async fn max(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
|
|||||||
let nums = args.get_number_array()?;
|
let nums = args.get_number_array()?;
|
||||||
let result = inner_max(nums);
|
let result = inner_max(nums);
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the maximum of the given arguments.
|
/// Compute the maximum of the given arguments.
|
||||||
@ -366,7 +366,7 @@ pub async fn pow(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
|
|||||||
|
|
||||||
let result = inner_pow(nums[0], nums[1])?;
|
let result = inner_pow(nums[0], nums[1])?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the number to a power.
|
/// Compute the number to a power.
|
||||||
@ -396,7 +396,7 @@ pub async fn acos(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
|
|||||||
let num = args.get_number()?;
|
let num = args.get_number()?;
|
||||||
let result = inner_acos(num)?;
|
let result = inner_acos(num)?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the arccosine of a number (in radians).
|
/// Compute the arccosine of a number (in radians).
|
||||||
@ -427,7 +427,7 @@ pub async fn asin(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
|
|||||||
let num = args.get_number()?;
|
let num = args.get_number()?;
|
||||||
let result = inner_asin(num)?;
|
let result = inner_asin(num)?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the arcsine of a number (in radians).
|
/// Compute the arcsine of a number (in radians).
|
||||||
@ -457,7 +457,7 @@ pub async fn atan(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
|
|||||||
let num = args.get_number()?;
|
let num = args.get_number()?;
|
||||||
let result = inner_atan(num)?;
|
let result = inner_atan(num)?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the arctangent of a number (in radians).
|
/// Compute the arctangent of a number (in radians).
|
||||||
@ -504,7 +504,7 @@ pub async fn log(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
|
|||||||
}
|
}
|
||||||
let result = inner_log(nums[0], nums[1])?;
|
let result = inner_log(nums[0], nums[1])?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the logarithm of the number with respect to an arbitrary base.
|
/// Compute the logarithm of the number with respect to an arbitrary base.
|
||||||
@ -536,7 +536,7 @@ pub async fn log2(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
|
|||||||
let num = args.get_number()?;
|
let num = args.get_number()?;
|
||||||
let result = inner_log2(num)?;
|
let result = inner_log2(num)?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the base 2 logarithm of the number.
|
/// Compute the base 2 logarithm of the number.
|
||||||
@ -564,7 +564,7 @@ pub async fn log10(_exec_state: &mut ExecState, args: Args) -> Result<KclValue,
|
|||||||
let num = args.get_number()?;
|
let num = args.get_number()?;
|
||||||
let result = inner_log10(num)?;
|
let result = inner_log10(num)?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the base 10 logarithm of the number.
|
/// Compute the base 10 logarithm of the number.
|
||||||
@ -592,7 +592,7 @@ pub async fn ln(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl
|
|||||||
let num = args.get_number()?;
|
let num = args.get_number()?;
|
||||||
let result = inner_ln(num)?;
|
let result = inner_ln(num)?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the natural logarithm of the number.
|
/// Compute the natural logarithm of the number.
|
||||||
@ -619,7 +619,7 @@ fn inner_ln(num: f64) -> Result<f64, KclError> {
|
|||||||
pub async fn e(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn e(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
let result = inner_e()?;
|
let result = inner_e()?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the value of Euler’s number `e`.
|
/// Return the value of Euler’s number `e`.
|
||||||
@ -648,7 +648,7 @@ fn inner_e() -> Result<f64, KclError> {
|
|||||||
pub async fn tau(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn tau(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
let result = inner_tau()?;
|
let result = inner_tau()?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the value of `tau`. The full circle constant (τ). Equal to 2π.
|
/// Return the value of `tau`. The full circle constant (τ). Equal to 2π.
|
||||||
@ -678,7 +678,7 @@ pub async fn to_radians(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
|
|||||||
let num = args.get_number()?;
|
let num = args.get_number()?;
|
||||||
let result = inner_to_radians(num)?;
|
let result = inner_to_radians(num)?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts a number from degrees to radians.
|
/// Converts a number from degrees to radians.
|
||||||
@ -708,7 +708,7 @@ pub async fn to_degrees(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
|
|||||||
let num = args.get_number()?;
|
let num = args.get_number()?;
|
||||||
let result = inner_to_degrees(num)?;
|
let result = inner_to_degrees(num)?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts a number from radians to degrees.
|
/// Converts a number from radians to degrees.
|
||||||
|
@ -244,7 +244,7 @@ pub enum FunctionKind {
|
|||||||
pub async fn leg_length(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn leg_length(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
let (hypotenuse, leg) = args.get_hypotenuse_leg()?;
|
let (hypotenuse, leg) = args.get_hypotenuse_leg()?;
|
||||||
let result = inner_leg_length(hypotenuse, leg);
|
let result = inner_leg_length(hypotenuse, leg);
|
||||||
args.make_user_val_from_f64(result)
|
Ok(KclValue::from_number(result, vec![args.into()]))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the length of the given leg.
|
/// Compute the length of the given leg.
|
||||||
@ -264,7 +264,7 @@ fn inner_leg_length(hypotenuse: f64, leg: f64) -> f64 {
|
|||||||
pub async fn leg_angle_x(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn leg_angle_x(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
let (hypotenuse, leg) = args.get_hypotenuse_leg()?;
|
let (hypotenuse, leg) = args.get_hypotenuse_leg()?;
|
||||||
let result = inner_leg_angle_x(hypotenuse, leg);
|
let result = inner_leg_angle_x(hypotenuse, leg);
|
||||||
args.make_user_val_from_f64(result)
|
Ok(KclValue::from_number(result, vec![args.into()]))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the angle of the given leg for x.
|
/// Compute the angle of the given leg for x.
|
||||||
@ -284,7 +284,7 @@ fn inner_leg_angle_x(hypotenuse: f64, leg: f64) -> f64 {
|
|||||||
pub async fn leg_angle_y(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn leg_angle_y(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
let (hypotenuse, leg) = args.get_hypotenuse_leg()?;
|
let (hypotenuse, leg) = args.get_hypotenuse_leg()?;
|
||||||
let result = inner_leg_angle_y(hypotenuse, leg);
|
let result = inner_leg_angle_y(hypotenuse, leg);
|
||||||
args.make_user_val_from_f64(result)
|
Ok(KclValue::from_number(result, vec![args.into()]))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the angle of the given leg for y.
|
/// Compute the angle of the given leg for y.
|
||||||
|
@ -14,13 +14,10 @@ use kittycad_modeling_cmds::{
|
|||||||
};
|
};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value as JValue;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
errors::{KclError, KclErrorDetails},
|
errors::{KclError, KclErrorDetails},
|
||||||
executor::{
|
executor::{ExecState, Geometries, Geometry, KclValue, Point3d, Sketch, SketchSet, Solid, SolidSet, SourceRange},
|
||||||
ExecState, Geometries, Geometry, KclValue, Point3d, Sketch, SketchSet, Solid, SolidSet, SourceRange, UserVal,
|
|
||||||
},
|
|
||||||
function_param::FunctionParam,
|
function_param::FunctionParam,
|
||||||
std::{types::Uint, Args},
|
std::{types::Uint, Args},
|
||||||
};
|
};
|
||||||
@ -361,10 +358,10 @@ async fn make_transform<'a>(
|
|||||||
exec_state: &mut ExecState,
|
exec_state: &mut ExecState,
|
||||||
) -> Result<Transform, KclError> {
|
) -> Result<Transform, KclError> {
|
||||||
// Call the transform fn for this repetition.
|
// Call the transform fn for this repetition.
|
||||||
let repetition_num = KclValue::UserVal(UserVal {
|
let repetition_num = KclValue::Int {
|
||||||
value: JValue::Number(i.into()),
|
value: i.into(),
|
||||||
meta: vec![source_range.into()],
|
meta: vec![source_range.into()],
|
||||||
});
|
};
|
||||||
let transform_fn_args = vec![repetition_num];
|
let transform_fn_args = vec![repetition_num];
|
||||||
let transform_fn_return = transform_function.call(exec_state, transform_fn_args).await?;
|
let transform_fn_return = transform_function.call(exec_state, transform_fn_args).await?;
|
||||||
|
|
||||||
@ -376,7 +373,7 @@ async fn make_transform<'a>(
|
|||||||
source_ranges: source_ranges.clone(),
|
source_ranges: source_ranges.clone(),
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
let KclValue::UserVal(transform) = transform_fn_return else {
|
let KclValue::Object { value: transform, meta } = transform_fn_return else {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
message: "Transform function must return a transform object".to_string(),
|
message: "Transform function must return a transform object".to_string(),
|
||||||
source_ranges: source_ranges.clone(),
|
source_ranges: source_ranges.clone(),
|
||||||
@ -384,9 +381,9 @@ async fn make_transform<'a>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Apply defaults to the transform.
|
// Apply defaults to the transform.
|
||||||
let replicate = match transform.value.get("replicate") {
|
let replicate = match transform.get("replicate") {
|
||||||
Some(JValue::Bool(true)) => true,
|
Some(KclValue::Bool { value: true, .. }) => true,
|
||||||
Some(JValue::Bool(false)) => false,
|
Some(KclValue::Bool { value: false, .. }) => false,
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
message: "The 'replicate' key must be a bool".to_string(),
|
message: "The 'replicate' key must be a bool".to_string(),
|
||||||
@ -395,38 +392,43 @@ async fn make_transform<'a>(
|
|||||||
}
|
}
|
||||||
None => true,
|
None => true,
|
||||||
};
|
};
|
||||||
let scale = match transform.value.get("scale") {
|
let scale = match transform.get("scale") {
|
||||||
Some(x) => array_to_point3d(x, source_ranges.clone())?,
|
Some(x) => array_to_point3d(x, source_ranges.clone())?,
|
||||||
None => Point3d { x: 1.0, y: 1.0, z: 1.0 },
|
None => Point3d { x: 1.0, y: 1.0, z: 1.0 },
|
||||||
};
|
};
|
||||||
let translate = match transform.value.get("translate") {
|
let translate = match transform.get("translate") {
|
||||||
Some(x) => array_to_point3d(x, source_ranges.clone())?,
|
Some(x) => array_to_point3d(x, source_ranges.clone())?,
|
||||||
None => Point3d { x: 0.0, y: 0.0, z: 0.0 },
|
None => Point3d { x: 0.0, y: 0.0, z: 0.0 },
|
||||||
};
|
};
|
||||||
let mut rotation = Rotation::default();
|
let mut rotation = Rotation::default();
|
||||||
if let Some(rot) = transform.value.get("rotation") {
|
if let Some(rot) = transform.get("rotation") {
|
||||||
|
let KclValue::Object { value: rot, meta: _ } = rot else {
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: "The 'rotation' key must be an object (with optional fields 'angle', 'axis' and 'origin')"
|
||||||
|
.to_string(),
|
||||||
|
source_ranges: source_ranges.clone(),
|
||||||
|
}));
|
||||||
|
};
|
||||||
if let Some(axis) = rot.get("axis") {
|
if let Some(axis) = rot.get("axis") {
|
||||||
rotation.axis = array_to_point3d(axis, source_ranges.clone())?.into();
|
rotation.axis = array_to_point3d(axis, source_ranges.clone())?.into();
|
||||||
}
|
}
|
||||||
if let Some(angle) = rot.get("angle") {
|
if let Some(angle) = rot.get("angle") {
|
||||||
match angle {
|
match angle {
|
||||||
JValue::Number(number) => {
|
KclValue::Number { value: number, meta: _ } => {
|
||||||
if let Some(number) = number.as_f64() {
|
rotation.angle = Angle::from_degrees(*number);
|
||||||
rotation.angle = Angle::from_degrees(number);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
message: "The 'rotation.angle' key must be a number (of degrees)".to_string(),
|
message: "The 'rotation.angle' key must be a number (of degrees)".to_string(),
|
||||||
source_ranges: source_ranges.clone(),
|
source_ranges: meta.iter().map(|m| m.source_range).collect(),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(origin) = rot.get("origin") {
|
if let Some(origin) = rot.get("origin") {
|
||||||
rotation.origin = match origin {
|
rotation.origin = match origin {
|
||||||
JValue::String(s) if s == "local" => OriginType::Local,
|
KclValue::String { value: s, meta: _ } if s == "local" => OriginType::Local,
|
||||||
JValue::String(s) if s == "global" => OriginType::Global,
|
KclValue::String { value: s, meta: _ } if s == "global" => OriginType::Global,
|
||||||
other => {
|
other => {
|
||||||
let origin = array_to_point3d(other, source_ranges.clone())?.into();
|
let origin = array_to_point3d(other, source_ranges.clone())?.into();
|
||||||
OriginType::Custom { origin }
|
OriginType::Custom { origin }
|
||||||
@ -443,8 +445,8 @@ async fn make_transform<'a>(
|
|||||||
Ok(t)
|
Ok(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn array_to_point3d(json: &JValue, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError> {
|
fn array_to_point3d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError> {
|
||||||
let JValue::Array(arr) = json else {
|
let KclValue::Array { value: arr, meta } = val else {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
message: "Expected an array of 3 numbers (i.e. a 3D point)".to_string(),
|
message: "Expected an array of 3 numbers (i.e. a 3D point)".to_string(),
|
||||||
source_ranges,
|
source_ranges,
|
||||||
@ -457,17 +459,21 @@ fn array_to_point3d(json: &JValue, source_ranges: Vec<SourceRange>) -> Result<Po
|
|||||||
source_ranges,
|
source_ranges,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
// Gets an f64 from a JSON value, returns Option.
|
// Gets an f64 from a KCL value.
|
||||||
let f = |j: &JValue| j.as_number().and_then(|num| num.as_f64()).map(|x| x.to_owned());
|
let f = |k: &KclValue, component: char| {
|
||||||
let err = |component| {
|
use super::args::FromKclValue;
|
||||||
KclError::Semantic(KclErrorDetails {
|
if let Some(value) = f64::from_mem_item(k) {
|
||||||
message: format!("{component} component of this point was not a number"),
|
Ok(value)
|
||||||
source_ranges: source_ranges.clone(),
|
} else {
|
||||||
})
|
Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!("{component} component of this point was not a number"),
|
||||||
|
source_ranges: meta.iter().map(|m| m.source_range).collect(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let x = f(&arr[0]).ok_or_else(|| err("X"))?;
|
let x = f(&arr[0], 'x')?;
|
||||||
let y = f(&arr[1]).ok_or_else(|| err("Y"))?;
|
let y = f(&arr[1], 'y')?;
|
||||||
let z = f(&arr[2]).ok_or_else(|| err("Z"))?;
|
let z = f(&arr[2], 'z')?;
|
||||||
Ok(Point3d { x, y, z })
|
Ok(Point3d { x, y, z })
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -477,8 +483,22 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_array_to_point3d() {
|
fn test_array_to_point3d() {
|
||||||
let input = serde_json::json! {
|
let input = KclValue::Array {
|
||||||
[1.1, 2.2, 3.3]
|
value: vec![
|
||||||
|
KclValue::Number {
|
||||||
|
value: 1.1,
|
||||||
|
meta: Default::default(),
|
||||||
|
},
|
||||||
|
KclValue::Number {
|
||||||
|
value: 2.2,
|
||||||
|
meta: Default::default(),
|
||||||
|
},
|
||||||
|
KclValue::Number {
|
||||||
|
value: 3.3,
|
||||||
|
meta: Default::default(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
meta: Default::default(),
|
||||||
};
|
};
|
||||||
let expected = Point3d { x: 1.1, y: 2.2, z: 3.3 };
|
let expected = Point3d { x: 1.1, y: 2.2, z: 3.3 };
|
||||||
let actual = array_to_point3d(&input, Vec::new());
|
let actual = array_to_point3d(&input, Vec::new());
|
||||||
|
@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
errors::KclError,
|
errors::KclError,
|
||||||
executor::{ExecState, KclValue, Metadata, Plane, UserVal},
|
executor::{ExecState, KclValue, Plane},
|
||||||
std::{sketch::PlaneData, Args},
|
std::{sketch::PlaneData, Args},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -50,15 +50,9 @@ impl From<StandardPlane> for PlaneData {
|
|||||||
/// Offset a plane by a distance along its normal.
|
/// Offset a plane by a distance along its normal.
|
||||||
pub async fn offset_plane(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn offset_plane(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
let (std_plane, offset): (StandardPlane, f64) = args.get_data_and_float()?;
|
let (std_plane, offset): (StandardPlane, f64) = args.get_data_and_float()?;
|
||||||
|
let plane_data = inner_offset_plane(std_plane, offset, exec_state).await?;
|
||||||
let plane = inner_offset_plane(std_plane, offset, exec_state).await?;
|
let plane = Plane::from_plane_data(plane_data, exec_state);
|
||||||
|
Ok(KclValue::Plane(Box::new(plane)))
|
||||||
Ok(KclValue::UserVal(UserVal::new(
|
|
||||||
vec![Metadata {
|
|
||||||
source_range: args.source_range,
|
|
||||||
}],
|
|
||||||
plane,
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Offset a plane by a distance along its normal.
|
/// Offset a plane by a distance along its normal.
|
||||||
@ -129,6 +123,20 @@ pub async fn offset_plane(exec_state: &mut ExecState, args: Args) -> Result<KclV
|
|||||||
///
|
///
|
||||||
/// loft([squareSketch, circleSketch])
|
/// loft([squareSketch, circleSketch])
|
||||||
/// ```
|
/// ```
|
||||||
|
/// ```no_run
|
||||||
|
/// // A circle on the XY plane
|
||||||
|
/// startSketchOn("XY")
|
||||||
|
/// |> startProfileAt([0, 0], %)
|
||||||
|
/// |> circle({radius: 10, center: [0, 0]}, %)
|
||||||
|
///
|
||||||
|
/// // Triangle on the plane 4 units above
|
||||||
|
/// startSketchOn(offsetPlane("XY", 4))
|
||||||
|
/// |> startProfileAt([0, 0], %)
|
||||||
|
/// |> line([10, 0], %)
|
||||||
|
/// |> line([0, 10], %)
|
||||||
|
/// |> close(%)
|
||||||
|
/// ```
|
||||||
|
|
||||||
#[stdlib {
|
#[stdlib {
|
||||||
name = "offsetPlane",
|
name = "offsetPlane",
|
||||||
}]
|
}]
|
||||||
|
@ -60,7 +60,7 @@ pub async fn segment_end_x(exec_state: &mut ExecState, args: Args) -> Result<Kcl
|
|||||||
let tag: TagIdentifier = args.get_data()?;
|
let tag: TagIdentifier = args.get_data()?;
|
||||||
let result = inner_segment_end_x(&tag, exec_state, args.clone())?;
|
let result = inner_segment_end_x(&tag, exec_state, args.clone())?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the ending point of the provided line segment along the 'x' axis.
|
/// Compute the ending point of the provided line segment along the 'x' axis.
|
||||||
@ -96,7 +96,7 @@ pub async fn segment_end_y(exec_state: &mut ExecState, args: Args) -> Result<Kcl
|
|||||||
let tag: TagIdentifier = args.get_data()?;
|
let tag: TagIdentifier = args.get_data()?;
|
||||||
let result = inner_segment_end_y(&tag, exec_state, args.clone())?;
|
let result = inner_segment_end_y(&tag, exec_state, args.clone())?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the ending point of the provided line segment along the 'y' axis.
|
/// Compute the ending point of the provided line segment along the 'y' axis.
|
||||||
@ -179,7 +179,7 @@ pub async fn segment_start_x(exec_state: &mut ExecState, args: Args) -> Result<K
|
|||||||
let tag: TagIdentifier = args.get_data()?;
|
let tag: TagIdentifier = args.get_data()?;
|
||||||
let result = inner_segment_start_x(&tag, exec_state, args.clone())?;
|
let result = inner_segment_start_x(&tag, exec_state, args.clone())?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the starting point of the provided line segment along the 'x' axis.
|
/// Compute the starting point of the provided line segment along the 'x' axis.
|
||||||
@ -215,7 +215,7 @@ pub async fn segment_start_y(exec_state: &mut ExecState, args: Args) -> Result<K
|
|||||||
let tag: TagIdentifier = args.get_data()?;
|
let tag: TagIdentifier = args.get_data()?;
|
||||||
let result = inner_segment_start_y(&tag, exec_state, args.clone())?;
|
let result = inner_segment_start_y(&tag, exec_state, args.clone())?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the starting point of the provided line segment along the 'y' axis.
|
/// Compute the starting point of the provided line segment along the 'y' axis.
|
||||||
@ -251,7 +251,7 @@ pub async fn last_segment_x(_exec_state: &mut ExecState, args: Args) -> Result<K
|
|||||||
let sketch = args.get_sketch()?;
|
let sketch = args.get_sketch()?;
|
||||||
let result = inner_last_segment_x(sketch, args.clone())?;
|
let result = inner_last_segment_x(sketch, args.clone())?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract the 'x' axis value of the last line segment in the provided 2-d
|
/// Extract the 'x' axis value of the last line segment in the provided 2-d
|
||||||
@ -291,7 +291,7 @@ pub async fn last_segment_y(_exec_state: &mut ExecState, args: Args) -> Result<K
|
|||||||
let sketch = args.get_sketch()?;
|
let sketch = args.get_sketch()?;
|
||||||
let result = inner_last_segment_y(sketch, args.clone())?;
|
let result = inner_last_segment_y(sketch, args.clone())?;
|
||||||
|
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract the 'y' axis value of the last line segment in the provided 2-d
|
/// Extract the 'y' axis value of the last line segment in the provided 2-d
|
||||||
@ -330,7 +330,7 @@ fn inner_last_segment_y(sketch: Sketch, args: Args) -> Result<f64, KclError> {
|
|||||||
pub async fn segment_length(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn segment_length(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
let tag: TagIdentifier = args.get_data()?;
|
let tag: TagIdentifier = args.get_data()?;
|
||||||
let result = inner_segment_length(&tag, exec_state, args.clone())?;
|
let result = inner_segment_length(&tag, exec_state, args.clone())?;
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the length of the provided line segment.
|
/// Compute the length of the provided line segment.
|
||||||
@ -376,7 +376,7 @@ pub async fn segment_angle(exec_state: &mut ExecState, args: Args) -> Result<Kcl
|
|||||||
let tag: TagIdentifier = args.get_data()?;
|
let tag: TagIdentifier = args.get_data()?;
|
||||||
|
|
||||||
let result = inner_segment_angle(&tag, exec_state, args.clone())?;
|
let result = inner_segment_angle(&tag, exec_state, args.clone())?;
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the angle (in degrees) of the provided line segment.
|
/// Compute the angle (in degrees) of the provided line segment.
|
||||||
@ -415,10 +415,10 @@ fn inner_segment_angle(tag: &TagIdentifier, exec_state: &mut ExecState, args: Ar
|
|||||||
pub async fn angle_to_match_length_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn angle_to_match_length_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
let (tag, to, sketch) = args.get_tag_to_number_sketch()?;
|
let (tag, to, sketch) = args.get_tag_to_number_sketch()?;
|
||||||
let result = inner_angle_to_match_length_x(&tag, to, sketch, exec_state, args.clone())?;
|
let result = inner_angle_to_match_length_x(&tag, to, sketch, exec_state, args.clone())?;
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the angle (in degrees) in o
|
/// Returns the angle to match the given length for x.
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// const sketch001 = startSketchOn('XZ')
|
/// const sketch001 = startSketchOn('XZ')
|
||||||
@ -478,7 +478,7 @@ fn inner_angle_to_match_length_x(
|
|||||||
pub async fn angle_to_match_length_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn angle_to_match_length_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
let (tag, to, sketch) = args.get_tag_to_number_sketch()?;
|
let (tag, to, sketch) = args.get_tag_to_number_sketch()?;
|
||||||
let result = inner_angle_to_match_length_y(&tag, to, sketch, exec_state, args.clone())?;
|
let result = inner_angle_to_match_length_y(&tag, to, sketch, exec_state, args.clone())?;
|
||||||
args.make_user_val_from_f64(result)
|
Ok(args.make_user_val_from_f64(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the angle to match the given length for y.
|
/// Returns the angle to match the given length for y.
|
||||||
|