Compare commits
50 Commits
stream-pau
...
v0.26.3
Author | SHA1 | Date | |
---|---|---|---|
64f0f5b773 | |||
f452f9bf00 | |||
97705234c6 | |||
30dfc167d3 | |||
d8105627c0 | |||
6b7fac3642 | |||
35805916aa | |||
4a4400e979 | |||
efd1f288b9 | |||
0337ab9cff | |||
f0dda692f6 | |||
2ce0c59d08 | |||
393b43d485 | |||
4fbcde8773 | |||
12d444fa69 | |||
683b4488af | |||
e1c1e07046 | |||
984420c155 | |||
7bad60dfa3 | |||
aaca88220c | |||
360384e8c8 | |||
ab2ad1313f | |||
897205acc2 | |||
862ca1124e | |||
d9981d9d7b | |||
8df0581831 | |||
54e6358df1 | |||
daf20a978d | |||
8e64798dda | |||
a1ceb4fa47 | |||
2db8d13051 | |||
aceb8052e2 | |||
62fae1e93b | |||
2abfbb9788 | |||
ad1cd56891 | |||
26951364cf | |||
26e995dc3f | |||
a8b816a3e2 | |||
43bec115c0 | |||
0c6c646fe7 | |||
0d52851da2 | |||
6b105897f7 | |||
9ff51de301 | |||
c161f578fd | |||
4804eedf3e | |||
99db31a6a4 | |||
90b57ec202 | |||
3f86f99f5e | |||
83e2b093a6 | |||
58f7e0086d |
4
.github/workflows/cargo-test.yml
vendored
@ -5,8 +5,6 @@ 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'
|
||||||
@ -17,8 +15,6 @@ 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
|
yarn build:wasm-dev
|
||||||
|
|
||||||
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
|
yarn build:wasm-dev # or yarn build:wasm for slower but more production-like build
|
||||||
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: "Returns the angle to match the given length for x."
|
excerpt: "Compute the angle (in degrees) in o"
|
||||||
layout: manual
|
layout: manual
|
||||||
---
|
---
|
||||||
|
|
||||||
Returns the angle to match the given length for x.
|
Compute the angle (in degrees) in o
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
---
|
|
||||||
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
@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
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,110 +23,8 @@ Any KCL value.
|
|||||||
|
|
||||||
| Property | Type | Description | Required |
|
| Property | Type | Description | Required |
|
||||||
|----------|------|-------------|----------|
|
|----------|------|-------------|----------|
|
||||||
| `type` |enum: `Uuid`| | No |
|
| `type` |enum: `UserVal`| | No |
|
||||||
| `value` |`string`| | No |
|
| `value` |``| | 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 |
|
||||||
|
|
||||||
|
|
||||||
@ -213,38 +111,6 @@ 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.
|
||||||
|
|
||||||
@ -324,23 +190,6 @@ 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}, %)
|
||||||
|> xLine(${commonPoints.num1}, %)`)
|
|> line([${commonPoints.num1}, 0], %)`)
|
||||||
}
|
}
|
||||||
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}, %)
|
||||||
|> xLine(${commonPoints.num1}, %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> yLine(${commonPoints.num1 + 0.01}, %)`)
|
|> line([0, ${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}, %)
|
||||||
|> xLine(${commonPoints.num1}, %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> yLine(${commonPoints.num1 + 0.01}, %)
|
|> line([0, ${commonPoints.num1 + 0.01}], %)
|
||||||
|> xLine(${commonPoints.num2 * -1}, %)`)
|
|> lineTo([0, ${commonPoints.num3}], %)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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}, %)
|
||||||
|> xLine(${commonPoints.num1}, %, $seg01)
|
|> line([${commonPoints.num1}, 0], %, $seg01)
|
||||||
|> yLine(${commonPoints.num1 + 0.01}, %)
|
|> line([0, ${commonPoints.num1 + 0.01}], %)
|
||||||
|> xLine(-segLen(seg01), %)`)
|
|> angledLine([180, segLen(seg01)], %)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
test.describe('Basic sketch', () => {
|
test.describe('Basic sketch', () => {
|
||||||
|
@ -694,9 +694,6 @@ 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, origin, and horizontal snapping`, async ({
|
test(`Verify axis and origin snapping`, async ({
|
||||||
app,
|
app,
|
||||||
editor,
|
editor,
|
||||||
toolbar,
|
toolbar,
|
||||||
@ -505,7 +505,7 @@ test(`Verify axis, origin, and horizontal 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: `xLine(${xAxisSloppy.kcl[0]}, %)`,
|
segmentOnXAxis: `lineTo([${xAxisSloppy.kcl[0]}, ${xAxisSloppy.kcl[1]}], %)`,
|
||||||
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], %)
|
||||||
|> xLine(12.73, %)
|
|> line([12.73, -0.09], %)
|
||||||
|> 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], %)
|
||||||
|> yLine(12.34, %)
|
|> line([-12.34, 12.34], %)
|
||||||
|
|
||||||
`)
|
`)
|
||||||
}).toPass({ timeout: 40_000, intervals: [1_000] })
|
}).toPass({ timeout: 40_000, intervals: [1_000] })
|
||||||
@ -202,19 +202,35 @@ test.describe('Sketch tests', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
const viewport = { width: 1200, height: 500 }
|
await page.setViewportSize({ 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()
|
||||||
|
|
||||||
const center = {
|
await page.waitForTimeout(100)
|
||||||
x: viewport.width / 2,
|
await u.openAndClearDebugPanel()
|
||||||
y: viewport.height / 2,
|
await u.sendCustomCmd({
|
||||||
}
|
type: 'modeling_cmd_req',
|
||||||
const modelAreaSize = await u.getModelViewAreaSize()
|
cmd_id: uuidv4(),
|
||||||
|
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')) {
|
||||||
@ -228,7 +244,7 @@ test.describe('Sketch tests', () => {
|
|||||||
await expect(u.codeLocator).not.toBeVisible()
|
await expect(u.codeLocator).not.toBeVisible()
|
||||||
}
|
}
|
||||||
|
|
||||||
const startPX = [center.x + 65, 458]
|
const startPX = [665, 458]
|
||||||
|
|
||||||
const dragPX = 30
|
const dragPX = 30
|
||||||
let prevContent = ''
|
let prevContent = ''
|
||||||
@ -239,7 +255,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(center.x + 100, 370)
|
await page.mouse.click(700, 370)
|
||||||
}
|
}
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Edit Sketch' })
|
page.getByRole('button', { name: 'Edit Sketch' })
|
||||||
@ -250,74 +266,45 @@ 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)
|
||||||
|
|
||||||
test.step('drag startProfileAt handle', async () => {
|
// drag startProfieAt 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)
|
||||||
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)
|
||||||
|
|
||||||
test.step('drag line handle', async () => {
|
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
|
||||||
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
|
await page.mouse.move(lineEnd.x - 5, lineEnd.y)
|
||||||
await page.mouse.move(lineEnd.x - 5, lineEnd.y)
|
await page.mouse.down()
|
||||||
await page.mouse.down()
|
await page.mouse.move(lineEnd.x + dragPX, lineEnd.y - dragPX, step5)
|
||||||
await page.mouse.move(lineEnd.x + dragPX, lineEnd.y - dragPX, step5)
|
await page.mouse.up()
|
||||||
await page.mouse.up()
|
await page.waitForTimeout(100)
|
||||||
await page.waitForTimeout(100)
|
if (openPanes.includes('code')) {
|
||||||
if (openPanes.includes('code')) {
|
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
prevContent = await page.locator('.cm-content').innerText()
|
||||||
prevContent = await page.locator('.cm-content').innerText()
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test.step('drag tangentialArcTo handle', async () => {
|
// drag tangentialArcTo handle
|
||||||
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(
|
await page.mouse.move(tangentEnd.x + dragPX, tangentEnd.y - dragPX, step5)
|
||||||
tangentEnd.x + dragPX,
|
await page.mouse.up()
|
||||||
tangentEnd.y - dragPX,
|
await page.waitForTimeout(100)
|
||||||
step5
|
if (openPanes.includes('code')) {
|
||||||
)
|
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()
|
||||||
@ -593,7 +580,7 @@ test.describe('Sketch tests', () => {
|
|||||||
})
|
})
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
const center = await u.getCenterOfModelViewArea()
|
const startPX = [665, 458]
|
||||||
|
|
||||||
const dragPX = 30
|
const dragPX = 30
|
||||||
|
|
||||||
@ -609,7 +596,7 @@ test.describe('Sketch tests', () => {
|
|||||||
|
|
||||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
|
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
|
||||||
|
|
||||||
// drag startProfileAt handle
|
// drag startProfieAt 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)
|
||||||
@ -651,7 +638,6 @@ 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)
|
||||||
|
|
||||||
@ -659,7 +645,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, toU, click00r } = getMovementUtils({ center, page })
|
const { toSU, click00r } = getMovementUtils({ center, page })
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
@ -675,32 +661,29 @@ 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 - 50, viewportSize.height * 0.55)
|
await page.mouse.click(center.x, 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
|
||||||
|
|
||||||
const { click00r } = await getMovementUtils({ center, page })
|
await click00r(0, 0)
|
||||||
|
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 += ` |> xLine(${toU(50, 0)[0]}, %)`
|
codeStr += ` |> lineTo(${toSU([50, 0])}, %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(0, 50)
|
await click00r(0, 50)
|
||||||
codeStr += ` |> yLine(${toU(0, 50)[1]}, %)`
|
codeStr += ` |> line(${toSU([0, 50])}, %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(-50, 0)
|
let clickCoords = await click00r(-50, 0)
|
||||||
codeStr += ` |> xLine(${toU(-50, 0)[0]}, %)`
|
expect(clickCoords).not.toBeUndefined()
|
||||||
|
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
|
||||||
@ -716,29 +699,28 @@ 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.move(center.x - 100, center.y + 50, { steps: 5 })
|
await page.mouse.click(center.x + 200, center.y + 100)
|
||||||
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()
|
||||||
|
|
||||||
coord = await click00r(30, 0)
|
await click00r(30, 0)
|
||||||
codeStr += ` |> startProfileAt(${coord.kcl}, %)`
|
codeStr += ` |> startProfileAt([2.03, 0], %)`
|
||||||
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 += ` |> xLine(2.04, %)`
|
codeStr += ` |> lineTo([4.07, 0], %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(0, 30)
|
await click00r(0, 30)
|
||||||
codeStr += ` |> yLine(-2.03, %)`
|
codeStr += ` |> line([0, -2.03], %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(-30, 0)
|
await click00r(-30, 0)
|
||||||
codeStr += ` |> xLine(-2.04, %)`
|
codeStr += ` |> line([-2.04, 0], %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(undefined, undefined)
|
await click00r(undefined, undefined)
|
||||||
@ -762,8 +744,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)}], %)
|
||||||
|> xLine(${roundOff(scale * 139.19)}, %)
|
|> line([${roundOff(scale * 139.19)}, 0], %)
|
||||||
|> yLine(-${roundOff(scale * 139.2)}, %)
|
|> line([0, -${roundOff(scale * 139.2)}], %)
|
||||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|> close(%)`
|
|> close(%)`
|
||||||
|
|
||||||
@ -782,21 +764,20 @@ 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(center.x + 100, 200, { steps: 10 })
|
await page.mouse.move(700, 200, { steps: 10 })
|
||||||
await page.mouse.click(center.x + 100, 200, { delay: 200 })
|
await page.mouse.click(700, 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 = [center.x + 100, 200]
|
const pointA = [700, 200]
|
||||||
const pointB = [center.x + 300, 200]
|
const pointB = [900, 200]
|
||||||
const pointC = [center.x + 300, 400]
|
const pointC = [900, 400]
|
||||||
|
|
||||||
// draw three lines
|
// draw three lines
|
||||||
await page.waitForTimeout(500)
|
await page.waitForTimeout(500)
|
||||||
@ -933,9 +914,7 @@ extrude001 = extrude(5, sketch001)
|
|||||||
|
|
||||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
|
|
||||||
const center = await u.getCenterOfModelViewArea()
|
await page.mouse.click(622, 355)
|
||||||
|
|
||||||
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 += `
|
||||||
|> xLine(7.25, %)`
|
|> line([7.25, 0], %)`
|
||||||
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 += `
|
||||||
|> xLine(7.25, %)`
|
|> line([7.25, 0], %)`
|
||||||
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 += `
|
||||||
|> xLine(184.3, %)`
|
|> line([184.3, 0], %)`
|
||||||
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: 49 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: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 73 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 36 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}, %)
|
||||||
|> xLine(${commonPoints.num1}, %)`)
|
|> line([${commonPoints.num1}, 0], %)`)
|
||||||
|
|
||||||
// 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], %)
|
||||||
|> xLine(12.34, %)
|
|> line([12.34, 0], %)
|
||||||
|> 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], %)
|
||||||
|> xLine(12.34, %)
|
|> line([12.34, 0], %)
|
||||||
|> line([-12.34, 12.34], %)
|
|> line([-12.34, 12.34], %)
|
||||||
|> xLine(-12.34, %)
|
|> lineTo([0, -12.34], %)
|
||||||
|
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
@ -8,21 +8,6 @@ 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'
|
||||||
@ -272,141 +257,55 @@ export const circleMove = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function rollingRound(n: number, digitsAfterDecimal: number) {
|
export const getMovementUtils = (opts: any) => {
|
||||||
const s = String(n).split('.')
|
// The way we truncate is kinda odd apparently, so we need this function
|
||||||
|
// "[k]itty[c]ad round"
|
||||||
|
const kcRound = (n: number) => Math.trunc(n * 100) / 100
|
||||||
|
|
||||||
// There are no decimals, just return the number.
|
// To translate between screen and engine ("[U]nit") coordinates
|
||||||
if (s.length === 1) return n
|
// NOTE: these pretty much can't be perfect because of screen scaling.
|
||||||
|
// 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
|
||||||
|
]
|
||||||
|
|
||||||
// Find the closest 9. We don't care about anything beyond that.
|
// Turn the array into a string with specific formatting
|
||||||
const nineIndex = s[1].indexOf('9')
|
const fromUToString = (xy: number[]) => `[${xy[0]}, ${xy[1]}]`
|
||||||
|
|
||||||
const fractStr = nineIndex > 0 ? s[1].slice(0, nineIndex + 1) : s[1]
|
// Combine because used often
|
||||||
|
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(x, y, { delay: 100 })
|
opts.page.mouse.click(opts.center.x + x, opts.center.y + 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, y: 0 }
|
last.x = 0
|
||||||
lastScreenSpace = { x: 0, y: 0 }
|
last.y = 0
|
||||||
return {
|
return
|
||||||
nextXY: [0, 0],
|
|
||||||
kcl: `[0, 0]`,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const absX = opts.center.x + x
|
await circleMove(
|
||||||
const absY = opts.center.y + y
|
opts.page,
|
||||||
|
opts.center.x + last.x + x,
|
||||||
const nextX = last.x + x
|
opts.center.y + last.y + y,
|
||||||
const nextY = last.y + y
|
10,
|
||||||
|
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
|
|
||||||
)
|
)
|
||||||
planeRaycaster.setFromCamera(mouseVector, camera)
|
await click00(last.x + x, last.y + y)
|
||||||
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
|
||||||
|
|
||||||
const relativeScreenSpace = {
|
// Returns the new absolute coordinate if you need it.
|
||||||
x: twoD.x - lastScreenSpace.x,
|
return [last.x, last.y]
|
||||||
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, toU, click00r }
|
return { toSU, click00r }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForAuthAndLsp(page: Page) {
|
async function waitForAuthAndLsp(page: Page) {
|
||||||
@ -457,30 +356,6 @@ 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,12 +43,10 @@ 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 center = await u.getCenterOfModelViewArea()
|
const startXPx = 500
|
||||||
|
|
||||||
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(center.x + 234, 244)
|
await page.mouse.click(834, 244)
|
||||||
await page.keyboard.up('Shift')
|
await page.keyboard.up('Shift')
|
||||||
|
|
||||||
await page
|
await page
|
||||||
|
@ -32,17 +32,10 @@ test.describe('Testing selections', () => {
|
|||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
|
|
||||||
const yAxisClick = () =>
|
const xAxisClick = () =>
|
||||||
test.step('Click on Y axis', async () => {
|
page.mouse.click(700, 253).then(() => page.waitForTimeout(100))
|
||||||
await page.mouse.move(600, 200, { steps: 5 })
|
|
||||||
await page.mouse.click(600, 200)
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
})
|
|
||||||
const xAxisClickAfterExitingSketch = () =>
|
const xAxisClickAfterExitingSketch = () =>
|
||||||
test.step(`Click on X axis after exiting sketch, which shifts it at the moment`, async () => {
|
page.mouse.click(639, 278).then(() => page.waitForTimeout(100))
|
||||||
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 })
|
||||||
@ -87,23 +80,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}, %)
|
||||||
|> xLine(${commonPoints.num1}, %)`)
|
|> line([${commonPoints.num1}, 0], %)`)
|
||||||
|
|
||||||
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}, %)
|
||||||
|> xLine(${commonPoints.num1}, %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> yLine(${commonPoints.num1 + 0.01}, %)`)
|
|> line([0, ${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}, %)
|
||||||
|> xLine(${commonPoints.num1}, %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> yLine(${commonPoints.num1 + 0.01}, %)
|
|> line([0, ${commonPoints.num1 + 0.01}], %)
|
||||||
|> xLine(${commonPoints.num2 * -1}, %)`)
|
|> lineTo([0, ${commonPoints.num3}], %)`)
|
||||||
|
|
||||||
// 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()
|
||||||
@ -128,58 +121,53 @@ 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 absXButton = page.getByRole('button', { name: 'Absolute X' })
|
const absYButton = page.getByRole('button', { name: 'Absolute Y' })
|
||||||
|
await constrainButton.click()
|
||||||
await test.step(`Select a segment and an axis, see that a relevant constraint is enabled`, async () => {
|
await expect(absYButton).toBeDisabled()
|
||||||
await topHorzSegmentClick()
|
await page.waitForTimeout(100)
|
||||||
await page.keyboard.down('Shift')
|
await xAxisClick()
|
||||||
await constrainButton.click()
|
await page.keyboard.up('Shift')
|
||||||
await expect(absXButton).toBeDisabled()
|
await constrainButton.click()
|
||||||
await page.waitForTimeout(100)
|
await absYButton.and(page.locator(':not([disabled])')).waitFor()
|
||||||
await yAxisClick()
|
await expect(absYButton).not.toBeDisabled()
|
||||||
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 test.step(`Same selection but click the axis first`, async () => {
|
await page.keyboard.up('Shift')
|
||||||
await yAxisClick()
|
await constrainButton.click()
|
||||||
await constrainButton.click()
|
await expect(absYButton).not.toBeDisabled()
|
||||||
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 test.step(`Same selection but code selection then axis`, async () => {
|
await page
|
||||||
await page
|
.getByText(` |> lineTo([0, ${commonPoints.num3}], %)`)
|
||||||
.getByText(` |> xLine(${commonPoints.num2 * -1}, %)`)
|
.click()
|
||||||
.click()
|
await page.keyboard.down('Shift')
|
||||||
await page.keyboard.down('Shift')
|
await constrainButton.click()
|
||||||
await constrainButton.click()
|
await expect(absYButton).toBeDisabled()
|
||||||
await expect(absXButton).toBeDisabled()
|
await page.waitForTimeout(100)
|
||||||
await page.waitForTimeout(100)
|
await xAxisClick()
|
||||||
await yAxisClick()
|
await page.keyboard.up('Shift')
|
||||||
await page.keyboard.up('Shift')
|
await constrainButton.click()
|
||||||
await constrainButton.click()
|
await expect(absYButton).not.toBeDisabled()
|
||||||
await expect(absXButton).not.toBeDisabled()
|
|
||||||
})
|
|
||||||
|
|
||||||
// clear selection by clicking on nothing
|
// clear selection by clicking on nothing
|
||||||
await emptySpaceClick()
|
await emptySpaceClick()
|
||||||
@ -194,7 +182,9 @@ test.describe('Testing selections', () => {
|
|||||||
process.platform === 'linux' ? 'Control' : 'Meta'
|
process.platform === 'linux' ? 'Control' : 'Meta'
|
||||||
)
|
)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await page.getByText(` |> xLine(${commonPoints.num2 * -1}, %)`).click()
|
await page
|
||||||
|
.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)
|
||||||
@ -938,7 +928,6 @@ 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.fixme(
|
test(
|
||||||
`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,6 +318,7 @@ 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 () => {
|
||||||
@ -743,19 +744,18 @@ 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 = await u.getCenterOfModelViewArea()
|
const sketchOriginLocation = { x: 600, y: 250 }
|
||||||
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 page.mouse.click(sketchOriginLocation.x, sketchOriginLocation.y)
|
await u.waitForAuthSkipAppStart()
|
||||||
|
await page.mouse.click(700, 200)
|
||||||
await expect(editSketchButton).toBeVisible()
|
await expect(editSketchButton).toBeVisible()
|
||||||
await editSketchButton.click()
|
await editSketchButton.click()
|
||||||
|
|
||||||
@ -766,18 +766,12 @@ 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(() => u.getGreatestPixDiff(line1, darkThemeSegmentColor))
|
.poll(() =>
|
||||||
.toBeLessThanOrEqual(34)
|
u.getGreatestPixDiff(sketchOriginLocation, darkThemeSegmentColor)
|
||||||
|
)
|
||||||
|
.toBeLessThan(15)
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step(`Change theme to light using command palette`, async () => {
|
await test.step(`Change theme to light using command palette`, async () => {
|
||||||
@ -792,8 +786,10 @@ 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(() => u.getGreatestPixDiff(line1, lightThemeSegmentColor))
|
.poll(() =>
|
||||||
.toBeLessThanOrEqual(34)
|
u.getGreatestPixDiff(sketchOriginLocation, lightThemeSegmentColor)
|
||||||
|
)
|
||||||
|
.toBeLessThan(15)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -503,16 +503,14 @@ test('Sketch on face', async ({ page }) => {
|
|||||||
|
|
||||||
let previousCodeContent = await page.locator('.cm-content').innerText()
|
let previousCodeContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
const center = await u.getCenterOfModelViewArea()
|
await u.openAndClearDebugPanel()
|
||||||
|
|
||||||
// This basically waits for sketch mode to be ready.
|
|
||||||
await u.doAndWaitForCmd(
|
await u.doAndWaitForCmd(
|
||||||
async () => page.mouse.click(center.x, 180),
|
() => page.mouse.click(625, 165),
|
||||||
'default_camera_get_settings',
|
'default_camera_get_settings',
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
await page.waitForTimeout(150)
|
||||||
await page.waitForTimeout(300)
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
const firstClickPosition = [612, 238]
|
const firstClickPosition = [612, 238]
|
||||||
const secondClickPosition = [661, 242]
|
const secondClickPosition = [661, 242]
|
||||||
|
1
interface.d.ts
vendored
@ -78,7 +78,6 @@ export interface IElectronAPI {
|
|||||||
) => Electron.IpcRenderer
|
) => Electron.IpcRenderer
|
||||||
onUpdateError: (callback: (value: { error: Error }) => void) => Electron
|
onUpdateError: (callback: (value: { error: Error }) => void) => Electron
|
||||||
appRestart: () => void
|
appRestart: () => void
|
||||||
getArgvParsed: () => any
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "zoo-modeling-app",
|
"name": "zoo-modeling-app",
|
||||||
"version": "0.26.5",
|
"version": "0.26.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"productName": "Zoo Modeling App",
|
"productName": "Zoo Modeling App",
|
||||||
"author": {
|
"author": {
|
||||||
@ -65,8 +65,7 @@
|
|||||||
"vscode-languageserver-protocol": "^3.17.5",
|
"vscode-languageserver-protocol": "^3.17.5",
|
||||||
"vscode-uri": "^3.0.8",
|
"vscode-uri": "^3.0.8",
|
||||||
"web-vitals": "^3.5.2",
|
"web-vitals": "^3.5.2",
|
||||||
"xstate": "^5.17.4",
|
"xstate": "^5.17.4"
|
||||||
"yargs": "^17.7.2"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
|
50
src/App.tsx
@ -1,14 +1,15 @@
|
|||||||
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, useSearchParams } from 'react-router-dom'
|
import { useLoaderData, useNavigate } 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, sceneInfra } from 'lib/singletons'
|
import { codeManager, engineCommandManager } 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'
|
||||||
@ -21,12 +22,6 @@ 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'
|
|
||||||
maybeWriteToDisk()
|
|
||||||
.then(() => {})
|
|
||||||
.catch(() => {})
|
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const { project, file } = useLoaderData() as IndexLoaderData
|
const { project, file } = useLoaderData() as IndexLoaderData
|
||||||
@ -38,13 +33,6 @@ 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(() => {
|
||||||
@ -65,10 +53,6 @@ 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()
|
||||||
})
|
})
|
||||||
@ -96,26 +80,14 @@ export function App() {
|
|||||||
enableMenu={true}
|
enableMenu={true}
|
||||||
/>
|
/>
|
||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
<ModelingSidebar paneOpacity={paneOpacity} ref={modelingSidebarRef} />
|
<ModelingSidebar paneOpacity={paneOpacity} />
|
||||||
<EngineStreamContext.Provider
|
<Stream />
|
||||||
options={{
|
{/* <CamToggle /> */}
|
||||||
input: {
|
<LowerRightControls coreDumpManager={coreDumpManager}>
|
||||||
videoRef,
|
<UnitsMenu />
|
||||||
canvasRef,
|
<Gizmo />
|
||||||
mediaStream: null,
|
<CameraProjectionToggle />
|
||||||
authToken: auth?.context?.token ?? null,
|
</LowerRightControls>
|
||||||
pool,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EngineStream />
|
|
||||||
{/* <CamToggle /> */}
|
|
||||||
<LowerRightControls coreDumpManager={coreDumpManager}>
|
|
||||||
<UnitsMenu />
|
|
||||||
<Gizmo />
|
|
||||||
<CameraProjectionToggle />
|
|
||||||
</LowerRightControls>
|
|
||||||
</EngineStreamContext.Provider>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
} from 'react-router-dom'
|
} from 'react-router-dom'
|
||||||
import { ErrorPage } from './components/ErrorPage'
|
import { ErrorPage } from './components/ErrorPage'
|
||||||
import { Settings } from './routes/Settings'
|
import { Settings } from './routes/Settings'
|
||||||
import { Telemetry } from './routes/Telemetry'
|
|
||||||
import Onboarding, { onboardingRoutes } from './routes/Onboarding'
|
import Onboarding, { onboardingRoutes } from './routes/Onboarding'
|
||||||
import SignIn from './routes/SignIn'
|
import SignIn from './routes/SignIn'
|
||||||
import { Auth } from './Auth'
|
import { Auth } from './Auth'
|
||||||
@ -29,7 +28,6 @@ import {
|
|||||||
homeLoader,
|
homeLoader,
|
||||||
onboardingRedirectLoader,
|
onboardingRedirectLoader,
|
||||||
settingsLoader,
|
settingsLoader,
|
||||||
telemetryLoader,
|
|
||||||
} from 'lib/routeLoaders'
|
} from 'lib/routeLoaders'
|
||||||
import { CommandBarProvider } from 'components/CommandBar/CommandBarProvider'
|
import { CommandBarProvider } from 'components/CommandBar/CommandBarProvider'
|
||||||
import SettingsAuthProvider from 'components/SettingsAuthProvider'
|
import SettingsAuthProvider from 'components/SettingsAuthProvider'
|
||||||
@ -45,7 +43,6 @@ import { coreDump } from 'lang/wasm'
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { AppStateProvider } from 'AppState'
|
import { AppStateProvider } from 'AppState'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
import { RouteProvider } from 'components/RouteProvider'
|
|
||||||
import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
|
import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
|
||||||
|
|
||||||
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
||||||
@ -59,21 +56,19 @@ const router = createRouter([
|
|||||||
* inefficient re-renders, use the react profiler to see. */
|
* inefficient re-renders, use the react profiler to see. */
|
||||||
element: (
|
element: (
|
||||||
<CommandBarProvider>
|
<CommandBarProvider>
|
||||||
<RouteProvider>
|
<SettingsAuthProvider>
|
||||||
<SettingsAuthProvider>
|
<LspProvider>
|
||||||
<LspProvider>
|
<ProjectsContextProvider>
|
||||||
<ProjectsContextProvider>
|
<KclContextProvider>
|
||||||
<KclContextProvider>
|
<AppStateProvider>
|
||||||
<AppStateProvider>
|
<MachineManagerProvider>
|
||||||
<MachineManagerProvider>
|
<Outlet />
|
||||||
<Outlet />
|
</MachineManagerProvider>
|
||||||
</MachineManagerProvider>
|
</AppStateProvider>
|
||||||
</AppStateProvider>
|
</KclContextProvider>
|
||||||
</KclContextProvider>
|
</ProjectsContextProvider>
|
||||||
</ProjectsContextProvider>
|
</LspProvider>
|
||||||
</LspProvider>
|
</SettingsAuthProvider>
|
||||||
</SettingsAuthProvider>
|
|
||||||
</RouteProvider>
|
|
||||||
</CommandBarProvider>
|
</CommandBarProvider>
|
||||||
),
|
),
|
||||||
errorElement: <ErrorPage />,
|
errorElement: <ErrorPage />,
|
||||||
@ -129,16 +124,6 @@ const router = createRouter([
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: PATHS.FILE + 'TELEMETRY',
|
|
||||||
loader: telemetryLoader,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: makeUrlPathRelative(PATHS.TELEMETRY),
|
|
||||||
element: <Telemetry />,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -164,11 +149,6 @@ const router = createRouter([
|
|||||||
loader: settingsLoader,
|
loader: settingsLoader,
|
||||||
element: <Settings />,
|
element: <Settings />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: makeUrlPathRelative(PATHS.TELEMETRY),
|
|
||||||
loader: telemetryLoader,
|
|
||||||
element: <Telemetry />,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -22,7 +22,6 @@ 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 = '',
|
||||||
@ -49,7 +48,7 @@ export function Toolbar({
|
|||||||
}, [engineCommandManager.artifactGraph, context.selectionRanges])
|
}, [engineCommandManager.artifactGraph, context.selectionRanges])
|
||||||
|
|
||||||
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
||||||
const { overallState, immediateState } = useNetworkContext()
|
const { overallState } = useNetworkContext()
|
||||||
const { isExecuting } = useKclContext()
|
const { isExecuting } = useKclContext()
|
||||||
const { isStreamReady } = useAppState()
|
const { isStreamReady } = useAppState()
|
||||||
|
|
||||||
@ -57,7 +56,6 @@ 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,5 +1,3 @@
|
|||||||
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,
|
||||||
@ -89,9 +87,6 @@ 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
|
||||||
@ -100,13 +95,6 @@ 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
|
||||||
@ -473,7 +461,6 @@ 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: {
|
||||||
@ -922,123 +909,18 @@ 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: 'default_camera_look_at',
|
type: 'zoom_to_fit',
|
||||||
...convertThreeCamValuesToEngineCam({
|
object_ids: [], // leave empty to zoom to all objects
|
||||||
isPerspective: true,
|
padding: 0.2, // padding around the objects
|
||||||
position: this.camera.position,
|
animated: false, // don't animate the zoom for now
|
||||||
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,11 +1,4 @@
|
|||||||
import {
|
import { useRef, useEffect, useState, useMemo, Fragment } from 'react'
|
||||||
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'
|
||||||
@ -209,20 +202,12 @@ 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(
|
||||||
@ -249,13 +234,6 @@ 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
|
||||||
@ -266,7 +244,6 @@ 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>
|
||||||
@ -275,7 +252,6 @@ 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(${
|
||||||
@ -317,7 +293,6 @@ 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'
|
||||||
@ -459,17 +434,15 @@ 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 style={style} className="relative">
|
<Popover className="relative">
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
@ -664,16 +637,10 @@ 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
|
||||||
await kclManager.updateAst(modifiedAst, true)
|
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)
|
||||||
}
|
}
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
// 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,7 +2,10 @@ 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'
|
||||||
@ -25,9 +28,15 @@ export function createGridHelper({
|
|||||||
gridHelper.rotation.x = Math.PI / 2
|
gridHelper.rotation.x = Math.PI / 2
|
||||||
return gridHelper
|
return gridHelper
|
||||||
}
|
}
|
||||||
|
const fudgeFactor = 72.66985970437086
|
||||||
|
|
||||||
// Re-export scale.ts
|
export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) =>
|
||||||
export * from './scale'
|
(0.55 * fudgeFactor) / cam.zoom / window.innerHeight
|
||||||
|
|
||||||
|
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)
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
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,7 +17,6 @@ import {
|
|||||||
Vector3,
|
Vector3,
|
||||||
} from 'three'
|
} from 'three'
|
||||||
import {
|
import {
|
||||||
ANGLE_SNAP_THRESHOLD_DEGREES,
|
|
||||||
ARROWHEAD,
|
ARROWHEAD,
|
||||||
AXIS_GROUP,
|
AXIS_GROUP,
|
||||||
DRAFT_POINT,
|
DRAFT_POINT,
|
||||||
@ -96,7 +95,6 @@ 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'
|
||||||
|
|
||||||
@ -453,7 +451,6 @@ 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)
|
||||||
|
|
||||||
@ -686,7 +683,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],
|
||||||
@ -801,24 +798,11 @@ export class SceneEntities {
|
|||||||
(sceneObject) => sceneObject.object.name === X_AXIS
|
(sceneObject) => sceneObject.object.name === X_AXIS
|
||||||
)
|
)
|
||||||
|
|
||||||
const lastSegment = sketch.paths.slice(-1)[0] || sketch.start
|
const lastSegment = sketch.paths.slice(-1)[0]
|
||||||
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'
|
||||||
|
|
||||||
@ -826,12 +810,6 @@ 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'
|
||||||
@ -857,11 +835,10 @@ 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,
|
||||||
@ -869,8 +846,6 @@ export class SceneEntities {
|
|||||||
segmentName
|
segmentName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
await codeManager.updateEditorWithAstAndWriteToFile(modifiedAst)
|
|
||||||
},
|
},
|
||||||
onMove: (args) => {
|
onMove: (args) => {
|
||||||
this.onDragSegment({
|
this.onDragSegment({
|
||||||
@ -995,51 +970,43 @@ 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') {
|
||||||
return
|
updateRectangleSketch(sketchInit, x, y, tags[0])
|
||||||
|
|
||||||
|
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)
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1199,17 +1166,13 @@ export class SceneEntities {
|
|||||||
if (err(moddedResult)) return
|
if (err(moddedResult)) return
|
||||||
modded = moddedResult.modifiedAst
|
modded = moddedResult.modifiedAst
|
||||||
|
|
||||||
const newCode = recast(modded)
|
let _recastAst = parse(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)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -1245,7 +1208,6 @@ export class SceneEntities {
|
|||||||
forward,
|
forward,
|
||||||
position,
|
position,
|
||||||
})
|
})
|
||||||
await codeManager.writeToFile()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDrag: async ({
|
onDrag: async ({
|
||||||
|
@ -50,8 +50,6 @@ 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 */
|
||||||
@ -291,14 +289,14 @@ export class SceneInfra {
|
|||||||
engineCommandManager
|
engineCommandManager
|
||||||
)
|
)
|
||||||
this.camControls.subscribeToCamChange(() => this.onCameraChange())
|
this.camControls.subscribeToCamChange(() => this.onCameraChange())
|
||||||
this.camControls.camera.layers.enable(constants.SKETCH_LAYER)
|
this.camControls.camera.layers.enable(SKETCH_LAYER)
|
||||||
if (constants.DEBUG_SHOW_INTERSECTION_PLANE)
|
if (DEBUG_SHOW_INTERSECTION_PLANE)
|
||||||
this.camControls.camera.layers.enable(constants.INTERSECTION_PLANE_LAYER)
|
this.camControls.camera.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||||
|
|
||||||
// RAYCASTERS
|
// RAYCASTERS
|
||||||
this.raycaster.layers.enable(constants.SKETCH_LAYER)
|
this.raycaster.layers.enable(SKETCH_LAYER)
|
||||||
this.raycaster.layers.disable(0)
|
this.raycaster.layers.disable(0)
|
||||||
this.planeRaycaster.layers.enable(constants.INTERSECTION_PLANE_LAYER)
|
this.planeRaycaster.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||||
|
|
||||||
// GRID
|
// GRID
|
||||||
const size = 100
|
const size = 100
|
||||||
@ -333,7 +331,7 @@ export class SceneInfra {
|
|||||||
this.camControls.target
|
this.camControls.target
|
||||||
)
|
)
|
||||||
const axisGroup = this.scene
|
const axisGroup = this.scene
|
||||||
.getObjectByName(constants.AXIS_GROUP)
|
.getObjectByName(AXIS_GROUP)
|
||||||
?.getObjectByName('gridHelper')
|
?.getObjectByName('gridHelper')
|
||||||
axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale)
|
axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale)
|
||||||
}
|
}
|
||||||
@ -344,6 +342,7 @@ 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)
|
||||||
@ -351,7 +350,6 @@ 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 = () => {
|
||||||
@ -655,11 +653,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 === constants.AXIS_GROUP
|
({ userData }) => userData?.type === AXIS_GROUP
|
||||||
)
|
)
|
||||||
const axisMap: { [key: string]: Axis } = {
|
const axisMap: { [key: string]: Axis } = {
|
||||||
[constants.X_AXIS]: 'x-axis',
|
[X_AXIS]: 'x-axis',
|
||||||
[constants.Y_AXIS]: 'y-axis',
|
[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: true,
|
isHandlesVisible,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
})
|
})
|
||||||
@ -476,7 +476,7 @@ class TangentialArcToSegment implements SegmentUtils {
|
|||||||
sceneInfra.updateOverlayDetails({
|
sceneInfra.updateOverlayDetails({
|
||||||
arrowGroup,
|
arrowGroup,
|
||||||
group,
|
group,
|
||||||
isHandlesVisible: true,
|
isHandlesVisible,
|
||||||
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)
|
group.add(arcMesh, arrowGroup, circleCenterGroup, radiusIndicatorGroup)
|
||||||
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: true,
|
isHandlesVisible,
|
||||||
from: from,
|
from: from,
|
||||||
to: [center[0], center[1]],
|
to: [center[0], center[1]],
|
||||||
angle: Math.PI / 4,
|
angle: Math.PI / 4,
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
import yargs from 'yargs'
|
|
||||||
import { hideBin } from 'yargs/helpers'
|
|
||||||
|
|
||||||
const argv = yargs(hideBin(process.argv))
|
|
||||||
.option('telemetry', {
|
|
||||||
alias: 't',
|
|
||||||
type: 'boolean',
|
|
||||||
description: 'Writes startup telemetry to file on disk.',
|
|
||||||
})
|
|
||||||
.parse()
|
|
||||||
|
|
||||||
export default argv
|
|
@ -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: 'String',
|
type: 'UserVal',
|
||||||
value,
|
value,
|
||||||
__meta: [],
|
__meta: [],
|
||||||
})
|
})
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
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'
|
||||||
@ -16,7 +14,6 @@ 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
|
||||||
@ -28,14 +25,6 @@ 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,20 +2,13 @@ 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"
|
||||||
|
@ -1161,29 +1161,6 @@ const CustomIconMap = {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
stopwatch: (
|
|
||||||
<svg
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M7.95705 5.99046C7.05643 6.44935 6.33654 7.19809 5.91336 8.11602C5.49019 9.03396 5.38838 10.0676 5.62434 11.0505C5.8603 12.0334 6.42029 12.9081 7.21408 13.5339C8.00787 14.1597 8.98922 14.5 10 14.5C11.0108 14.5 11.9921 14.1597 12.7859 13.5339C13.5797 12.9082 14.1397 12.0334 14.3757 11.0505C14.6116 10.0676 14.5098 9.03396 14.0866 8.11603C13.6635 7.19809 12.9436 6.44935 12.043 5.99046L12.497 5.09946C13.5977 5.66032 14.4776 6.57544 14.9948 7.69737C15.512 8.81929 15.6364 10.0827 15.348 11.2839C15.0596 12.4852 14.3752 13.5544 13.405 14.3192C12.4348 15.0841 11.2354 15.5 10 15.5C8.7646 15.5 7.56517 15.0841 6.59499 14.3192C5.6248 13.5544 4.94037 12.4852 4.65197 11.2839C4.36357 10.0827 4.488 8.81929 5.00522 7.69736C5.52243 6.57544 6.40231 5.66032 7.50306 5.09946L7.95705 5.99046Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
<path d="M10 5.5V4M10 4H8M10 4H12" stroke="currentColor" />
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M12.8536 7.85356L10.3536 10.3536C10.1583 10.5488 9.84171 10.5488 9.64645 10.3536C9.45118 10.1583 9.45118 9.84172 9.64645 9.64645L12.1464 7.14645L12.8536 7.85356Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type CustomIconName = keyof typeof CustomIconMap
|
export type CustomIconName = keyof typeof CustomIconMap
|
||||||
|
@ -1,293 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -29,7 +29,6 @@ import {
|
|||||||
KclSamplesManifestItem,
|
KclSamplesManifestItem,
|
||||||
} from 'lib/getKclSamplesManifest'
|
} from 'lib/getKclSamplesManifest'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { markOnce } from 'lib/performance'
|
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -55,7 +54,6 @@ export const FileMachineProvider = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
markOnce('code/didLoadFile')
|
|
||||||
async function fetchKclSamples() {
|
async function fetchKclSamples() {
|
||||||
setKclSamples(await getKclSamplesManifest())
|
setKclSamples(await getKclSamplesManifest())
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,6 @@ 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})`
|
||||||
@ -197,7 +196,8 @@ const FileTreeItem = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCurrentFile && eventType === 'change') {
|
// Don't try to read a file that was removed.
|
||||||
|
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().catch(reportRejection)
|
void handleClick()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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().catch(reportRejection)
|
void handleClick()
|
||||||
}}
|
}}
|
||||||
onKeyUp={handleKeyUp}
|
onKeyUp={handleKeyUp}
|
||||||
>
|
>
|
||||||
|
@ -96,23 +96,6 @@ export function LowerRightControls({
|
|||||||
Report a bug
|
Report a bug
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</a>
|
</a>
|
||||||
<Link
|
|
||||||
to={
|
|
||||||
location.pathname.includes(PATHS.FILE)
|
|
||||||
? filePath + PATHS.TELEMETRY + '?tab=project'
|
|
||||||
: PATHS.HOME + PATHS.TELEMETRY
|
|
||||||
}
|
|
||||||
data-testid="telemetry-link"
|
|
||||||
>
|
|
||||||
<CustomIcon
|
|
||||||
name="stopwatch"
|
|
||||||
className={`w-5 h-5 ${linkOverrideClassName}`}
|
|
||||||
/>
|
|
||||||
<span className="sr-only">Telemetry</span>
|
|
||||||
<Tooltip position="top" contentClassName="text-xs">
|
|
||||||
Telemetry
|
|
||||||
</Tooltip>
|
|
||||||
</Link>
|
|
||||||
<Link
|
<Link
|
||||||
to={
|
to={
|
||||||
location.pathname.includes(PATHS.FILE)
|
location.pathname.includes(PATHS.FILE)
|
||||||
|
@ -1,47 +1,40 @@
|
|||||||
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 = <div className={className}></div>
|
let icon = <Spinner className={className} />
|
||||||
let dataTestId = 'model-state-indicator'
|
let dataTestId = 'model-state-indicator'
|
||||||
|
|
||||||
if (engineStreamState.value === EngineStreamState.Paused) {
|
if (lastCommandType === 'receive-reliable') {
|
||||||
className += 'text-secondary'
|
className +=
|
||||||
icon = <CustomIcon data-testid={dataTestId + '-paused'} name="parallel" />
|
'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'
|
||||||
} else if (engineStreamState.value === EngineStreamState.Resuming) {
|
icon = (
|
||||||
className += 'text-secondary'
|
<CustomIcon
|
||||||
icon = <CustomIcon data-testid={dataTestId + '-resuming'} name="parallel" />
|
data-testid={dataTestId + '-receive-reliable'}
|
||||||
} else if (isDone) {
|
name="checkmark"
|
||||||
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,6 +20,7 @@ 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,
|
||||||
@ -111,8 +112,13 @@ export const ModelingMachineProvider = ({
|
|||||||
auth,
|
auth,
|
||||||
settings: {
|
settings: {
|
||||||
context: {
|
context: {
|
||||||
app: { theme },
|
app: { theme, enableSSAO },
|
||||||
modeling: { defaultUnit, highlightEdges, cameraProjection },
|
modeling: {
|
||||||
|
defaultUnit,
|
||||||
|
cameraProjection,
|
||||||
|
highlightEdges,
|
||||||
|
showScaleGrid,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} = useSettingsAuthContext()
|
} = useSettingsAuthContext()
|
||||||
@ -123,6 +129,9 @@ 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
|
||||||
@ -295,7 +304,6 @@ 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({
|
||||||
@ -649,9 +657,6 @@ 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,
|
||||||
@ -672,9 +677,6 @@ export const ModelingMachineProvider = ({
|
|||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
input.planeId
|
input.planeId
|
||||||
)
|
)
|
||||||
await sceneInfra.camControls.centerModelRelativeToPanes({
|
|
||||||
resetLastPaneWidth: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sketchPathToNode: pathToNode,
|
sketchPathToNode: pathToNode,
|
||||||
@ -697,9 +699,6 @@ 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,
|
||||||
@ -733,11 +732,6 @@ 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,
|
||||||
@ -774,11 +768,6 @@ 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,
|
||||||
@ -824,11 +813,6 @@ 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,
|
||||||
@ -862,11 +846,6 @@ 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,
|
||||||
@ -902,11 +881,6 @@ 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,
|
||||||
@ -943,11 +917,6 @@ 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,
|
||||||
@ -984,11 +953,6 @@ 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,
|
||||||
@ -1035,11 +999,6 @@ 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,
|
||||||
@ -1068,6 +1027,21 @@ 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' })
|
||||||
|
@ -4,7 +4,7 @@ import { Themes, getSystemTheme } from 'lib/theme'
|
|||||||
import { useMemo, useRef } from 'react'
|
import { useMemo, useRef } from 'react'
|
||||||
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'
|
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'
|
||||||
import { lineHighlightField } from 'editor/highlightextension'
|
import { lineHighlightField } from 'editor/highlightextension'
|
||||||
import { onMouseDragMakeANewNumber, onMouseDragRegex } from 'lib/utils'
|
import { roundOff } from 'lib/utils'
|
||||||
import {
|
import {
|
||||||
lineNumbers,
|
lineNumbers,
|
||||||
rectangularSelection,
|
rectangularSelection,
|
||||||
@ -129,9 +129,7 @@ export const KclEditorPane = () => {
|
|||||||
closeBrackets(),
|
closeBrackets(),
|
||||||
highlightActiveLine(),
|
highlightActiveLine(),
|
||||||
highlightSelectionMatches(),
|
highlightSelectionMatches(),
|
||||||
syntaxHighlighting(defaultHighlightStyle, {
|
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||||
fallback: true,
|
|
||||||
}),
|
|
||||||
rectangularSelection(),
|
rectangularSelection(),
|
||||||
dropCursor(),
|
dropCursor(),
|
||||||
interact({
|
interact({
|
||||||
@ -139,12 +137,29 @@ export const KclEditorPane = () => {
|
|||||||
// a rule for a number dragger
|
// a rule for a number dragger
|
||||||
{
|
{
|
||||||
// the regexp matching the value
|
// the regexp matching the value
|
||||||
regexp: onMouseDragRegex,
|
regexp: /-?\b\d+\.?\d*\b/g,
|
||||||
// set cursor to "ew-resize" on hover
|
// set cursor to "ew-resize" on hover
|
||||||
cursor: 'ew-resize',
|
cursor: 'ew-resize',
|
||||||
// change number value based on mouse X movement on drag
|
// change number value based on mouse X movement on drag
|
||||||
onDrag: (text, setText, e) => {
|
onDrag: (text, setText, e) => {
|
||||||
onMouseDragMakeANewNumber(text, setText, e)
|
const multiplier =
|
||||||
|
e.shiftKey && e.metaKey
|
||||||
|
? 0.01
|
||||||
|
: e.metaKey
|
||||||
|
? 0.1
|
||||||
|
: e.shiftKey
|
||||||
|
? 10
|
||||||
|
: 1
|
||||||
|
|
||||||
|
const delta = e.movementX * multiplier
|
||||||
|
|
||||||
|
const newVal = roundOff(
|
||||||
|
Number(text) + delta,
|
||||||
|
multiplier === 0.01 ? 2 : multiplier === 0.1 ? 1 : 0
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isNaN(newVal)) return
|
||||||
|
setText(newVal.toString())
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -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 === 'Sketch' ||
|
(val.type === 'UserVal' && val.value.type === 'Sketch') ||
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
val.type !== 'Function'
|
(val.type !== 'Function' && val.type !== 'UserVal')
|
||||||
) {
|
) {
|
||||||
const sg = sketchFromKclValue(val, key)
|
const sg = sketchFromKclValue(val, key)
|
||||||
if (val.type === 'Solid') {
|
if (val.type === 'Solid') {
|
||||||
@ -110,6 +110,8 @@ 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,11 +6,6 @@ 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'
|
||||||
@ -24,12 +19,9 @@ 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 {
|
||||||
@ -41,34 +33,19 @@ function getPlatformString(): 'web' | 'desktop' {
|
|||||||
return isDesktop() ? 'desktop' : 'web'
|
return isDesktop() ? 'desktop' : 'web'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModelingSidebar = forwardRef<
|
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||||
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, state, context } = useModelingContext()
|
const { send, 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(
|
||||||
() => ({
|
() => ({
|
||||||
@ -182,37 +159,8 @@ export const ModelingSidebar = forwardRef<
|
|||||||
[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',
|
||||||
@ -244,7 +192,6 @@ export const ModelingSidebar = forwardRef<
|
|||||||
>
|
>
|
||||||
<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' : '')
|
||||||
@ -289,8 +236,6 @@ export const ModelingSidebar = forwardRef<
|
|||||||
</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`)
|
||||||
@ -320,7 +265,7 @@ export const ModelingSidebar = forwardRef<
|
|||||||
</div>
|
</div>
|
||||||
</Resizable>
|
</Resizable>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
interface ModelingPaneButtonProps
|
interface ModelingPaneButtonProps
|
||||||
extends React.HTMLAttributes<HTMLButtonElement> {
|
extends React.HTMLAttributes<HTMLButtonElement> {
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
import { useEffect, useState, createContext, ReactNode } from 'react'
|
|
||||||
import { useNavigation, useLocation } from 'react-router-dom'
|
|
||||||
import { PATHS } from 'lib/paths'
|
|
||||||
import { markOnce } from 'lib/performance'
|
|
||||||
|
|
||||||
export const RouteProviderContext = createContext({})
|
|
||||||
|
|
||||||
export function RouteProvider({ children }: { children: ReactNode }) {
|
|
||||||
const [first, setFirstState] = useState(true)
|
|
||||||
const navigation = useNavigation()
|
|
||||||
const location = useLocation()
|
|
||||||
useEffect(() => {
|
|
||||||
// On initialization, the react-router-dom does not send a 'loading' state event.
|
|
||||||
// it sends an idle event first.
|
|
||||||
const pathname = first ? location.pathname : navigation.location?.pathname
|
|
||||||
const isHome = pathname === PATHS.HOME
|
|
||||||
const isFile =
|
|
||||||
pathname?.includes(PATHS.FILE) &&
|
|
||||||
pathname?.substring(pathname?.length - 4) === '.kcl'
|
|
||||||
if (isHome) {
|
|
||||||
markOnce('code/willLoadHome')
|
|
||||||
} else if (isFile) {
|
|
||||||
markOnce('code/willLoadFile')
|
|
||||||
}
|
|
||||||
setFirstState(false)
|
|
||||||
}, [navigation])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RouteProviderContext.Provider value={{}}>
|
|
||||||
{children}
|
|
||||||
</RouteProviderContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,7 +1,7 @@
|
|||||||
import { trap } from 'lib/trap'
|
import { trap } from 'lib/trap'
|
||||||
import { useMachine } from '@xstate/react'
|
import { useMachine } from '@xstate/react'
|
||||||
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
|
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
|
||||||
import { PATHS, BROWSER_PATH } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
|
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
|
||||||
import withBaseUrl from '../lib/withBaseURL'
|
import withBaseUrl from '../lib/withBaseURL'
|
||||||
import React, { createContext, useEffect, useState } from 'react'
|
import React, { createContext, useEffect, useState } from 'react'
|
||||||
@ -42,7 +42,6 @@ import { getAppSettingsFilePath } from 'lib/desktop'
|
|||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||||
import { codeManager } from 'lib/singletons'
|
import { codeManager } from 'lib/singletons'
|
||||||
import { createRouteCommands } from 'lib/commandBarConfigs/routeCommandConfig'
|
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -289,44 +288,6 @@ export const SettingsAuthProviderBase = ({
|
|||||||
settingsWithCommandConfigs,
|
settingsWithCommandConfigs,
|
||||||
])
|
])
|
||||||
|
|
||||||
// Due to the route provider, i've moved this to the SettingsAuthProvider instead of CommandBarProvider
|
|
||||||
// This will register the commands to route to Telemetry, Home, and Settings.
|
|
||||||
useEffect(() => {
|
|
||||||
const filePath =
|
|
||||||
PATHS.FILE +
|
|
||||||
'/' +
|
|
||||||
encodeURIComponent(loadedProject?.file?.path || BROWSER_PATH)
|
|
||||||
const { RouteTelemetryCommand, RouteHomeCommand, RouteSettingsCommand } =
|
|
||||||
createRouteCommands(navigate, location, filePath)
|
|
||||||
commandBarSend({
|
|
||||||
type: 'Remove commands',
|
|
||||||
data: {
|
|
||||||
commands: [
|
|
||||||
RouteTelemetryCommand,
|
|
||||||
RouteHomeCommand,
|
|
||||||
RouteSettingsCommand,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (location.pathname === PATHS.HOME) {
|
|
||||||
commandBarSend({
|
|
||||||
type: 'Add commands',
|
|
||||||
data: { commands: [RouteTelemetryCommand, RouteSettingsCommand] },
|
|
||||||
})
|
|
||||||
} else if (location.pathname.includes(PATHS.FILE)) {
|
|
||||||
commandBarSend({
|
|
||||||
type: 'Add commands',
|
|
||||||
data: {
|
|
||||||
commands: [
|
|
||||||
RouteTelemetryCommand,
|
|
||||||
RouteSettingsCommand,
|
|
||||||
RouteHomeCommand,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [location])
|
|
||||||
|
|
||||||
// Listen for changes to the system theme and update the app theme accordingly
|
// Listen for changes to the system theme and update the app theme accordingly
|
||||||
// This is only done if the theme setting is set to 'system'.
|
// This is only done if the theme setting is set to 'system'.
|
||||||
// It can't be done in XState (in an invoked callback, for example)
|
// It can't be done in XState (in an invoked callback, for example)
|
||||||
|
340
src/components/Stream.tsx
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
@ -1,72 +0,0 @@
|
|||||||
import { getMarks } from 'lib/performance'
|
|
||||||
|
|
||||||
import {
|
|
||||||
printDeltaTotal,
|
|
||||||
printInvocationCount,
|
|
||||||
printMarkDownTable,
|
|
||||||
printRawMarks,
|
|
||||||
} from 'lib/telemetry'
|
|
||||||
|
|
||||||
export function TelemetryExplorer() {
|
|
||||||
const marks = getMarks()
|
|
||||||
const markdownTable = printMarkDownTable(marks)
|
|
||||||
const rawMarks = printRawMarks(marks)
|
|
||||||
const deltaTotalTable = printDeltaTotal(marks)
|
|
||||||
const invocationCount = printInvocationCount(marks)
|
|
||||||
// TODO data-telemetry-type
|
|
||||||
// TODO data-telemetry-name
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1 className="pb-4">Marks</h1>
|
|
||||||
<div className="max-w-xl max-h-64 overflow-auto select-all">
|
|
||||||
{marks.map((mark, index) => {
|
|
||||||
return (
|
|
||||||
<pre className="text-xs" key={index}>
|
|
||||||
<code key={index}>{JSON.stringify(mark, null, 2)}</code>
|
|
||||||
</pre>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<h1 className="pb-4">Startup Performance</h1>
|
|
||||||
<div className="max-w-xl max-h-64 overflow-auto select-all">
|
|
||||||
{markdownTable.map((line, index) => {
|
|
||||||
return (
|
|
||||||
<pre className="text-xs" key={index}>
|
|
||||||
<code key={index}>{line}</code>
|
|
||||||
</pre>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<h1 className="pb-4">Delta and Totals</h1>
|
|
||||||
<div className="max-w-xl max-h-64 overflow-auto select-all">
|
|
||||||
{deltaTotalTable.map((line, index) => {
|
|
||||||
return (
|
|
||||||
<pre className="text-xs" key={index}>
|
|
||||||
<code key={index}>{line}</code>
|
|
||||||
</pre>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<h1 className="pb-4">Raw Marks</h1>
|
|
||||||
<div className="max-w-xl max-h-64 overflow-auto select-all">
|
|
||||||
{rawMarks.map((line, index) => {
|
|
||||||
return (
|
|
||||||
<pre className="text-xs" key={index}>
|
|
||||||
<code key={index}>{line}</code>
|
|
||||||
</pre>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<h1 className="pb-4">Invocation Count</h1>
|
|
||||||
<div className="max-w-xl max-h-64 overflow-auto select-all">
|
|
||||||
{invocationCount.map((line, index) => {
|
|
||||||
return (
|
|
||||||
<pre className="text-xs" key={index}>
|
|
||||||
<code key={index}>{line}</code>
|
|
||||||
</pre>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,5 +1,4 @@
|
|||||||
import { EditorView, ViewUpdate } from '@codemirror/view'
|
import { EditorView, ViewUpdate } from '@codemirror/view'
|
||||||
import { syntaxTree } from '@codemirror/language'
|
|
||||||
import { EditorSelection, Annotation, Transaction } from '@codemirror/state'
|
import { EditorSelection, Annotation, Transaction } from '@codemirror/state'
|
||||||
import { engineCommandManager } from 'lib/singletons'
|
import { engineCommandManager } from 'lib/singletons'
|
||||||
import { modelingMachine, ModelingMachineEvent } from 'machines/modelingMachine'
|
import { modelingMachine, ModelingMachineEvent } from 'machines/modelingMachine'
|
||||||
@ -13,7 +12,6 @@ import {
|
|||||||
setDiagnosticsEffect,
|
setDiagnosticsEffect,
|
||||||
} from '@codemirror/lint'
|
} from '@codemirror/lint'
|
||||||
import { StateFrom } from 'xstate'
|
import { StateFrom } from 'xstate'
|
||||||
import { markOnce } from 'lib/performance'
|
|
||||||
|
|
||||||
const updateOutsideEditorAnnotation = Annotation.define<boolean>()
|
const updateOutsideEditorAnnotation = Annotation.define<boolean>()
|
||||||
export const updateOutsideEditorEvent = updateOutsideEditorAnnotation.of(true)
|
export const updateOutsideEditorEvent = updateOutsideEditorAnnotation.of(true)
|
||||||
@ -24,6 +22,10 @@ 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
|
||||||
@ -57,49 +59,6 @@ export default class EditorManager {
|
|||||||
|
|
||||||
setEditorView(editorView: EditorView) {
|
setEditorView(editorView: EditorView) {
|
||||||
this._editorView = editorView
|
this._editorView = editorView
|
||||||
this.overrideTreeHighlighterUpdateForPerformanceTracking()
|
|
||||||
}
|
|
||||||
|
|
||||||
overrideTreeHighlighterUpdateForPerformanceTracking() {
|
|
||||||
// @ts-ignore
|
|
||||||
this._editorView?.plugins.forEach((e) => {
|
|
||||||
let sawATreeDiff = false
|
|
||||||
|
|
||||||
// we cannot use <>.constructor.name since it will get destroyed
|
|
||||||
// when packaging the application.
|
|
||||||
const isTreeHighlightPlugin =
|
|
||||||
e?.value &&
|
|
||||||
e.value?.hasOwnProperty('tree') &&
|
|
||||||
e.value?.hasOwnProperty('decoratedTo') &&
|
|
||||||
e.value?.hasOwnProperty('decorations')
|
|
||||||
|
|
||||||
if (isTreeHighlightPlugin) {
|
|
||||||
let originalUpdate = e.value.update
|
|
||||||
// @ts-ignore
|
|
||||||
function performanceTrackingUpdate(args) {
|
|
||||||
/**
|
|
||||||
* TreeHighlighter.update will be called multiple times on start up.
|
|
||||||
* We do not want to track the highlight performance of an empty update.
|
|
||||||
* mark the syntax highlight one time when the new tree comes in with the
|
|
||||||
* initial code
|
|
||||||
*/
|
|
||||||
const treeIsDifferent =
|
|
||||||
// @ts-ignore
|
|
||||||
!sawATreeDiff && this.tree !== syntaxTree(args.state)
|
|
||||||
if (treeIsDifferent && !sawATreeDiff) {
|
|
||||||
markOnce('code/willSyntaxHighlight')
|
|
||||||
}
|
|
||||||
// Call the original function
|
|
||||||
// @ts-ignore
|
|
||||||
originalUpdate.apply(this, [args])
|
|
||||||
if (treeIsDifferent && !sawATreeDiff) {
|
|
||||||
markOnce('code/didSyntaxHighlight')
|
|
||||||
sawATreeDiff = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e.value.update = performanceTrackingUpdate
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get editorView(): EditorView | null {
|
get editorView(): EditorView | null {
|
||||||
@ -158,29 +117,20 @@ 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.
|
||||||
diagnostics = this.makeUniqueDiagnostics(diagnostics)
|
for (const diagnostic of 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)],
|
||||||
|
@ -1,237 +0,0 @@
|
|||||||
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, codeManager } from 'lib/singletons'
|
import { editorManager, kclManager } from 'lib/singletons'
|
||||||
import { reportRejection, trap, err } from 'lib/trap'
|
import { reportRejection, trap } 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, recast } from 'lang/wasm'
|
import { PathToNode, SourceRange } from 'lang/wasm'
|
||||||
import { useKclContext } from 'lang/KclProvider'
|
import { useKclContext } from 'lang/KclProvider'
|
||||||
import { toSync } from 'lib/utils'
|
import { toSync } from 'lib/utils'
|
||||||
|
|
||||||
@ -57,11 +57,6 @@ 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)
|
||||||
|
@ -8,10 +8,8 @@ import ModalContainer from 'react-modal-promise'
|
|||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { AppStreamProvider } from 'AppState'
|
import { AppStreamProvider } from 'AppState'
|
||||||
import { ToastUpdate } from 'components/ToastUpdate'
|
import { ToastUpdate } from 'components/ToastUpdate'
|
||||||
import { markOnce } from 'lib/performance'
|
|
||||||
import { AUTO_UPDATER_TOAST_ID } from 'lib/constants'
|
import { AUTO_UPDATER_TOAST_ID } from 'lib/constants'
|
||||||
|
|
||||||
markOnce('code/willAuth')
|
|
||||||
// uncomment for xstate inspector
|
// uncomment for xstate inspector
|
||||||
// import { DEV } from 'env'
|
// import { DEV } from 'env'
|
||||||
// import { inspect } from '@xstate/inspect'
|
// import { inspect } from '@xstate/inspect'
|
||||||
|
@ -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, CommandLogType } from './std/engineConnection'
|
import { EngineCommandManager } 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'
|
||||||
|
|
||||||
@ -21,7 +21,6 @@ import {
|
|||||||
import { getNodeFromPath } from './queryAst'
|
import { getNodeFromPath } from './queryAst'
|
||||||
import { codeManager, editorManager, sceneInfra } from 'lib/singletons'
|
import { codeManager, editorManager, sceneInfra } from 'lib/singletons'
|
||||||
import { Diagnostic } from '@codemirror/lint'
|
import { Diagnostic } from '@codemirror/lint'
|
||||||
import { markOnce } from 'lib/performance'
|
|
||||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
|
|
||||||
interface ExecuteArgs {
|
interface ExecuteArgs {
|
||||||
@ -125,7 +124,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[]) {
|
||||||
@ -258,7 +257,6 @@ export class KclManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ast = args.ast || this.ast
|
const ast = args.ast || this.ast
|
||||||
markOnce('code/startExecuteAst')
|
|
||||||
|
|
||||||
const currentExecutionId = args.executionId || Date.now()
|
const currentExecutionId = args.executionId || Date.now()
|
||||||
this._cancelTokens.set(currentExecutionId, false)
|
this._cancelTokens.set(currentExecutionId, false)
|
||||||
@ -290,9 +288,15 @@ export class KclManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
await sceneInfra.camControls.centerModelRelativeToPanes({
|
await this.engineCommandManager.sendSceneCommand({
|
||||||
zoomToFit: true,
|
type: 'modeling_cmd_req',
|
||||||
zoomObjectId,
|
cmd_id: uuidv4(),
|
||||||
|
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
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -322,12 +326,11 @@ export class KclManager {
|
|||||||
this.ast = { ...ast }
|
this.ast = { ...ast }
|
||||||
this._executeCallback()
|
this._executeCallback()
|
||||||
this.engineCommandManager.addCommandLog({
|
this.engineCommandManager.addCommandLog({
|
||||||
type: CommandLogType.ExecutionDone,
|
type: 'execution-done',
|
||||||
data: null,
|
data: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
this._cancelTokens.delete(currentExecutionId)
|
this._cancelTokens.delete(currentExecutionId)
|
||||||
markOnce('code/endExecuteAst')
|
|
||||||
}
|
}
|
||||||
// NOTE: this always updates the code state and editor.
|
// NOTE: this always updates the code state and editor.
|
||||||
// DO NOT CALL THIS from codemirror ever.
|
// DO NOT CALL THIS from codemirror ever.
|
||||||
@ -351,6 +354,9 @@ 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({
|
||||||
@ -488,6 +494,11 @@ 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,7 +18,8 @@ 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: 'Sketch',
|
type: 'UserVal',
|
||||||
|
__meta: [{ sourceRange: [46, 71, 0] }],
|
||||||
value: {
|
value: {
|
||||||
type: 'Sketch',
|
type: 'Sketch',
|
||||||
on: expect.any(Object),
|
on: expect.any(Object),
|
||||||
|
@ -7,8 +7,6 @@ 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'
|
||||||
|
|
||||||
@ -149,13 +147,6 @@ 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,13 +58,7 @@ 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 sk = mem.get('mySketch')
|
const minusGeo = mem.get('mySketch')?.value?.paths
|
||||||
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',
|
||||||
@ -156,7 +150,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: 'Sketch',
|
type: 'UserVal',
|
||||||
value: {
|
value: {
|
||||||
type: 'Sketch',
|
type: 'Sketch',
|
||||||
on: expect.any(Object),
|
on: expect.any(Object),
|
||||||
@ -221,6 +215,7 @@ 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 () => {
|
||||||
@ -230,7 +225,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: 'Int',
|
type: 'UserVal',
|
||||||
value: 3,
|
value: 3,
|
||||||
__meta: [
|
__meta: [
|
||||||
{
|
{
|
||||||
@ -239,17 +234,8 @@ const newVar = myVar + 1`
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
expect(mem.get('yo')).toEqual({
|
expect(mem.get('yo')).toEqual({
|
||||||
type: 'Array',
|
type: 'UserVal',
|
||||||
value: [
|
value: [1, '2', 3, 9],
|
||||||
{ 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],
|
||||||
@ -267,25 +253,8 @@ 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: 'Object',
|
type: 'UserVal',
|
||||||
value: {
|
value: { aStr: 'str', anum: 2, identifier: 3, binExp: 9 },
|
||||||
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],
|
||||||
@ -299,11 +268,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: 'String',
|
type: 'UserVal',
|
||||||
value: '123',
|
value: '123',
|
||||||
__meta: [
|
__meta: [
|
||||||
{
|
{
|
||||||
sourceRange: [19, 24, 0],
|
sourceRange: [41, 50, 0],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@ -387,26 +356,7 @@ 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([
|
expect(mem.get('myVar')?.value).toEqual([1, -3])
|
||||||
{
|
|
||||||
__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,13 +55,18 @@ 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
|
||||||
files.forEach((file: KclSampleFile) => {
|
it(
|
||||||
it(`should parse ${file.filename} without errors`, async () => {
|
'should run through all the files',
|
||||||
const code = await getKclSampleCodeFromGithub(file.filename)
|
async () => {
|
||||||
const parsed = parse(code)
|
for (let i = 0; i < files.length; i++) {
|
||||||
assert(!(parsed instanceof Error))
|
const file: KclSampleFile = files[i]
|
||||||
}, 1000)
|
const code = await getKclSampleCodeFromGithub(file.filename)
|
||||||
})
|
const parsed = parse(code)
|
||||||
|
assert(!(parsed instanceof Error))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
files.length * 1000
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('when performing enginelessExecutor', () => {
|
describe('when performing enginelessExecutor', () => {
|
||||||
|