Compare commits
57 Commits
pierremtb/
...
nightly-v2
Author | SHA1 | Date | |
---|---|---|---|
96652a0c48 | |||
04e586d07b | |||
fe5f574a77 | |||
e787495ad0 | |||
8bb9be7a5e | |||
00892464e8 | |||
05ed2a3367 | |||
10cc5bce59 | |||
a32f150fc1 | |||
ac60082e67 | |||
d44dc1b21a | |||
813962ea4c | |||
738443a6ab | |||
4b6bbbe2c5 | |||
6ff8addc8b | |||
da05c38b9e | |||
191b9b71fd | |||
05163fdded | |||
7ed26e21c6 | |||
c668d40efc | |||
f38c6b90b7 | |||
7bc8bae0ec | |||
3804aca27e | |||
b127680f2f | |||
b7de8e60cf | |||
058fccb5e1 | |||
00e97257ae | |||
aeb656d176 | |||
ac49ebd6e0 | |||
b40f03ad25 | |||
a8ad86e645 | |||
87f50cd5e9 | |||
0400e6228e | |||
26f150fd6c | |||
3049f405f5 | |||
53d40301dc | |||
671c01e36f | |||
e80151979b | |||
668e2afb99 | |||
548c664db0 | |||
d3a3f4410c | |||
22eb343171 | |||
f2cfa4d5cf | |||
3f1f40eeba | |||
ff2d161606 | |||
210c78029d | |||
e27840219b | |||
c943a3f192 | |||
6aa588f09f | |||
59a6333aad | |||
403f1507ae | |||
eac7b83504 | |||
667500d1b9 | |||
b15aac9f48 | |||
54153aa646 | |||
943cf21d34 | |||
5a6728c45a |
3
.github/workflows/build-apps.yml
vendored
@ -165,7 +165,6 @@ jobs:
|
|||||||
- name: Build the app (release)
|
- name: Build the app (release)
|
||||||
if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }}
|
if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }}
|
||||||
env:
|
env:
|
||||||
PUBLISH_FOR_PULL_REQUEST: true
|
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
@ -173,7 +172,6 @@ jobs:
|
|||||||
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
|
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||||
CSC_FOR_PULL_REQUEST: true
|
|
||||||
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
|
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
|
||||||
run: yarn electron-builder --config --publish always
|
run: yarn electron-builder --config --publish always
|
||||||
|
|
||||||
@ -229,7 +227,6 @@ jobs:
|
|||||||
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
|
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||||
CSC_FOR_PULL_REQUEST: true
|
|
||||||
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
|
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
|
||||||
run: yarn electron-builder --config --publish always
|
run: yarn electron-builder --config --publish always
|
||||||
|
|
||||||
|
22
.github/workflows/cargo-test.yml
vendored
@ -2,28 +2,8 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
|
||||||
- 'src/wasm-lib/**.rs'
|
|
||||||
- 'src/wasm-lib/**.hbs'
|
|
||||||
- 'src/wasm-lib/**.gen'
|
|
||||||
- 'src/wasm-lib/**.snap'
|
|
||||||
- '**/Cargo.toml'
|
|
||||||
- '**/Cargo.lock'
|
|
||||||
- '**/rust-toolchain.toml'
|
|
||||||
- 'src/wasm-lib/**.kcl'
|
|
||||||
- .github/workflows/cargo-test.yml
|
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
|
||||||
- 'src/wasm-lib/**.rs'
|
|
||||||
- 'src/wasm-lib/**.hbs'
|
|
||||||
- 'src/wasm-lib/**.gen'
|
|
||||||
- 'src/wasm-lib/**.snap'
|
|
||||||
- '**/Cargo.toml'
|
|
||||||
- '**/Cargo.lock'
|
|
||||||
- '**/rust-toolchain.toml'
|
|
||||||
- 'src/wasm-lib/**.kcl'
|
|
||||||
- .github/workflows/cargo-test.yml
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
permissions: read-all
|
permissions: read-all
|
||||||
concurrency:
|
concurrency:
|
||||||
@ -71,7 +51,7 @@ jobs:
|
|||||||
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
|
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
|
||||||
RUST_MIN_STACK: 10485760000
|
RUST_MIN_STACK: 10485760000
|
||||||
- name: Upload to codecov.io
|
- name: Upload to codecov.io
|
||||||
uses: codecov/codecov-action@v4
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
token: ${{secrets.CODECOV_TOKEN}}
|
token: ${{secrets.CODECOV_TOKEN}}
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
|
@ -22,3 +22,5 @@ once fixed in engine will just start working here with no language changes.
|
|||||||
|
|
||||||
- **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple
|
- **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple
|
||||||
chamfer cases work currently.
|
chamfer cases work currently.
|
||||||
|
|
||||||
|
- **Appearance**: Changing the appearance on a loft does not work.
|
||||||
|
239
docs/kcl/appearance.md
Normal file
49
docs/kcl/atan2.md
Normal file
@ -19,6 +19,7 @@ layout: manual
|
|||||||
* [`angledLineThatIntersects`](kcl/angledLineThatIntersects)
|
* [`angledLineThatIntersects`](kcl/angledLineThatIntersects)
|
||||||
* [`angledLineToX`](kcl/angledLineToX)
|
* [`angledLineToX`](kcl/angledLineToX)
|
||||||
* [`angledLineToY`](kcl/angledLineToY)
|
* [`angledLineToY`](kcl/angledLineToY)
|
||||||
|
* [`appearance`](kcl/appearance)
|
||||||
* [`arc`](kcl/arc)
|
* [`arc`](kcl/arc)
|
||||||
* [`arcTo`](kcl/arcTo)
|
* [`arcTo`](kcl/arcTo)
|
||||||
* [`asin`](kcl/asin)
|
* [`asin`](kcl/asin)
|
||||||
@ -29,6 +30,7 @@ layout: manual
|
|||||||
* [`assertLessThan`](kcl/assertLessThan)
|
* [`assertLessThan`](kcl/assertLessThan)
|
||||||
* [`assertLessThanOrEq`](kcl/assertLessThanOrEq)
|
* [`assertLessThanOrEq`](kcl/assertLessThanOrEq)
|
||||||
* [`atan`](kcl/atan)
|
* [`atan`](kcl/atan)
|
||||||
|
* [`atan2`](kcl/atan2)
|
||||||
* [`bezierCurve`](kcl/bezierCurve)
|
* [`bezierCurve`](kcl/bezierCurve)
|
||||||
* [`ceil`](kcl/ceil)
|
* [`ceil`](kcl/ceil)
|
||||||
* [`chamfer`](kcl/chamfer)
|
* [`chamfer`](kcl/chamfer)
|
||||||
@ -101,6 +103,7 @@ layout: manual
|
|||||||
* [`startProfileAt`](kcl/startProfileAt)
|
* [`startProfileAt`](kcl/startProfileAt)
|
||||||
* [`startSketchAt`](kcl/startSketchAt)
|
* [`startSketchAt`](kcl/startSketchAt)
|
||||||
* [`startSketchOn`](kcl/startSketchOn)
|
* [`startSketchOn`](kcl/startSketchOn)
|
||||||
|
* [`sweep`](kcl/sweep)
|
||||||
* [`tan`](kcl/tan)
|
* [`tan`](kcl/tan)
|
||||||
* [`tangentToEnd`](kcl/tangentToEnd)
|
* [`tangentToEnd`](kcl/tangentToEnd)
|
||||||
* [`tangentialArc`](kcl/tangentialArc)
|
* [`tangentialArc`](kcl/tangentialArc)
|
||||||
|
@ -45,7 +45,7 @@ circles = map([1..3], drawCircle)
|
|||||||
```js
|
```js
|
||||||
r = 10 // radius
|
r = 10 // radius
|
||||||
// Call `map`, using an anonymous function instead of a named one.
|
// Call `map`, using an anonymous function instead of a named one.
|
||||||
circles = map([1..3], (id) {
|
circles = map([1..3], fn(id) {
|
||||||
return startSketchOn("XY")
|
return startSketchOn("XY")
|
||||||
|> circle({ center = [id * 2 * r, 0], radius = r }, %)
|
|> circle({ center = [id * 2 * r, 0], radius = r }, %)
|
||||||
})
|
})
|
||||||
|
@ -43,7 +43,7 @@ fn sum(arr) {
|
|||||||
|
|
||||||
/* The above is basically like this pseudo-code:
|
/* The above is basically like this pseudo-code:
|
||||||
fn sum(arr):
|
fn sum(arr):
|
||||||
let sumSoFar = 0
|
sumSoFar = 0
|
||||||
for i in arr:
|
for i in arr:
|
||||||
sumSoFar = add(sumSoFar, i)
|
sumSoFar = add(sumSoFar, i)
|
||||||
return sumSoFar */
|
return sumSoFar */
|
||||||
@ -61,7 +61,7 @@ assertEqual(sum([1, 2, 3]), 6, 0.00001, "1 + 2 + 3 summed is 6")
|
|||||||
// an anonymous `add` function as its parameter, instead of declaring a
|
// an anonymous `add` function as its parameter, instead of declaring a
|
||||||
// named function outside.
|
// named function outside.
|
||||||
arr = [1, 2, 3]
|
arr = [1, 2, 3]
|
||||||
sum = reduce(arr, 0, (i, result_so_far) {
|
sum = reduce(arr, 0, fn(i, result_so_far) {
|
||||||
return i + result_so_far
|
return i + result_so_far
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -84,7 +84,7 @@ fn decagon(radius) {
|
|||||||
// Use a `reduce` to draw the remaining decagon sides.
|
// Use a `reduce` to draw the remaining decagon sides.
|
||||||
// For each number in the array 1..10, run the given function,
|
// For each number in the array 1..10, run the given function,
|
||||||
// which takes a partially-sketched decagon and adds one more edge to it.
|
// which takes a partially-sketched decagon and adds one more edge to it.
|
||||||
fullDecagon = reduce([1..10], startOfDecagonSketch, (i, partialDecagon) {
|
fullDecagon = reduce([1..10], startOfDecagonSketch, fn(i, partialDecagon) {
|
||||||
// Draw one edge of the decagon.
|
// Draw one edge of the decagon.
|
||||||
x = cos(stepAngle * i) * radius
|
x = cos(stepAngle * i) * radius
|
||||||
y = sin(stepAngle * i) * radius
|
y = sin(stepAngle * i) * radius
|
||||||
@ -96,14 +96,14 @@ fn decagon(radius) {
|
|||||||
|
|
||||||
/* The `decagon` above is basically like this pseudo-code:
|
/* The `decagon` above is basically like this pseudo-code:
|
||||||
fn decagon(radius):
|
fn decagon(radius):
|
||||||
let stepAngle = (1/10) * tau()
|
stepAngle = (1/10) * tau()
|
||||||
let startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)])
|
startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)])
|
||||||
|
|
||||||
// Here's the reduce part.
|
// Here's the reduce part.
|
||||||
let partialDecagon = startOfDecagonSketch
|
partialDecagon = startOfDecagonSketch
|
||||||
for i in [1..10]:
|
for i in [1..10]:
|
||||||
let x = cos(stepAngle * i) * radius
|
x = cos(stepAngle * i) * radius
|
||||||
let y = sin(stepAngle * i) * radius
|
y = sin(stepAngle * i) * radius
|
||||||
partialDecagon = lineTo([x, y], partialDecagon)
|
partialDecagon = lineTo([x, y], partialDecagon)
|
||||||
fullDecagon = partialDecagon // it's now full
|
fullDecagon = partialDecagon // it's now full
|
||||||
return fullDecagon */
|
return fullDecagon */
|
||||||
|
11204
docs/kcl/std.json
55
docs/kcl/sweep.md
Normal file
23
docs/kcl/types/AppearanceData.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
title: "AppearanceData"
|
||||||
|
excerpt: "Data for appearance."
|
||||||
|
layout: manual
|
||||||
|
---
|
||||||
|
|
||||||
|
Data for appearance.
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Type | Description | Required |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| `color` |`string`| Color of the new material, a hex string like "#ff0000". | No |
|
||||||
|
| `metalness` |`number` (**maximum:** 100.0)| Metalness of the new material, a percentage like 95.7. | No |
|
||||||
|
| `roughness` |`number` (**maximum:** 100.0)| Roughness of the new material, a percentage like 95.7. | No |
|
||||||
|
|
||||||
|
|
@ -12,5 +12,10 @@ KCL value for an optional parameter which was not given an argument. (remember,
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Type | Description | Required |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
|
||||||
|
|
||||||
|
|
||||||
|
23
docs/kcl/types/SweepData.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
title: "SweepData"
|
||||||
|
excerpt: "Data for a sweep."
|
||||||
|
layout: manual
|
||||||
|
---
|
||||||
|
|
||||||
|
Data for a sweep.
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Type | Description | Required |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| `path` |[`Sketch`](/docs/kcl/types/Sketch)| The path to sweep along. | No |
|
||||||
|
| `sectional` |`boolean`| If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components. | No |
|
||||||
|
| `tolerance` |`number`| Tolerance for the sweep operation. | No |
|
||||||
|
|
||||||
|
|
@ -57,23 +57,26 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
|
|||||||
const startXPx = 600
|
const startXPx = 600
|
||||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
if (openPanes.includes('code')) {
|
if (openPanes.includes('code')) {
|
||||||
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
await expect(u.codeLocator).toContainText(
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)`)
|
`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
await page.waitForTimeout(500)
|
await page.waitForTimeout(500)
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
await page.waitForTimeout(500)
|
await page.waitForTimeout(500)
|
||||||
|
|
||||||
if (openPanes.includes('code')) {
|
if (openPanes.includes('code')) {
|
||||||
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
await expect(u.codeLocator)
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
|
||||||
|> xLine(${commonPoints.num1}, %)`)
|
|> xLine(${commonPoints.num1}, %)`)
|
||||||
}
|
}
|
||||||
await page.waitForTimeout(500)
|
await page.waitForTimeout(500)
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||||
if (openPanes.includes('code')) {
|
if (openPanes.includes('code')) {
|
||||||
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
await expect(u.codeLocator)
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${
|
||||||
|
commonPoints.startAt
|
||||||
|
}, sketch001)
|
||||||
|> xLine(${commonPoints.num1}, %)
|
|> xLine(${commonPoints.num1}, %)
|
||||||
|> yLine(${commonPoints.num1 + 0.01}, %)`)
|
|> yLine(${commonPoints.num1 + 0.01}, %)`)
|
||||||
} else {
|
} else {
|
||||||
@ -82,8 +85,10 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
|
|||||||
await page.waitForTimeout(200)
|
await page.waitForTimeout(200)
|
||||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||||
if (openPanes.includes('code')) {
|
if (openPanes.includes('code')) {
|
||||||
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
await expect(u.codeLocator)
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${
|
||||||
|
commonPoints.startAt
|
||||||
|
}, sketch001)
|
||||||
|> xLine(${commonPoints.num1}, %)
|
|> xLine(${commonPoints.num1}, %)
|
||||||
|> yLine(${commonPoints.num1 + 0.01}, %)
|
|> yLine(${commonPoints.num1 + 0.01}, %)
|
||||||
|> xLine(${commonPoints.num2 * -1}, %)`)
|
|> xLine(${commonPoints.num2 * -1}, %)`)
|
||||||
@ -140,8 +145,10 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
|
|||||||
|
|
||||||
// Open the code pane.
|
// Open the code pane.
|
||||||
await u.openKclCodePanel()
|
await u.openKclCodePanel()
|
||||||
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
await expect(u.codeLocator)
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${
|
||||||
|
commonPoints.startAt
|
||||||
|
}, sketch001)
|
||||||
|> xLine(${commonPoints.num1}, %, $seg01)
|
|> xLine(${commonPoints.num1}, %, $seg01)
|
||||||
|> yLine(${commonPoints.num1 + 0.01}, %)
|
|> yLine(${commonPoints.num1 + 0.01}, %)
|
||||||
|> xLine(-segLen(seg01), %)`)
|
|> xLine(-segLen(seg01), %)`)
|
||||||
|
@ -44,8 +44,7 @@ test.describe('Can create sketches on all planes and their back sides', () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const code = `sketch001 = startSketchOn('${plane}')
|
const code = `sketch001 = startSketchOn('${plane}')profile001 = startProfileAt([0.9, -1.22], sketch001)`
|
||||||
|> startProfileAt([0.9, -1.22], %)`
|
|
||||||
|
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
|
|
||||||
|
@ -94,6 +94,51 @@ test.describe('Editor tests', () => {
|
|||||||
|> close(%)`)
|
|> close(%)`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('ensure we use the cache, and do not re-execute', async ({ page }) => {
|
||||||
|
const u = await getUtils(page)
|
||||||
|
await page.setViewportSize({ width: 1000, height: 500 })
|
||||||
|
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
await u.codeLocator.click()
|
||||||
|
await page.keyboard.type(`sketch001 = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-10, -10], %)
|
||||||
|
|> line([20, 0], %)
|
||||||
|
|> line([0, 20], %)
|
||||||
|
|> line([-20, 0], %)
|
||||||
|
|> close(%)`)
|
||||||
|
|
||||||
|
// Ensure we execute the first time.
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-receive-command-type="scene_clear_all"]')
|
||||||
|
).toHaveCount(2)
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-message-type="execution-done"]')
|
||||||
|
).toHaveCount(2)
|
||||||
|
|
||||||
|
// Add whitespace to the end of the code.
|
||||||
|
await u.codeLocator.click()
|
||||||
|
await page.keyboard.press('ArrowUp')
|
||||||
|
await page.keyboard.press('ArrowUp')
|
||||||
|
await page.keyboard.press('ArrowUp')
|
||||||
|
await page.keyboard.press('ArrowUp')
|
||||||
|
await page.keyboard.press('Home')
|
||||||
|
await page.keyboard.type(' ')
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
await page.keyboard.type(' ')
|
||||||
|
|
||||||
|
// Ensure we don't execute the second time.
|
||||||
|
await u.openDebugPanel()
|
||||||
|
// Make sure we didn't clear the scene.
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-message-type="execution-done"]')
|
||||||
|
).toHaveCount(3)
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-receive-command-type="scene_clear_all"]')
|
||||||
|
).toHaveCount(2)
|
||||||
|
})
|
||||||
|
|
||||||
test('if you click the format button it formats your code and executes so lints are still there', async ({
|
test('if you click the format button it formats your code and executes so lints are still there', async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
|
|
||||||
type mouseParams = {
|
type mouseParams = {
|
||||||
pixelDiff?: number
|
pixelDiff?: number
|
||||||
|
shouldDbClick?: boolean
|
||||||
}
|
}
|
||||||
type mouseDragToParams = mouseParams & {
|
type mouseDragToParams = mouseParams & {
|
||||||
fromPoint: { x: number; y: number }
|
fromPoint: { x: number; y: number }
|
||||||
@ -75,11 +76,16 @@ export class SceneFixture {
|
|||||||
if (clickParams?.pixelDiff) {
|
if (clickParams?.pixelDiff) {
|
||||||
return doAndWaitForImageDiff(
|
return doAndWaitForImageDiff(
|
||||||
this.page,
|
this.page,
|
||||||
() => this.page.mouse.click(x, y),
|
() =>
|
||||||
|
clickParams?.shouldDbClick
|
||||||
|
? this.page.mouse.dblclick(x, y)
|
||||||
|
: this.page.mouse.click(x, y),
|
||||||
clickParams.pixelDiff
|
clickParams.pixelDiff
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return this.page.mouse.click(x, y)
|
return clickParams?.shouldDbClick
|
||||||
|
? this.page.mouse.dblclick(x, y)
|
||||||
|
: this.page.mouse.click(x, y)
|
||||||
},
|
},
|
||||||
(moveParams?: mouseParams) => {
|
(moveParams?: mouseParams) => {
|
||||||
if (moveParams?.pixelDiff) {
|
if (moveParams?.pixelDiff) {
|
||||||
@ -214,23 +220,7 @@ export class SceneFixture {
|
|||||||
coords: { x: number; y: number },
|
coords: { x: number; y: number },
|
||||||
diff: number
|
diff: number
|
||||||
) => {
|
) => {
|
||||||
let finalValue = colour
|
await expectPixelColor(this.page, colour, coords, diff)
|
||||||
await expect
|
|
||||||
.poll(async () => {
|
|
||||||
const pixel = (await getPixelRGBs(this.page)(coords, 1))[0]
|
|
||||||
if (!pixel) return null
|
|
||||||
finalValue = pixel
|
|
||||||
return pixel.every(
|
|
||||||
(channel, index) => Math.abs(channel - colour[index]) < diff
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.toBeTruthy()
|
|
||||||
.catch((cause) => {
|
|
||||||
throw new Error(
|
|
||||||
`ExpectPixelColor: expecting ${colour} got ${finalValue}`,
|
|
||||||
{ cause }
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get gizmo() {
|
get gizmo() {
|
||||||
@ -246,3 +236,28 @@ export class SceneFixture {
|
|||||||
await buttonToTest.click()
|
await buttonToTest.click()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function expectPixelColor(
|
||||||
|
page: Page,
|
||||||
|
colour: [number, number, number],
|
||||||
|
coords: { x: number; y: number },
|
||||||
|
diff: number
|
||||||
|
) {
|
||||||
|
let finalValue = colour
|
||||||
|
await expect
|
||||||
|
.poll(async () => {
|
||||||
|
const pixel = (await getPixelRGBs(page)(coords, 1))[0]
|
||||||
|
if (!pixel) return null
|
||||||
|
finalValue = pixel
|
||||||
|
return pixel.every(
|
||||||
|
(channel, index) => Math.abs(channel - colour[index]) < diff
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.toBeTruthy()
|
||||||
|
.catch((cause) => {
|
||||||
|
throw new Error(
|
||||||
|
`ExpectPixelColor: expecting ${colour} got ${finalValue}`,
|
||||||
|
{ cause }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -7,10 +7,14 @@ export class ToolbarFixture {
|
|||||||
|
|
||||||
extrudeButton!: Locator
|
extrudeButton!: Locator
|
||||||
loftButton!: Locator
|
loftButton!: Locator
|
||||||
|
shellButton!: Locator
|
||||||
offsetPlaneButton!: Locator
|
offsetPlaneButton!: Locator
|
||||||
startSketchBtn!: Locator
|
startSketchBtn!: Locator
|
||||||
lineBtn!: Locator
|
lineBtn!: Locator
|
||||||
|
tangentialArcBtn!: Locator
|
||||||
|
circleBtn!: Locator
|
||||||
rectangleBtn!: Locator
|
rectangleBtn!: Locator
|
||||||
|
lengthConstraintBtn!: Locator
|
||||||
exitSketchBtn!: Locator
|
exitSketchBtn!: Locator
|
||||||
editSketchBtn!: Locator
|
editSketchBtn!: Locator
|
||||||
fileTreeBtn!: Locator
|
fileTreeBtn!: Locator
|
||||||
@ -28,10 +32,14 @@ export class ToolbarFixture {
|
|||||||
this.page = page
|
this.page = page
|
||||||
this.extrudeButton = page.getByTestId('extrude')
|
this.extrudeButton = page.getByTestId('extrude')
|
||||||
this.loftButton = page.getByTestId('loft')
|
this.loftButton = page.getByTestId('loft')
|
||||||
|
this.shellButton = page.getByTestId('shell')
|
||||||
this.offsetPlaneButton = page.getByTestId('plane-offset')
|
this.offsetPlaneButton = page.getByTestId('plane-offset')
|
||||||
this.startSketchBtn = page.getByTestId('sketch')
|
this.startSketchBtn = page.getByTestId('sketch')
|
||||||
this.lineBtn = page.getByTestId('line')
|
this.lineBtn = page.getByTestId('line')
|
||||||
|
this.tangentialArcBtn = page.getByTestId('tangential-arc')
|
||||||
|
this.circleBtn = page.getByTestId('circle-center')
|
||||||
this.rectangleBtn = page.getByTestId('corner-rectangle')
|
this.rectangleBtn = page.getByTestId('corner-rectangle')
|
||||||
|
this.lengthConstraintBtn = page.getByTestId('constraint-length')
|
||||||
this.exitSketchBtn = page.getByTestId('sketch-exit')
|
this.exitSketchBtn = page.getByTestId('sketch-exit')
|
||||||
this.editSketchBtn = page.getByText('Edit Sketch')
|
this.editSketchBtn = page.getByText('Edit Sketch')
|
||||||
this.fileTreeBtn = page.locator('[id="files-button-holder"]')
|
this.fileTreeBtn = page.locator('[id="files-button-holder"]')
|
||||||
@ -89,4 +97,13 @@ export class ToolbarFixture {
|
|||||||
await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 })
|
await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
selectCenterRectangle = async () => {
|
||||||
|
await this.page
|
||||||
|
.getByRole('button', { name: 'caret down Corner rectangle:' })
|
||||||
|
.click()
|
||||||
|
await expect(
|
||||||
|
this.page.getByTestId('dropdown-center-rectangle')
|
||||||
|
).toBeVisible()
|
||||||
|
await this.page.getByTestId('dropdown-center-rectangle').click()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
TEST_SETTINGS_ONBOARDING_USER_MENU,
|
TEST_SETTINGS_ONBOARDING_USER_MENU,
|
||||||
} from './storageStates'
|
} from './storageStates'
|
||||||
import * as TOML from '@iarna/toml'
|
import * as TOML from '@iarna/toml'
|
||||||
|
import { expectPixelColor } from './fixtures/sceneFixture'
|
||||||
|
|
||||||
test.beforeEach(async ({ context, page }, testInfo) => {
|
test.beforeEach(async ({ context, page }, testInfo) => {
|
||||||
if (testInfo.tags.includes('@electron')) {
|
if (testInfo.tags.includes('@electron')) {
|
||||||
@ -45,7 +46,7 @@ test.describe('Onboarding tests', () => {
|
|||||||
{ settingsKey: TEST_SETTINGS_KEY }
|
{ settingsKey: TEST_SETTINGS_KEY }
|
||||||
)
|
)
|
||||||
|
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 1000 })
|
||||||
|
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
@ -54,6 +55,12 @@ test.describe('Onboarding tests', () => {
|
|||||||
|
|
||||||
// *and* that the code is shown in the editor
|
// *and* that the code is shown in the editor
|
||||||
await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
|
await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
|
||||||
|
|
||||||
|
// Make sure the model loaded
|
||||||
|
const XYPlanePoint = { x: 774, y: 116 } as const
|
||||||
|
const modelColor: [number, number, number] = [45, 45, 45]
|
||||||
|
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
|
||||||
|
expect(await u.getGreatestPixDiff(XYPlanePoint, modelColor)).toBeLessThan(8)
|
||||||
})
|
})
|
||||||
|
|
||||||
test(
|
test(
|
||||||
@ -72,7 +79,7 @@ test.describe('Onboarding tests', () => {
|
|||||||
|
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
|
|
||||||
const viewportSize = { width: 1200, height: 500 }
|
const viewportSize = { width: 1200, height: 1000 }
|
||||||
await page.setViewportSize(viewportSize)
|
await page.setViewportSize(viewportSize)
|
||||||
|
|
||||||
await test.step(`Create a project and open to the onboarding`, async () => {
|
await test.step(`Create a project and open to the onboarding`, async () => {
|
||||||
@ -92,6 +99,14 @@ test.describe('Onboarding tests', () => {
|
|||||||
await expect(page.locator('.cm-content')).toContainText(
|
await expect(page.locator('.cm-content')).toContainText(
|
||||||
'// Shelf Bracket'
|
'// Shelf Bracket'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: jess make less shit
|
||||||
|
// Make sure the model loaded
|
||||||
|
//const XYPlanePoint = { x: 986, y: 522 } as const
|
||||||
|
//const modelColor: [number, number, number] = [76, 76, 76]
|
||||||
|
//await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
|
||||||
|
|
||||||
|
//await expectPixelColor(page, modelColor, XYPlanePoint, 8)
|
||||||
})
|
})
|
||||||
|
|
||||||
await electronApp.close()
|
await electronApp.close()
|
||||||
@ -108,7 +123,7 @@ test.describe('Onboarding tests', () => {
|
|||||||
}, initialCode)
|
}, initialCode)
|
||||||
|
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 1000 })
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
// Replay the onboarding
|
// Replay the onboarding
|
||||||
@ -140,6 +155,12 @@ test.describe('Onboarding tests', () => {
|
|||||||
return localStorage.getItem('persistCode')
|
return localStorage.getItem('persistCode')
|
||||||
})
|
})
|
||||||
).toContain('// Shelf Bracket')
|
).toContain('// Shelf Bracket')
|
||||||
|
|
||||||
|
// Make sure the model loaded
|
||||||
|
const XYPlanePoint = { x: 986, y: 522 } as const
|
||||||
|
const modelColor: [number, number, number] = [76, 76, 76]
|
||||||
|
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
|
||||||
|
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Click through each onboarding step', async ({ page }) => {
|
test('Click through each onboarding step', async ({ page }) => {
|
||||||
@ -179,6 +200,17 @@ test.describe('Onboarding tests', () => {
|
|||||||
// Test that the onboarding pane is gone
|
// Test that the onboarding pane is gone
|
||||||
await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
|
await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
|
||||||
await expect(page.url()).not.toContain('onboarding')
|
await expect(page.url()).not.toContain('onboarding')
|
||||||
|
|
||||||
|
await u.openAndClearDebugPanel()
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
|
// TODO: jess to fix
|
||||||
|
// Make sure the model loaded
|
||||||
|
//const XYPlanePoint = { x: 774, y: 516 } as const
|
||||||
|
// const modelColor: [number, number, number] = [129, 129, 129]
|
||||||
|
// await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
|
||||||
|
// await expectPixelColor(page, modelColor, XYPlanePoint, 20)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Onboarding redirects and code updating', async ({ page }) => {
|
test('Onboarding redirects and code updating', async ({ page }) => {
|
||||||
@ -439,7 +471,7 @@ test(
|
|||||||
})
|
})
|
||||||
|
|
||||||
await test.step('Navigate into project', async () => {
|
await test.step('Navigate into project', async () => {
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 1000 })
|
||||||
|
|
||||||
page.on('console', console.log)
|
page.on('console', console.log)
|
||||||
|
|
||||||
@ -462,7 +494,15 @@ test(
|
|||||||
await test.step('Confirm that the onboarding has restarted', async () => {
|
await test.step('Confirm that the onboarding has restarted', async () => {
|
||||||
await expect(tutorialProjectIndicator).toBeVisible()
|
await expect(tutorialProjectIndicator).toBeVisible()
|
||||||
await expect(tutorialModalText).toBeVisible()
|
await expect(tutorialModalText).toBeVisible()
|
||||||
|
// Make sure the model loaded
|
||||||
|
const XYPlanePoint = { x: 988, y: 523 } as const
|
||||||
|
const modelColor: [number, number, number] = [76, 76, 76]
|
||||||
|
|
||||||
|
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
|
||||||
|
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
|
||||||
await tutorialDismissButton.click()
|
await tutorialDismissButton.click()
|
||||||
|
// Make sure model still there.
|
||||||
|
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step('Clear code and restart onboarding from settings', async () => {
|
await test.step('Clear code and restart onboarding from settings', async () => {
|
||||||
|
@ -135,7 +135,9 @@ test.describe('verify sketch on chamfer works', () => {
|
|||||||
pixelDiff: 50,
|
pixelDiff: 50,
|
||||||
})
|
})
|
||||||
await rectangle2ndClick()
|
await rectangle2ndClick()
|
||||||
await editor.expectEditor.toContain(afterRectangle2ndClickSnippet)
|
await editor.expectEditor.toContain(afterRectangle2ndClickSnippet, {
|
||||||
|
shouldNormalise: true,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step('Clean up so that `_sketchOnAChamfer` util can be called again', async () => {
|
await test.step('Clean up so that `_sketchOnAChamfer` util can be called again', async () => {
|
||||||
@ -177,18 +179,13 @@ test.describe('verify sketch on chamfer works', () => {
|
|||||||
|
|
||||||
afterChamferSelectSnippet:
|
afterChamferSelectSnippet:
|
||||||
'sketch002 = startSketchOn(extrude001, seg03)',
|
'sketch002 = startSketchOn(extrude001, seg03)',
|
||||||
afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)',
|
afterRectangle1stClickSnippet:
|
||||||
afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002)
|
'startProfileAt([205.96, 254.59], sketch002)',
|
||||||
|> angledLine([
|
afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002)
|
||||||
segAng(rectangleSegmentA002) - 90,
|
|>angledLine([segAng(rectangleSegmentA002)-90,105.26],%)
|
||||||
105.26
|
|>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%)
|
||||||
], %, $rectangleSegmentB001)
|
|>lineTo([profileStartX(%),profileStartY(%)],%)
|
||||||
|> angledLine([
|
|>close(%)`,
|
||||||
segAng(rectangleSegmentA002),
|
|
||||||
-segLen(rectangleSegmentA002)
|
|
||||||
], %, $rectangleSegmentC001)
|
|
||||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
|
||||||
|> close(%)`,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await sketchOnAChamfer({
|
await sketchOnAChamfer({
|
||||||
@ -209,19 +206,15 @@ test.describe('verify sketch on chamfer works', () => {
|
|||||||
|
|
||||||
afterChamferSelectSnippet:
|
afterChamferSelectSnippet:
|
||||||
'sketch003 = startSketchOn(extrude001, seg04)',
|
'sketch003 = startSketchOn(extrude001, seg04)',
|
||||||
afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)',
|
afterRectangle1stClickSnippet:
|
||||||
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003)
|
'startProfileAt([-209.64, 255.28], sketch003)',
|
||||||
|> angledLine([
|
afterRectangle2ndClickSnippet: `angledLine([0,11.56],%,$rectangleSegmentA003)
|
||||||
segAng(rectangleSegmentA003) - 90,
|
|>angledLine([segAng(rectangleSegmentA003)-90,106.84],%)
|
||||||
106.84
|
|>angledLine([segAng(rectangleSegmentA003),-segLen(rectangleSegmentA003)],%)
|
||||||
], %, $rectangleSegmentB002)
|
|>lineTo([profileStartX(%),profileStartY(%)],%)
|
||||||
|> angledLine([
|
|>close(%)`,
|
||||||
segAng(rectangleSegmentA003),
|
|
||||||
-segLen(rectangleSegmentA003)
|
|
||||||
], %, $rectangleSegmentC002)
|
|
||||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
|
||||||
|> close(%)`,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await sketchOnAChamfer({
|
await sketchOnAChamfer({
|
||||||
clickCoords: { x: 677, y: 87 },
|
clickCoords: { x: 677, y: 87 },
|
||||||
cameraPos: { x: -6200, y: 1500, z: 6200 },
|
cameraPos: { x: -6200, y: 1500, z: 6200 },
|
||||||
@ -234,19 +227,14 @@ test.describe('verify sketch on chamfer works', () => {
|
|||||||
]
|
]
|
||||||
}, %)`,
|
}, %)`,
|
||||||
afterChamferSelectSnippet:
|
afterChamferSelectSnippet:
|
||||||
'sketch003 = startSketchOn(extrude001, seg04)',
|
'sketch004 = startSketchOn(extrude001, seg05)',
|
||||||
afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)',
|
afterRectangle1stClickSnippet:
|
||||||
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003)
|
'startProfileAt([82.57, 322.96], sketch004)',
|
||||||
|> angledLine([
|
afterRectangle2ndClickSnippet: `angledLine([0,11.16],%,$rectangleSegmentA004)
|
||||||
segAng(rectangleSegmentA003) - 90,
|
|>angledLine([segAng(rectangleSegmentA004)-90,103.07],%)
|
||||||
106.84
|
|>angledLine([segAng(rectangleSegmentA004),-segLen(rectangleSegmentA004)],%)
|
||||||
], %, $rectangleSegmentB002)
|
|>lineTo([profileStartX(%),profileStartY(%)],%)|
|
||||||
|> angledLine([
|
>close(%)`,
|
||||||
segAng(rectangleSegmentA003),
|
|
||||||
-segLen(rectangleSegmentA003)
|
|
||||||
], %, $rectangleSegmentC002)
|
|
||||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
|
||||||
|> close(%)`,
|
|
||||||
})
|
})
|
||||||
/// last one
|
/// last one
|
||||||
await sketchOnAChamfer({
|
await sketchOnAChamfer({
|
||||||
@ -259,104 +247,97 @@ test.describe('verify sketch on chamfer works', () => {
|
|||||||
}, %)`,
|
}, %)`,
|
||||||
afterChamferSelectSnippet:
|
afterChamferSelectSnippet:
|
||||||
'sketch005 = startSketchOn(extrude001, seg06)',
|
'sketch005 = startSketchOn(extrude001, seg06)',
|
||||||
afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)',
|
afterRectangle1stClickSnippet:
|
||||||
afterRectangle2ndClickSnippet: `angledLine([0, 9.1], %, $rectangleSegmentA005)
|
'startProfileAt([-23.43, 19.69], sketch005)',
|
||||||
|
afterRectangle2ndClickSnippet: `angledLine([0,9.1],%,$rectangleSegmentA005)
|
||||||
|> angledLine([
|
|>angledLine([segAng(rectangleSegmentA005)-90,84.07],%)
|
||||||
segAng(rectangleSegmentA005) - 90,
|
|>angledLine([segAng(rectangleSegmentA005),-segLen(rectangleSegmentA005)],%)
|
||||||
84.07
|
|>lineTo([profileStartX(%),profileStartY(%)],%)
|
||||||
], %, $rectangleSegmentB004)
|
|>close(%)`,
|
||||||
|> angledLine([
|
|
||||||
segAng(rectangleSegmentA005),
|
|
||||||
-segLen(rectangleSegmentA005)
|
|
||||||
], %, $rectangleSegmentC004)
|
|
||||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
|
||||||
|> close(%)`,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step('verify at the end of the test that final code is what is expected', async () => {
|
await test.step('verify at the end of the test that final code is what is expected', async () => {
|
||||||
await editor.expectEditor.toContain(
|
await editor.expectEditor.toContain(
|
||||||
`sketch001 = startSketchOn('XZ')
|
`sketch001 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]
|
||||||
|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]
|
|> angledLine([0, 268.43], %, $rectangleSegmentA001)
|
||||||
|> angledLine([0, 268.43], %, $rectangleSegmentA001)
|
|> angledLine([
|
||||||
|> angledLine([
|
segAng(rectangleSegmentA001) - 90,
|
||||||
segAng(rectangleSegmentA001) - 90,
|
217.26
|
||||||
217.26
|
], %, $seg01)
|
||||||
], %, $seg01)
|
|> angledLine([
|
||||||
|> angledLine([
|
segAng(rectangleSegmentA001),
|
||||||
segAng(rectangleSegmentA001),
|
-segLen(rectangleSegmentA001)
|
||||||
-segLen(rectangleSegmentA001)
|
], %, $yo)
|
||||||
], %, $yo)
|
|> lineTo([profileStartX(%), profileStartY(%)], %, $seg02)
|
||||||
|> lineTo([profileStartX(%), profileStartY(%)], %, $seg02)
|
|> close(%)
|
||||||
|> close(%)
|
extrude001 = extrude(100, sketch001)
|
||||||
extrude001 = extrude(100, sketch001)
|
|> chamfer({
|
||||||
|> chamfer({
|
length = 30,
|
||||||
length = 30,
|
tags = [getOppositeEdge(seg01)]
|
||||||
tags = [getOppositeEdge(seg01)]
|
}, %, $seg03)
|
||||||
}, %, $seg03)
|
|> chamfer({ length = 30, tags = [seg01] }, %, $seg04)
|
||||||
|> chamfer({ length = 30, tags = [seg01] }, %, $seg04)
|
|> chamfer({
|
||||||
|> chamfer({
|
length = 30,
|
||||||
length = 30,
|
tags = [getNextAdjacentEdge(seg02)]
|
||||||
tags = [getNextAdjacentEdge(seg02)]
|
}, %, $seg05)
|
||||||
}, %, $seg05)
|
|> chamfer({
|
||||||
|> chamfer({
|
length = 30,
|
||||||
length = 30,
|
tags = [getNextAdjacentEdge(yo)]
|
||||||
tags = [getNextAdjacentEdge(yo)]
|
}, %, $seg06)
|
||||||
}, %, $seg06)
|
sketch005 = startSketchOn(extrude001, seg06)
|
||||||
sketch005 = startSketchOn(extrude001, seg06)
|
profile004 = startProfileAt([-23.43, 19.69], sketch005)
|
||||||
|> startProfileAt([-23.43, 19.69], %)
|
|> angledLine([0, 9.1], %, $rectangleSegmentA005)
|
||||||
|> angledLine([0, 9.1], %, $rectangleSegmentA005)
|
|> angledLine([
|
||||||
|> angledLine([
|
segAng(rectangleSegmentA005) - 90,
|
||||||
segAng(rectangleSegmentA005) - 90,
|
84.07
|
||||||
84.07
|
], %)
|
||||||
], %, $rectangleSegmentB004)
|
|> angledLine([
|
||||||
|> angledLine([
|
segAng(rectangleSegmentA005),
|
||||||
segAng(rectangleSegmentA005),
|
-segLen(rectangleSegmentA005)
|
||||||
-segLen(rectangleSegmentA005)
|
], %)
|
||||||
], %, $rectangleSegmentC004)
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
|> close(%)
|
||||||
|> close(%)
|
sketch004 = startSketchOn(extrude001, seg05)
|
||||||
sketch004 = startSketchOn(extrude001, seg05)
|
profile003 = startProfileAt([82.57, 322.96], sketch004)
|
||||||
|> startProfileAt([82.57, 322.96], %)
|
|> angledLine([0, 11.16], %, $rectangleSegmentA004)
|
||||||
|> angledLine([0, 11.16], %, $rectangleSegmentA004)
|
|> angledLine([
|
||||||
|> angledLine([
|
segAng(rectangleSegmentA004) - 90,
|
||||||
segAng(rectangleSegmentA004) - 90,
|
103.07
|
||||||
103.07
|
], %)
|
||||||
], %, $rectangleSegmentB003)
|
|> angledLine([
|
||||||
|> angledLine([
|
segAng(rectangleSegmentA004),
|
||||||
segAng(rectangleSegmentA004),
|
-segLen(rectangleSegmentA004)
|
||||||
-segLen(rectangleSegmentA004)
|
], %)
|
||||||
], %, $rectangleSegmentC003)
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
|> close(%)
|
||||||
|> close(%)
|
sketch003 = startSketchOn(extrude001, seg04)
|
||||||
sketch003 = startSketchOn(extrude001, seg04)
|
profile002 = startProfileAt([-209.64, 255.28], sketch003)
|
||||||
|> startProfileAt([-209.64, 255.28], %)
|
|> angledLine([0, 11.56], %, $rectangleSegmentA003)
|
||||||
|> angledLine([0, 11.56], %, $rectangleSegmentA003)
|
|> angledLine([
|
||||||
|> angledLine([
|
segAng(rectangleSegmentA003) - 90,
|
||||||
segAng(rectangleSegmentA003) - 90,
|
106.84
|
||||||
106.84
|
], %)
|
||||||
], %, $rectangleSegmentB002)
|
|> angledLine([
|
||||||
|> angledLine([
|
segAng(rectangleSegmentA003),
|
||||||
segAng(rectangleSegmentA003),
|
-segLen(rectangleSegmentA003)
|
||||||
-segLen(rectangleSegmentA003)
|
], %)
|
||||||
], %, $rectangleSegmentC002)
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
|> close(%)
|
||||||
|> close(%)
|
sketch002 = startSketchOn(extrude001, seg03)
|
||||||
sketch002 = startSketchOn(extrude001, seg03)
|
profile001 = startProfileAt([205.96, 254.59], sketch002)
|
||||||
|> startProfileAt([205.96, 254.59], %)
|
|> angledLine([0, 11.39], %, $rectangleSegmentA002)
|
||||||
|> angledLine([0, 11.39], %, $rectangleSegmentA002)
|
|> angledLine([
|
||||||
|> angledLine([
|
segAng(rectangleSegmentA002) - 90,
|
||||||
segAng(rectangleSegmentA002) - 90,
|
105.26
|
||||||
105.26
|
], %)
|
||||||
], %, $rectangleSegmentB001)
|
|> angledLine([
|
||||||
|> angledLine([
|
segAng(rectangleSegmentA002),
|
||||||
segAng(rectangleSegmentA002),
|
-segLen(rectangleSegmentA002)
|
||||||
-segLen(rectangleSegmentA002)
|
], %)
|
||||||
], %, $rectangleSegmentC001)
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
|> close(%)
|
||||||
|> close(%)
|
`,
|
||||||
`,
|
|
||||||
{ shouldNormalise: true }
|
{ shouldNormalise: true }
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -392,18 +373,13 @@ test.describe('verify sketch on chamfer works', () => {
|
|||||||
beforeChamferSnippetEnd: '}, extrude001)',
|
beforeChamferSnippetEnd: '}, extrude001)',
|
||||||
afterChamferSelectSnippet:
|
afterChamferSelectSnippet:
|
||||||
'sketch002 = startSketchOn(extrude001, seg03)',
|
'sketch002 = startSketchOn(extrude001, seg03)',
|
||||||
afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)',
|
afterRectangle1stClickSnippet:
|
||||||
afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002)
|
'startProfileAt([205.96, 254.59], sketch002)',
|
||||||
|> angledLine([
|
afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002)
|
||||||
segAng(rectangleSegmentA002) - 90,
|
|>angledLine([segAng(rectangleSegmentA002)-90,105.26],%)
|
||||||
105.26
|
|>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%)
|
||||||
], %, $rectangleSegmentB001)
|
|>lineTo([profileStartX(%),profileStartY(%)],%)
|
||||||
|> angledLine([
|
|>close(%)`,
|
||||||
segAng(rectangleSegmentA002),
|
|
||||||
-segLen(rectangleSegmentA002)
|
|
||||||
], %, $rectangleSegmentC001)
|
|
||||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
|
||||||
|> close(%)`,
|
|
||||||
})
|
})
|
||||||
await editor.expectEditor.toContain(
|
await editor.expectEditor.toContain(
|
||||||
`sketch001 = startSketchOn('XZ')
|
`sketch001 = startSketchOn('XZ')
|
||||||
@ -433,16 +409,16 @@ chamf = chamfer({
|
|||||||
]
|
]
|
||||||
}, %)
|
}, %)
|
||||||
sketch002 = startSketchOn(extrude001, seg03)
|
sketch002 = startSketchOn(extrude001, seg03)
|
||||||
|> startProfileAt([205.96, 254.59], %)
|
profile001 = startProfileAt([205.96, 254.59], sketch002)
|
||||||
|> angledLine([0, 11.39], %, $rectangleSegmentA002)
|
|> angledLine([0, 11.39], %, $rectangleSegmentA002)
|
||||||
|> angledLine([
|
|> angledLine([
|
||||||
segAng(rectangleSegmentA002) - 90,
|
segAng(rectangleSegmentA002) - 90,
|
||||||
105.26
|
105.26
|
||||||
], %, $rectangleSegmentB001)
|
], %)
|
||||||
|> angledLine([
|
|> angledLine([
|
||||||
segAng(rectangleSegmentA002),
|
segAng(rectangleSegmentA002),
|
||||||
-segLen(rectangleSegmentA002)
|
-segLen(rectangleSegmentA002)
|
||||||
], %, $rectangleSegmentC001)
|
], %)
|
||||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
`,
|
`,
|
||||||
@ -504,10 +480,10 @@ 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]}], sketch001)`,
|
||||||
segmentOnXAxis: `xLine(${xAxisSloppy.kcl[0]}, %)`,
|
segmentOnXAxis: `xLine(${xAxisSloppy.kcl[0]}, %)`,
|
||||||
afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], %)`,
|
afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], sketch001)`,
|
||||||
afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`,
|
afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], sketch001)`,
|
||||||
}
|
}
|
||||||
|
|
||||||
await app.initialise()
|
await app.initialise()
|
||||||
@ -768,3 +744,168 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const shellPointAndClickCapCases = [
|
||||||
|
{ shouldPreselect: true },
|
||||||
|
{ shouldPreselect: false },
|
||||||
|
]
|
||||||
|
shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
|
||||||
|
test(`Shell point-and-click cap (preselected sketches: ${shouldPreselect})`, async ({
|
||||||
|
app,
|
||||||
|
scene,
|
||||||
|
editor,
|
||||||
|
toolbar,
|
||||||
|
cmdBar,
|
||||||
|
}) => {
|
||||||
|
const initialCode = `sketch001 = startSketchOn('XZ')
|
||||||
|
|> circle({ center = [0, 0], radius = 30 }, %)
|
||||||
|
extrude001 = extrude(30, sketch001)
|
||||||
|
`
|
||||||
|
await app.initialise(initialCode)
|
||||||
|
|
||||||
|
// One dumb hardcoded screen pixel value
|
||||||
|
const testPoint = { x: 575, y: 200 }
|
||||||
|
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||||
|
const shellDeclaration =
|
||||||
|
"shell001 = shell({ faces = ['end'], thickness = 5 }, extrude001)"
|
||||||
|
|
||||||
|
await test.step(`Look for the grey of the shape`, async () => {
|
||||||
|
await scene.expectPixelColor([127, 127, 127], testPoint, 15)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!shouldPreselect) {
|
||||||
|
await test.step(`Go through the command bar flow without preselected faces`, async () => {
|
||||||
|
await toolbar.shellButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'selection',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '',
|
||||||
|
Thickness: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'selection',
|
||||||
|
commandName: 'Shell',
|
||||||
|
})
|
||||||
|
await clickOnCap()
|
||||||
|
await app.page.waitForTimeout(500)
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '1 cap',
|
||||||
|
Thickness: '5',
|
||||||
|
},
|
||||||
|
commandName: 'Shell',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await test.step(`Preselect the cap`, async () => {
|
||||||
|
await clickOnCap()
|
||||||
|
await app.page.waitForTimeout(500)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => {
|
||||||
|
await toolbar.shellButton.click()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '1 cap',
|
||||||
|
Thickness: '5',
|
||||||
|
},
|
||||||
|
commandName: 'Shell',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
||||||
|
await editor.expectEditor.toContain(shellDeclaration)
|
||||||
|
await editor.expectState({
|
||||||
|
diagnostics: [],
|
||||||
|
activeLines: [shellDeclaration],
|
||||||
|
highlightedCode: '',
|
||||||
|
})
|
||||||
|
await scene.expectPixelColor([146, 146, 146], testPoint, 15)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Shell point-and-click wall', async ({
|
||||||
|
app,
|
||||||
|
page,
|
||||||
|
scene,
|
||||||
|
editor,
|
||||||
|
toolbar,
|
||||||
|
cmdBar,
|
||||||
|
}) => {
|
||||||
|
const initialCode = `sketch001 = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-20, 20], %)
|
||||||
|
|> xLine(40, %)
|
||||||
|
|> yLine(-60, %)
|
||||||
|
|> xLine(-40, %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)
|
||||||
|
extrude001 = extrude(40, sketch001)
|
||||||
|
`
|
||||||
|
await app.initialise(initialCode)
|
||||||
|
|
||||||
|
// One dumb hardcoded screen pixel value
|
||||||
|
const testPoint = { x: 580, y: 180 }
|
||||||
|
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||||
|
const [clickOnWall] = scene.makeMouseHelpers(testPoint.x, testPoint.y + 70)
|
||||||
|
const mutatedCode = 'xLine(-40, %, $seg01)'
|
||||||
|
const shellDeclaration =
|
||||||
|
"shell001 = shell({ faces = ['end', seg01], thickness = 5}, extrude001)"
|
||||||
|
const formattedOutLastLine = '}, extrude001)'
|
||||||
|
|
||||||
|
await test.step(`Look for the grey of the shape`, async () => {
|
||||||
|
await scene.expectPixelColor([99, 99, 99], testPoint, 15)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Go through the command bar flow, selecting a wall and keeping default thickness`, async () => {
|
||||||
|
await toolbar.shellButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'selection',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '',
|
||||||
|
Thickness: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'selection',
|
||||||
|
commandName: 'Shell',
|
||||||
|
})
|
||||||
|
await clickOnCap()
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
await clickOnWall()
|
||||||
|
await app.page.waitForTimeout(500)
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '1 cap, 1 face',
|
||||||
|
Thickness: '5',
|
||||||
|
},
|
||||||
|
commandName: 'Shell',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
||||||
|
await editor.expectEditor.toContain(mutatedCode)
|
||||||
|
await editor.expectEditor.toContain(shellDeclaration)
|
||||||
|
await editor.expectState({
|
||||||
|
diagnostics: [],
|
||||||
|
activeLines: [formattedOutLastLine],
|
||||||
|
highlightedCode: '',
|
||||||
|
})
|
||||||
|
await scene.expectPixelColor([49, 49, 49], testPoint, 15)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
@ -114,9 +114,9 @@ test.describe('Sketch tests', () => {
|
|||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'persistCode',
|
'persistCode',
|
||||||
`sketch001 = startSketchOn('XZ')
|
`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt([4.61, -14.01], %)
|
|> startProfileAt([2.61, -4.01], %)
|
||||||
|> xLine(12.73, %)
|
|> xLine(8.73, %)
|
||||||
|> tangentialArcTo([24.95, -5.38], %)`
|
|> tangentialArcTo([8.33, -1.31], %)`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -126,7 +126,7 @@ test.describe('Sketch tests', () => {
|
|||||||
|
|
||||||
await expect(async () => {
|
await expect(async () => {
|
||||||
await page.mouse.click(700, 200)
|
await page.mouse.click(700, 200)
|
||||||
await page.getByText('tangentialArcTo([24.95, -5.38], %)').click()
|
await page.getByText('tangentialArcTo([8.33, -1.31], %)').click()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Edit Sketch' })
|
page.getByRole('button', { name: 'Edit Sketch' })
|
||||||
).toBeEnabled({ timeout: 1000 })
|
).toBeEnabled({ timeout: 1000 })
|
||||||
@ -135,7 +135,7 @@ test.describe('Sketch tests', () => {
|
|||||||
|
|
||||||
await page.waitForTimeout(600) // wait for animation
|
await page.waitForTimeout(600) // wait for animation
|
||||||
|
|
||||||
await page.getByText('tangentialArcTo([24.95, -5.38], %)').click()
|
await page.getByText('tangentialArcTo([8.33, -1.31], %)').click()
|
||||||
await page.keyboard.press('End')
|
await page.keyboard.press('End')
|
||||||
await page.keyboard.down('Shift')
|
await page.keyboard.down('Shift')
|
||||||
await page.keyboard.press('ArrowUp')
|
await page.keyboard.press('ArrowUp')
|
||||||
@ -149,17 +149,21 @@ test.describe('Sketch tests', () => {
|
|||||||
|
|
||||||
await page.getByRole('button', { name: 'line Line', exact: true }).click()
|
await page.getByRole('button', { name: 'line Line', exact: true }).click()
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
// click start profileAt handle to continue profile
|
||||||
|
await page.mouse.click(702, 407)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await expect(async () => {
|
await expect(async () => {
|
||||||
|
// click to add segment
|
||||||
await page.mouse.click(700, 200)
|
await page.mouse.click(700, 200)
|
||||||
|
|
||||||
await expect.poll(u.normalisedEditorCode, { timeout: 1000 })
|
await expect.poll(u.normalisedEditorCode, { timeout: 1000 })
|
||||||
.toBe(`sketch001 = startSketchOn('XZ')
|
.toBe(`sketch002 = startSketchOn('XZ')
|
||||||
|> startProfileAt([12.34, -12.34], %)
|
sketch001 = startProfileAt([12.34, -12.34], sketch002)
|
||||||
|> yLine(12.34, %)
|
|> yLine(12.34, %)
|
||||||
|
|
||||||
`)
|
`)
|
||||||
}).toPass({ timeout: 40_000, intervals: [1_000] })
|
}).toPass({ timeout: 5_000, intervals: [1_000] })
|
||||||
})
|
})
|
||||||
test('Can exit selection of face', async ({ page }) => {
|
test('Can exit selection of face', async ({ page }) => {
|
||||||
// Load the app with the code panes
|
// Load the app with the code panes
|
||||||
@ -669,7 +673,7 @@ test.describe('Sketch tests', () => {
|
|||||||
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||||
|
|
||||||
await click00r(0, 0)
|
await click00r(0, 0)
|
||||||
codeStr += ` |> startProfileAt(${toSU([0, 0])}, %)`
|
codeStr += `profile001 = startProfileAt(${toSU([0, 0])}, sketch001)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(50, 0)
|
await click00r(50, 0)
|
||||||
@ -705,7 +709,7 @@ test.describe('Sketch tests', () => {
|
|||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
await click00r(30, 0)
|
await click00r(30, 0)
|
||||||
codeStr += ` |> startProfileAt([2.03, 0], %)`
|
codeStr += `profile002 = startProfileAt([2.03, 0], sketch002)`
|
||||||
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
|
||||||
@ -742,7 +746,9 @@ test.describe('Sketch tests', () => {
|
|||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
|
|
||||||
const code = `sketch001 = startSketchOn('-XZ')
|
const code = `sketch001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(scale * 34.8)}], %)
|
profile001 = startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(
|
||||||
|
scale * 34.8
|
||||||
|
)}], sketch001)
|
||||||
|> xLine(${roundOff(scale * 139.19)}, %)
|
|> xLine(${roundOff(scale * 139.19)}, %)
|
||||||
|> yLine(-${roundOff(scale * 139.2)}, %)
|
|> yLine(-${roundOff(scale * 139.2)}, %)
|
||||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
@ -808,11 +814,17 @@ test.describe('Sketch tests', () => {
|
|||||||
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()
|
||||||
|
|
||||||
await expect(page.locator('.cm-content')).toHaveText(code)
|
await expect
|
||||||
// Assert the tool was unequipped
|
.poll(async () => {
|
||||||
|
const text = await page.locator('.cm-content').innerText()
|
||||||
|
return text.replace(/\s/g, '')
|
||||||
|
})
|
||||||
|
.toBe(code.replace(/\s/g, ''))
|
||||||
|
|
||||||
|
// Assert the tool stays equipped after a profile is closed (ready for the next one)
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'line Line', exact: true })
|
page.getByRole('button', { name: 'line Line', exact: true })
|
||||||
).not.toHaveAttribute('aria-pressed', 'true')
|
).toHaveAttribute('aria-pressed', 'true')
|
||||||
|
|
||||||
// exit sketch
|
// exit sketch
|
||||||
await u.openAndClearDebugPanel()
|
await u.openAndClearDebugPanel()
|
||||||
@ -1130,11 +1142,17 @@ sketch002 = startSketchOn(extrude001, 'END')
|
|||||||
await page.mouse.click(XYPlanePoint.x, XYPlanePoint.y)
|
await page.mouse.click(XYPlanePoint.x, XYPlanePoint.y)
|
||||||
await page.waitForTimeout(200)
|
await page.waitForTimeout(200)
|
||||||
await page.mouse.click(XYPlanePoint.x + 50, XYPlanePoint.y + 50)
|
await page.mouse.click(XYPlanePoint.x + 50, XYPlanePoint.y + 50)
|
||||||
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
await expect
|
||||||
|> startProfileAt([11.8, 9.09], %)
|
.poll(async () => {
|
||||||
|> line([3.39, -3.39], %)
|
const text = await u.codeLocator.innerText()
|
||||||
`)
|
return text.replace(/\s/g, '')
|
||||||
|
})
|
||||||
|
.toBe(
|
||||||
|
`sketch001 = startSketchOn('XZ')
|
||||||
|
profile001 = startProfileAt([11.8, 9.09], sketch001)
|
||||||
|
|> line([3.39, -3.39], %)
|
||||||
|
`.replace(/\s/g, '')
|
||||||
|
)
|
||||||
await page.addInitScript(async () => {
|
await page.addInitScript(async () => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'persistCode',
|
'persistCode',
|
||||||
@ -1419,3 +1437,560 @@ test2.describe(`Sketching with offset planes`, () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test2.describe('multi-profile sketching', () => {
|
||||||
|
test2(
|
||||||
|
'Can add multiple profiles to a sketch (all tool types)',
|
||||||
|
async ({ app, scene, toolbar, editor }) => {
|
||||||
|
await app.initialise(``)
|
||||||
|
|
||||||
|
const [selectXZPlane] = scene.makeMouseHelpers(650, 150)
|
||||||
|
|
||||||
|
const [startProfile1] = scene.makeMouseHelpers(568, 70)
|
||||||
|
const [endLineStartTanArc] = scene.makeMouseHelpers(701, 78)
|
||||||
|
const [endArcStartLine] = scene.makeMouseHelpers(745, 189)
|
||||||
|
|
||||||
|
const [startProfile2] = scene.makeMouseHelpers(782, 80)
|
||||||
|
const [profile2Point2] = scene.makeMouseHelpers(921, 90)
|
||||||
|
const [profile2Point3] = scene.makeMouseHelpers(953, 178)
|
||||||
|
|
||||||
|
const [circle1Center] = scene.makeMouseHelpers(842, 147)
|
||||||
|
const [circle1Radius] = scene.makeMouseHelpers(870, 171)
|
||||||
|
|
||||||
|
const [circle2Center] = scene.makeMouseHelpers(850, 222)
|
||||||
|
const [circle2Radius] = scene.makeMouseHelpers(843, 230)
|
||||||
|
|
||||||
|
const [crnRect1point1] = scene.makeMouseHelpers(583, 205)
|
||||||
|
const [crnRect1point2] = scene.makeMouseHelpers(618, 320)
|
||||||
|
|
||||||
|
const [crnRect2point1] = scene.makeMouseHelpers(663, 215)
|
||||||
|
const [crnRect2point2] = scene.makeMouseHelpers(744, 276)
|
||||||
|
|
||||||
|
const [cntrRect1point1] = scene.makeMouseHelpers(624, 387)
|
||||||
|
const [cntrRect1point2] = scene.makeMouseHelpers(676, 355)
|
||||||
|
|
||||||
|
const [cntrRect2point1] = scene.makeMouseHelpers(785, 332)
|
||||||
|
const [cntrRect2point2] = scene.makeMouseHelpers(808, 286)
|
||||||
|
|
||||||
|
await toolbar.startSketchPlaneSelection()
|
||||||
|
await selectXZPlane()
|
||||||
|
// timeout wait for engine animation is unavoidable
|
||||||
|
await app.page.waitForTimeout(600)
|
||||||
|
await editor.expectEditor.toContain(`sketch001 = startSketchOn('XZ')`)
|
||||||
|
await test.step('Create a close profile stopping mid profile to equip the tangential arc, and than back to the line tool', async () => {
|
||||||
|
await startProfile1()
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`profile001 = startProfileAt([4.61, 12.21], sketch001)`
|
||||||
|
)
|
||||||
|
|
||||||
|
await endLineStartTanArc()
|
||||||
|
await editor.expectEditor.toContain(`|> line([9.02, -0.55], %)`)
|
||||||
|
await toolbar.tangentialArcBtn.click()
|
||||||
|
await app.page.waitForTimeout(100)
|
||||||
|
await endLineStartTanArc()
|
||||||
|
|
||||||
|
await endArcStartLine()
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`|> tangentialArcTo([16.61, 4.14], %)`
|
||||||
|
)
|
||||||
|
await toolbar.lineBtn.click()
|
||||||
|
await app.page.waitForTimeout(100)
|
||||||
|
await endArcStartLine()
|
||||||
|
|
||||||
|
await app.page.mouse.click(572, 110)
|
||||||
|
await editor.expectEditor.toContain(`|> line([-11.73, 5.35], %)`)
|
||||||
|
await startProfile1()
|
||||||
|
await editor.expectEditor
|
||||||
|
.toContain(`|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)`)
|
||||||
|
await app.page.waitForTimeout(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Without unequipping from the last step, make another profile, and one that is not closed', async () => {
|
||||||
|
await startProfile2()
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`profile002 = startProfileAt([19.12, 11.53], sketch001)`
|
||||||
|
)
|
||||||
|
await profile2Point2()
|
||||||
|
await editor.expectEditor.toContain(`|> line([9.43, -0.68], %)`)
|
||||||
|
await profile2Point3()
|
||||||
|
await editor.expectEditor.toContain(`|> line([2.17, -5.97], %)`)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('create two circles in a row without unequip', async () => {
|
||||||
|
await toolbar.circleBtn.click()
|
||||||
|
|
||||||
|
await circle1Center()
|
||||||
|
await app.page.waitForTimeout(100)
|
||||||
|
await circle1Radius()
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`profile003 = circle({ center = [23.19, 6.98], radius = 2.5 }, sketch001)`
|
||||||
|
)
|
||||||
|
|
||||||
|
await test.step('hover in empty space to wait for overlays to get out of the way', async () => {
|
||||||
|
await app.page.mouse.move(951, 223)
|
||||||
|
await app.page.waitForTimeout(1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
await circle2Center()
|
||||||
|
await app.page.waitForTimeout(100)
|
||||||
|
await circle2Radius()
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`profile004 = circle({ center = [23.74, 1.9], radius = 0.72 }, sketch001)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
await test.step('create two corner rectangles in a row without unequip', async () => {
|
||||||
|
await toolbar.rectangleBtn.click()
|
||||||
|
|
||||||
|
await crnRect1point1()
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`profile005 = startProfileAt([5.63, 3.05], sketch001)`
|
||||||
|
)
|
||||||
|
await crnRect1point2()
|
||||||
|
await editor.expectEditor
|
||||||
|
.toContain(`|> angledLine([0, 2.37], %, $rectangleSegmentA001)
|
||||||
|
|> angledLine([segAng(rectangleSegmentA001) - 90, 7.8], %)
|
||||||
|
|> angledLine([
|
||||||
|
segAng(rectangleSegmentA001),
|
||||||
|
-segLen(rectangleSegmentA001)
|
||||||
|
], %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)`)
|
||||||
|
await app.page.waitForTimeout(100)
|
||||||
|
|
||||||
|
await crnRect2point1()
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`profile006 = startProfileAt([11.05, 2.37], sketch001)`
|
||||||
|
)
|
||||||
|
await crnRect2point2()
|
||||||
|
await editor.expectEditor
|
||||||
|
.toContain(`|> angledLine([0, 5.49], %, $rectangleSegmentA002)
|
||||||
|
|> angledLine([
|
||||||
|
segAng(rectangleSegmentA002) - 90,
|
||||||
|
4.14
|
||||||
|
], %)
|
||||||
|
|> angledLine([
|
||||||
|
segAng(rectangleSegmentA002),
|
||||||
|
-segLen(rectangleSegmentA002)
|
||||||
|
], %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)`)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('create two center rectangles in a row without unequip', async () => {
|
||||||
|
await toolbar.selectCenterRectangle()
|
||||||
|
|
||||||
|
await cntrRect1point1()
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`profile007 = startProfileAt([8.41, -9.29], sketch001)`
|
||||||
|
)
|
||||||
|
await cntrRect1point2()
|
||||||
|
await editor.expectEditor
|
||||||
|
.toContain(`|> angledLine([0, 7.06], %, $rectangleSegmentA003)
|
||||||
|
|> angledLine([
|
||||||
|
segAng(rectangleSegmentA003) + 90,
|
||||||
|
4.34
|
||||||
|
], %)
|
||||||
|
|> angledLine([
|
||||||
|
segAng(rectangleSegmentA003),
|
||||||
|
-segLen(rectangleSegmentA003)
|
||||||
|
], %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)`)
|
||||||
|
await app.page.waitForTimeout(100)
|
||||||
|
|
||||||
|
await cntrRect2point1()
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`profile008 = startProfileAt([19.33, -5.56], sketch001)`
|
||||||
|
)
|
||||||
|
await cntrRect2point2()
|
||||||
|
await editor.expectEditor
|
||||||
|
.toContain(`|> angledLine([0, 3.12], %, $rectangleSegmentA004)
|
||||||
|
|> angledLine([
|
||||||
|
segAng(rectangleSegmentA004) + 90,
|
||||||
|
6.24
|
||||||
|
], %)
|
||||||
|
|> angledLine([
|
||||||
|
segAng(rectangleSegmentA004),
|
||||||
|
-segLen(rectangleSegmentA004)
|
||||||
|
], %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
test2(
|
||||||
|
'Can edit a sketch with multiple profiles, dragging segments to edit them, and adding one new profile',
|
||||||
|
async ({ app, scene, toolbar, editor }) => {
|
||||||
|
await app.initialise(`sketch001 = startSketchOn('XZ')
|
||||||
|
profile001 = startProfileAt([6.24, 4.54], sketch001)
|
||||||
|
|> line([-0.41, 6.99], %)
|
||||||
|
|> line([8.61, 0.74], %)
|
||||||
|
|> line([10.99, -5.22], %)
|
||||||
|
profile002 = startProfileAt([11.19, 5.02], sketch001)
|
||||||
|
|> angledLine([0, 10.78], %, $rectangleSegmentA001)
|
||||||
|
|> angledLine([
|
||||||
|
segAng(rectangleSegmentA001) - 90,
|
||||||
|
4.14
|
||||||
|
], %)
|
||||||
|
|> angledLine([
|
||||||
|
segAng(rectangleSegmentA001),
|
||||||
|
-segLen(rectangleSegmentA001)
|
||||||
|
], %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)
|
||||||
|
profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001)
|
||||||
|
`)
|
||||||
|
|
||||||
|
const [pointOnSegment] = scene.makeMouseHelpers(590, 141)
|
||||||
|
const [profileEnd] = scene.makeMouseHelpers(970, 105)
|
||||||
|
const profileEndMv = scene.makeMouseHelpers(951, 101)[1]
|
||||||
|
const [newProfileEnd] = scene.makeMouseHelpers(764, 104)
|
||||||
|
const dragSegmentTo = scene.makeMouseHelpers(850, 104)[1]
|
||||||
|
|
||||||
|
const rectHandle = scene.makeMouseHelpers(901, 150)[1]
|
||||||
|
const rectDragTo = scene.makeMouseHelpers(901, 180)[1]
|
||||||
|
|
||||||
|
const circleEdge = scene.makeMouseHelpers(691, 331)[1]
|
||||||
|
const dragCircleTo = scene.makeMouseHelpers(720, 331)[1]
|
||||||
|
|
||||||
|
const [rectStart] = scene.makeMouseHelpers(794, 322)
|
||||||
|
const [rectEnd] = scene.makeMouseHelpers(757, 395)
|
||||||
|
|
||||||
|
await test2.step('enter sketch and setup', async () => {
|
||||||
|
await pointOnSegment({ shouldDbClick: true })
|
||||||
|
await app.page.waitForTimeout(600)
|
||||||
|
|
||||||
|
await toolbar.lineBtn.click()
|
||||||
|
await app.page.waitForTimeout(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test2.step('extend existing profile', async () => {
|
||||||
|
await profileEnd()
|
||||||
|
await app.page.waitForTimeout(100)
|
||||||
|
await newProfileEnd()
|
||||||
|
await editor.expectEditor.toContain(`|> line([-11.4, 0.71], %)`)
|
||||||
|
await toolbar.lineBtn.click()
|
||||||
|
await app.page.waitForTimeout(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test2.step('edit existing profile', async () => {
|
||||||
|
await profileEndMv()
|
||||||
|
await app.page.mouse.down()
|
||||||
|
await dragSegmentTo()
|
||||||
|
await app.page.mouse.up()
|
||||||
|
await editor.expectEditor.toContain(`line([4.16, -4.51], %)`)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test2.step('edit existing rect', async () => {
|
||||||
|
await rectHandle()
|
||||||
|
await app.page.mouse.down()
|
||||||
|
await rectDragTo()
|
||||||
|
await app.page.mouse.up()
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`angledLine([-7, 10.2], %, $rectangleSegmentA001)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test2.step('edit existing circl', async () => {
|
||||||
|
await circleEdge()
|
||||||
|
await app.page.mouse.down()
|
||||||
|
await dragCircleTo()
|
||||||
|
await app.page.mouse.up()
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`profile003 = circle({ center = [6.92, -4.2], radius = 4.77 }, sketch001)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test2.step('add new profile', async () => {
|
||||||
|
await toolbar.rectangleBtn.click()
|
||||||
|
await app.page.waitForTimeout(100)
|
||||||
|
await rectStart()
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`profile004 = startProfileAt([15.62, -3.83], sketch001)`
|
||||||
|
)
|
||||||
|
await app.page.waitForTimeout(100)
|
||||||
|
await rectEnd()
|
||||||
|
await editor.expectEditor
|
||||||
|
.toContain(`|> angledLine([180, 1.97], %, $rectangleSegmentA002)
|
||||||
|
|> angledLine([
|
||||||
|
segAng(rectangleSegmentA002) + 90,
|
||||||
|
3.88
|
||||||
|
], %)
|
||||||
|
|> angledLine([
|
||||||
|
segAng(rectangleSegmentA002),
|
||||||
|
-segLen(rectangleSegmentA002)
|
||||||
|
], %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
test2(
|
||||||
|
'Can delete a profile in the editor while is sketch mode, and sketch mode does not break, can ctrl+z to undo after constraint with variable was added',
|
||||||
|
async ({ app, scene, toolbar, editor, cmdBar }) => {
|
||||||
|
await app.initialise(`sketch001 = startSketchOn('XZ')
|
||||||
|
profile001 = startProfileAt([6.24, 4.54], sketch001)
|
||||||
|
|> line([-0.41, 6.99], %)
|
||||||
|
|> line([8.61, 0.74], %)
|
||||||
|
|> line([10.99, -5.22], %)
|
||||||
|
profile002 = startProfileAt([11.19, 5.02], sketch001)
|
||||||
|
|> angledLine([0, 10.78], %, $rectangleSegmentA001)
|
||||||
|
|> angledLine([
|
||||||
|
segAng(rectangleSegmentA001) - 90,
|
||||||
|
4.14
|
||||||
|
], %)
|
||||||
|
|> angledLine([
|
||||||
|
segAng(rectangleSegmentA001),
|
||||||
|
-segLen(rectangleSegmentA001)
|
||||||
|
], %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)
|
||||||
|
profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001)
|
||||||
|
`)
|
||||||
|
|
||||||
|
const [pointOnSegment] = scene.makeMouseHelpers(590, 141)
|
||||||
|
const [segment1Click] = scene.makeMouseHelpers(616, 131)
|
||||||
|
const sketchIsDrawnProperly = async () => {
|
||||||
|
await test2.step(
|
||||||
|
'check the sketch is still drawn properly',
|
||||||
|
async () => {
|
||||||
|
await app.page.waitForTimeout(200)
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
[255, 255, 255],
|
||||||
|
{ x: 617, y: 163 },
|
||||||
|
15
|
||||||
|
)
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
[255, 255, 255],
|
||||||
|
{ x: 629, y: 331 },
|
||||||
|
15
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await test2.step('enter sketch and setup', async () => {
|
||||||
|
await pointOnSegment({ shouldDbClick: true })
|
||||||
|
await app.page.waitForTimeout(600)
|
||||||
|
|
||||||
|
await toolbar.lineBtn.click()
|
||||||
|
await app.page.waitForTimeout(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test2.step('select and delete code for a profile', async () => {})
|
||||||
|
await app.page.getByText('close(%)').click()
|
||||||
|
await app.page.keyboard.down('Shift')
|
||||||
|
for (let i = 0; i < 11; i++) {
|
||||||
|
await app.page.keyboard.press('ArrowUp')
|
||||||
|
}
|
||||||
|
await app.page.keyboard.press('Home')
|
||||||
|
await app.page.keyboard.up('Shift')
|
||||||
|
await app.page.keyboard.press('Backspace')
|
||||||
|
|
||||||
|
await sketchIsDrawnProperly()
|
||||||
|
|
||||||
|
await test2.step('add random new var between profiles', async () => {
|
||||||
|
await app.page.keyboard.type('myVar = 5')
|
||||||
|
await app.page.keyboard.press('Enter')
|
||||||
|
await app.page.waitForTimeout(600)
|
||||||
|
})
|
||||||
|
|
||||||
|
await sketchIsDrawnProperly()
|
||||||
|
|
||||||
|
await test2.step(
|
||||||
|
'Adding a constraint with a variable, and than ctrl-z-ing which will remove the variable again does not break sketch mode',
|
||||||
|
async () => {
|
||||||
|
await expect(async () => {
|
||||||
|
await segment1Click()
|
||||||
|
await editor.expectState({
|
||||||
|
diagnostics: [],
|
||||||
|
activeLines: ['|>line([-0.41,6.99],%)'],
|
||||||
|
highlightedCode: 'line([-0.41,6.99],%)',
|
||||||
|
})
|
||||||
|
}).toPass({ timeout: 5_000, intervals: [500] })
|
||||||
|
|
||||||
|
await toolbar.lengthConstraintBtn.click()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await editor.expectEditor.toContain('length001 = 7')
|
||||||
|
|
||||||
|
// wait for execute defer
|
||||||
|
await app.page.waitForTimeout(600)
|
||||||
|
await sketchIsDrawnProperly()
|
||||||
|
|
||||||
|
await app.page.keyboard.down('Meta')
|
||||||
|
await app.page.keyboard.press('KeyZ')
|
||||||
|
await app.page.keyboard.up('Meta')
|
||||||
|
|
||||||
|
await editor.expectEditor.not.toContain('length001 = 7')
|
||||||
|
await sketchIsDrawnProperly()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
test2(
|
||||||
|
'can enter sketch when there is an extrude',
|
||||||
|
async ({ app, scene, toolbar }) => {
|
||||||
|
await app.initialise(`sketch001 = startSketchOn('XZ')
|
||||||
|
profile001 = startProfileAt([-63.43, 193.08], sketch001)
|
||||||
|
|> line([168.52, 149.87], %)
|
||||||
|
|> line([190.29, -39.18], %)
|
||||||
|
|> tangentialArcTo([319.63, 129.65], %)
|
||||||
|
|> line([-217.65, -21.76], %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)
|
||||||
|
profile003 = startProfileAt([16.79, 38.24], sketch001)
|
||||||
|
|> angledLine([0, 182.82], %, $rectangleSegmentA001)
|
||||||
|
|> angledLine([
|
||||||
|
segAng(rectangleSegmentA001) - 90,
|
||||||
|
105.71
|
||||||
|
], %)
|
||||||
|
|> angledLine([
|
||||||
|
segAng(rectangleSegmentA001),
|
||||||
|
-segLen(rectangleSegmentA001)
|
||||||
|
], %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)
|
||||||
|
profile004 = circle({
|
||||||
|
center = [280.45, 47.57],
|
||||||
|
radius = 55.26
|
||||||
|
}, sketch001)
|
||||||
|
extrude002 = extrude(50, profile001)
|
||||||
|
extrude001 = extrude(5, profile003)
|
||||||
|
`)
|
||||||
|
const [pointOnSegment] = scene.makeMouseHelpers(574, 207)
|
||||||
|
|
||||||
|
await pointOnSegment()
|
||||||
|
await toolbar.editSketch()
|
||||||
|
// wait for engine animation
|
||||||
|
await app.page.waitForTimeout(600)
|
||||||
|
|
||||||
|
await test2.step('check the sketch is still drawn properly', async () => {
|
||||||
|
await scene.expectPixelColor([255, 255, 255], { x: 591, y: 167 }, 15)
|
||||||
|
await scene.expectPixelColor([255, 255, 255], { x: 638, y: 222 }, 15)
|
||||||
|
await scene.expectPixelColor([255, 255, 255], { x: 756, y: 214 }, 15)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
test2(
|
||||||
|
'exit new sketch without drawing anything should not be a problem',
|
||||||
|
async ({ app, scene, toolbar, editor, cmdBar }) => {
|
||||||
|
await app.initialise(`myVar = 5`)
|
||||||
|
const [selectXZPlane] = scene.makeMouseHelpers(650, 150)
|
||||||
|
|
||||||
|
await toolbar.startSketchPlaneSelection()
|
||||||
|
await selectXZPlane()
|
||||||
|
// timeout wait for engine animation is unavoidable
|
||||||
|
await app.page.waitForTimeout(600)
|
||||||
|
|
||||||
|
await editor.expectEditor.toContain(`sketch001 = startSketchOn('XZ')`)
|
||||||
|
await toolbar.exitSketchBtn.click()
|
||||||
|
|
||||||
|
await editor.expectEditor.not.toContain(`sketch001 = startSketchOn('XZ')`)
|
||||||
|
|
||||||
|
await test2.step(
|
||||||
|
"still renders code, hasn't got into a weird state",
|
||||||
|
async () => {
|
||||||
|
await editor.replaceCode(
|
||||||
|
'myVar = 5',
|
||||||
|
`myVar = 5
|
||||||
|
sketch001 = startSketchOn('XZ')
|
||||||
|
profile001 = circle({
|
||||||
|
center = [12.41, 3.87],
|
||||||
|
radius = myVar
|
||||||
|
}, sketch001)`
|
||||||
|
)
|
||||||
|
|
||||||
|
await scene.expectPixelColor([255, 255, 255], { x: 633, y: 211 }, 15)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
test2(
|
||||||
|
'A sketch with only "startProfileAt" and no segments should still be able to be continued ',
|
||||||
|
async ({ app, scene, toolbar, editor }) => {
|
||||||
|
await app.initialise(`sketch001 = startSketchOn('XZ')
|
||||||
|
profile001 = startProfileAt([85.19, 338.59], sketch001)
|
||||||
|
|> line([213.3, -94.52], %)
|
||||||
|
|> line([-230.09, -55.34], %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)
|
||||||
|
sketch002 = startSketchOn('XY')
|
||||||
|
profile002 = startProfileAt([85.81, 52.55], sketch002)
|
||||||
|
|
||||||
|
`)
|
||||||
|
const [startProfileAt] = scene.makeMouseHelpers(606, 184)
|
||||||
|
const [nextPoint] = scene.makeMouseHelpers(763, 130)
|
||||||
|
await app.page
|
||||||
|
.getByText('startProfileAt([85.81, 52.55], sketch002)')
|
||||||
|
.click()
|
||||||
|
await toolbar.editSketch()
|
||||||
|
// timeout wait for engine animation is unavoidable
|
||||||
|
await app.page.waitForTimeout(600)
|
||||||
|
|
||||||
|
// equip line tool
|
||||||
|
await toolbar.lineBtn.click()
|
||||||
|
await app.page.waitForTimeout(100)
|
||||||
|
await startProfileAt()
|
||||||
|
await app.page.waitForTimeout(100)
|
||||||
|
await nextPoint()
|
||||||
|
await editor.expectEditor.toContain(`|> line([126.05, 44.12], %)`)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
test2(
|
||||||
|
'old style sketch all in one pipe (with extrude) will break up to allow users to add a new profile to the same sketch',
|
||||||
|
async ({ app, scene, toolbar, editor }) => {
|
||||||
|
await app.initialise(`thePart = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([7.53, 10.51], %)
|
||||||
|
|> line([12.54, 1.83], %)
|
||||||
|
|> line([6.65, -6.91], %)
|
||||||
|
|> line([-6.31, -8.69], %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)
|
||||||
|
extrude001 = extrude(75, thePart)
|
||||||
|
`)
|
||||||
|
const [objClick] = scene.makeMouseHelpers(565, 343)
|
||||||
|
const [profilePoint1] = scene.makeMouseHelpers(609, 289)
|
||||||
|
const [profilePoint2] = scene.makeMouseHelpers(714, 389)
|
||||||
|
|
||||||
|
await test2.step('enter sketch and setup', async () => {
|
||||||
|
await objClick()
|
||||||
|
await toolbar.editSketch()
|
||||||
|
// timeout wait for engine animation is unavoidable
|
||||||
|
await app.page.waitForTimeout(600)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test2.step(
|
||||||
|
'expect code to match initial conditions still',
|
||||||
|
async () => {
|
||||||
|
await editor.expectEditor.toContain(`thePart = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([7.53, 10.51], %)`)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await test2.step(
|
||||||
|
'equiping the line tool should break up the pipe expression',
|
||||||
|
async () => {
|
||||||
|
await toolbar.lineBtn.click()
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`sketch001 = startSketchOn('XZ')thePart = startProfileAt([7.53, 10.51], sketch001)`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await test2.step(
|
||||||
|
'can continue on to add a new profile to this sketch',
|
||||||
|
async () => {
|
||||||
|
await profilePoint1()
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`profile001 = startProfileAt([19.77, -7.08], sketch001)`
|
||||||
|
)
|
||||||
|
await profilePoint2()
|
||||||
|
await editor.expectEditor.toContain(`|> line([19.05, -18.14], %)`)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
@ -446,8 +446,7 @@ test(
|
|||||||
|
|
||||||
const startXPx = 600
|
const startXPx = 600
|
||||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
code += `
|
code += `profile001 = startProfileAt([7.19, -9.7], sketch001)`
|
||||||
|> startProfileAt([7.19, -9.7], %)`
|
|
||||||
await expect(page.locator('.cm-content')).toHaveText(code)
|
await expect(page.locator('.cm-content')).toHaveText(code)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
@ -469,6 +468,10 @@ test(
|
|||||||
.getByRole('button', { name: 'arc Tangential Arc', exact: true })
|
.getByRole('button', { name: 'arc Tangential Arc', exact: true })
|
||||||
.click()
|
.click()
|
||||||
|
|
||||||
|
// click to continue profile
|
||||||
|
await page.mouse.move(813, 392, { steps: 10 })
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 })
|
await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 })
|
||||||
|
|
||||||
await page.waitForTimeout(1000)
|
await page.waitForTimeout(1000)
|
||||||
@ -591,8 +594,7 @@ test(
|
|||||||
mask: [page.getByTestId('model-state-indicator')],
|
mask: [page.getByTestId('model-state-indicator')],
|
||||||
})
|
})
|
||||||
await expect(page.locator('.cm-content')).toHaveText(
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
`sketch001 = startSketchOn('XZ')
|
`sketch001 = startSketchOn('XZ')profile001 = circle({ center = [14.44, -2.44], radius = 1 }, sketch001)`
|
||||||
|> circle({ center = [14.44, -2.44], radius = 1 }, %)`
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -636,8 +638,7 @@ test.describe(
|
|||||||
|
|
||||||
const startXPx = 600
|
const startXPx = 600
|
||||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
code += `
|
code += `profile001 = startProfileAt([7.19, -9.7], sketch001)`
|
||||||
|> startProfileAt([7.19, -9.7], %)`
|
|
||||||
await expect(u.codeLocator).toHaveText(code)
|
await expect(u.codeLocator).toHaveText(code)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
@ -655,6 +656,10 @@ test.describe(
|
|||||||
.click()
|
.click()
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
// click to continue profile
|
||||||
|
await page.mouse.click(813, 392)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
|
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
|
||||||
|
|
||||||
code += `
|
code += `
|
||||||
@ -741,8 +746,7 @@ test.describe(
|
|||||||
|
|
||||||
const startXPx = 600
|
const startXPx = 600
|
||||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
code += `
|
code += `profile001 = startProfileAt([182.59, -246.32], sketch001)`
|
||||||
|> startProfileAt([182.59, -246.32], %)`
|
|
||||||
await expect(u.codeLocator).toHaveText(code)
|
await expect(u.codeLocator).toHaveText(code)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
@ -760,6 +764,10 @@ test.describe(
|
|||||||
.click()
|
.click()
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
// click to continue profile
|
||||||
|
await page.mouse.click(813, 392)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
|
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
|
||||||
|
|
||||||
code += `
|
code += `
|
||||||
@ -950,7 +958,75 @@ test(
|
|||||||
|
|
||||||
test.describe('Grid visibility', { tag: '@snapshot' }, () => {
|
test.describe('Grid visibility', { tag: '@snapshot' }, () => {
|
||||||
// FIXME: Skip on macos its being weird.
|
// FIXME: Skip on macos its being weird.
|
||||||
test.skip(process.platform === 'darwin', 'Skip on macos')
|
// test.skip(process.platform === 'darwin', 'Skip on macos')
|
||||||
|
|
||||||
|
test('Grid turned off to on via command bar', async ({ page }) => {
|
||||||
|
const u = await getUtils(page)
|
||||||
|
const stream = page.getByTestId('stream')
|
||||||
|
const mask = [
|
||||||
|
page.locator('#app-header'),
|
||||||
|
page.locator('#sidebar-top-ribbon'),
|
||||||
|
page.locator('#sidebar-bottom-ribbon'),
|
||||||
|
]
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
await u.openDebugPanel()
|
||||||
|
// wait for execution done
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-message-type="execution-done"]')
|
||||||
|
).toHaveCount(1)
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
await u.closeKclCodePanel()
|
||||||
|
// TODO: Find a way to truly know that the objects have finished
|
||||||
|
// rendering, because an execution-done message is not sufficient.
|
||||||
|
await page.waitForTimeout(1000)
|
||||||
|
|
||||||
|
// Open the command bar.
|
||||||
|
await page
|
||||||
|
.getByRole('button', { name: 'Commands', exact: false })
|
||||||
|
.or(page.getByRole('button', { name: '⌘K' }))
|
||||||
|
.click()
|
||||||
|
const commandName = 'show scale grid'
|
||||||
|
const commandOption = page.getByRole('option', {
|
||||||
|
name: commandName,
|
||||||
|
exact: false,
|
||||||
|
})
|
||||||
|
const cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||||
|
// This selector changes after we set the setting
|
||||||
|
await cmdSearchBar.fill(commandName)
|
||||||
|
await expect(commandOption).toBeVisible()
|
||||||
|
await commandOption.click()
|
||||||
|
|
||||||
|
const toggleInput = page.getByPlaceholder('Off')
|
||||||
|
await expect(toggleInput).toBeVisible()
|
||||||
|
await expect(toggleInput).toBeFocused()
|
||||||
|
|
||||||
|
// Select On
|
||||||
|
await page.keyboard.press('ArrowDown')
|
||||||
|
await expect(page.getByRole('option', { name: 'Off' })).toHaveAttribute(
|
||||||
|
'data-headlessui-state',
|
||||||
|
'active selected'
|
||||||
|
)
|
||||||
|
await page.keyboard.press('ArrowUp')
|
||||||
|
await expect(page.getByRole('option', { name: 'On' })).toHaveAttribute(
|
||||||
|
'data-headlessui-state',
|
||||||
|
'active'
|
||||||
|
)
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
|
||||||
|
// Check the toast appeared
|
||||||
|
await expect(
|
||||||
|
page.getByText(`Set show scale grid to "true" as a user default`)
|
||||||
|
).toBeVisible()
|
||||||
|
|
||||||
|
await expect(stream).toHaveScreenshot({
|
||||||
|
maxDiffPixels: 100,
|
||||||
|
mask,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test('Grid turned off', async ({ page }) => {
|
test('Grid turned off', async ({ page }) => {
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
@ -1096,3 +1172,109 @@ test.fixme('theme persists', async ({ page, context }) => {
|
|||||||
maxDiffPixels: 100,
|
maxDiffPixels: 100,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test.describe('code color goober', { tag: '@snapshot' }, () => {
|
||||||
|
test('code color goober', async ({ page, context }) => {
|
||||||
|
const u = await getUtils(page)
|
||||||
|
await context.addInitScript(async () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`// Create a pipe using a sweep.
|
||||||
|
|
||||||
|
// Create a path for the sweep.
|
||||||
|
sweepPath = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([0.05, 0.05], %)
|
||||||
|
|> line([0, 7], %)
|
||||||
|
|> tangentialArc({ offset = 90, radius = 5 }, %)
|
||||||
|
|> line([-3, 0], %)
|
||||||
|
|> tangentialArc({ offset = -90, radius = 5 }, %)
|
||||||
|
|> line([0, 7], %)
|
||||||
|
|
||||||
|
sweepSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([2, 0], %)
|
||||||
|
|> arc({
|
||||||
|
angleEnd = 360,
|
||||||
|
angleStart = 0,
|
||||||
|
radius = 2
|
||||||
|
}, %)
|
||||||
|
|> sweep({
|
||||||
|
path = sweepPath,
|
||||||
|
}, %)
|
||||||
|
|> appearance({
|
||||||
|
color = "#bb00ff",
|
||||||
|
metalness = 90,
|
||||||
|
roughness = 90
|
||||||
|
}, %)
|
||||||
|
`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1200, height: 1000 })
|
||||||
|
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
await u.clearAndCloseDebugPanel()
|
||||||
|
|
||||||
|
await expect(page, 'expect small color widget').toHaveScreenshot({
|
||||||
|
maxDiffPixels: 100,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('code color goober opening window', async ({ page, context }) => {
|
||||||
|
const u = await getUtils(page)
|
||||||
|
await context.addInitScript(async () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`// Create a pipe using a sweep.
|
||||||
|
|
||||||
|
// Create a path for the sweep.
|
||||||
|
sweepPath = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([0.05, 0.05], %)
|
||||||
|
|> line([0, 7], %)
|
||||||
|
|> tangentialArc({ offset = 90, radius = 5 }, %)
|
||||||
|
|> line([-3, 0], %)
|
||||||
|
|> tangentialArc({ offset = -90, radius = 5 }, %)
|
||||||
|
|> line([0, 7], %)
|
||||||
|
|
||||||
|
sweepSketch = startSketchOn('XY')
|
||||||
|
|> startProfileAt([2, 0], %)
|
||||||
|
|> arc({
|
||||||
|
angleEnd = 360,
|
||||||
|
angleStart = 0,
|
||||||
|
radius = 2
|
||||||
|
}, %)
|
||||||
|
|> sweep({
|
||||||
|
path = sweepPath,
|
||||||
|
}, %)
|
||||||
|
|> appearance({
|
||||||
|
color = "#bb00ff",
|
||||||
|
metalness = 90,
|
||||||
|
roughness = 90
|
||||||
|
}, %)
|
||||||
|
`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1200, height: 1000 })
|
||||||
|
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
await u.clearAndCloseDebugPanel()
|
||||||
|
|
||||||
|
await expect(page.locator('.cm-css-color-picker-wrapper')).toBeVisible()
|
||||||
|
|
||||||
|
// Click the color widget
|
||||||
|
await page.locator('.cm-css-color-picker-wrapper input').click()
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page,
|
||||||
|
'expect small color widget to have window open'
|
||||||
|
).toHaveScreenshot({
|
||||||
|
maxDiffPixels: 100,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 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: 52 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 52 KiB |
After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
After Width: | Height: | Size: 144 KiB |
After Width: | Height: | Size: 130 KiB |
After Width: | Height: | Size: 139 KiB |
After Width: | Height: | Size: 124 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 40 KiB |
@ -14,7 +14,7 @@ export const TEST_SETTINGS = {
|
|||||||
},
|
},
|
||||||
modeling: {
|
modeling: {
|
||||||
defaultUnit: 'in',
|
defaultUnit: 'in',
|
||||||
mouseControls: 'KittyCAD',
|
mouseControls: 'Zoo',
|
||||||
cameraProjection: 'perspective',
|
cameraProjection: 'perspective',
|
||||||
showDebugPanel: true,
|
showDebugPanel: true,
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { test, expect } from '@playwright/test'
|
import { test, expect } from '@playwright/test'
|
||||||
|
|
||||||
import { commonPoints, getUtils, setup, tearDown } from './test-utils'
|
import { commonPoints, getUtils, setup, tearDown } from './test-utils'
|
||||||
|
import { uuidv4 } from 'lib/utils'
|
||||||
|
import { EngineCommand } from 'lang/std/artifactGraph'
|
||||||
|
|
||||||
test.beforeEach(async ({ context, page }, testInfo) => {
|
test.beforeEach(async ({ context, page }, testInfo) => {
|
||||||
await setup(context, page, testInfo)
|
await setup(context, page, testInfo)
|
||||||
@ -130,17 +132,16 @@ test.describe('Test network and connection issues', () => {
|
|||||||
|
|
||||||
const startXPx = 600
|
const startXPx = 600
|
||||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)`
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)`)
|
)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|
||||||
|> xLine(${commonPoints.num1}, %)`)
|
|> xLine(${commonPoints.num1}, %)`)
|
||||||
|
|
||||||
// Expect the network to be up
|
// Expect the network to be up
|
||||||
@ -188,7 +189,9 @@ test.describe('Test network and connection issues', () => {
|
|||||||
await page.mouse.click(100, 100)
|
await page.mouse.click(100, 100)
|
||||||
|
|
||||||
// select a line
|
// select a line
|
||||||
await page.getByText(`startProfileAt(${commonPoints.startAt}, %)`).click()
|
await page
|
||||||
|
.getByText(`startProfileAt(${commonPoints.startAt}, sketch001)`)
|
||||||
|
.click()
|
||||||
|
|
||||||
// enter sketch again
|
// enter sketch again
|
||||||
await u.doAndWaitForCmd(
|
await u.doAndWaitForCmd(
|
||||||
@ -202,11 +205,36 @@ test.describe('Test network and connection issues', () => {
|
|||||||
|
|
||||||
await page.waitForTimeout(150)
|
await page.waitForTimeout(150)
|
||||||
|
|
||||||
|
const camCommand: EngineCommand = {
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_look_at',
|
||||||
|
center: { x: 109, y: 0, z: -152 },
|
||||||
|
vantage: { x: 115, y: -505, z: -152 },
|
||||||
|
up: { x: 0, y: 0, z: 1 },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const updateCamCommand: EngineCommand = {
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_get_settings',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await u.sendCustomCmd(camCommand)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await u.sendCustomCmd(updateCamCommand)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
// click to continue profile
|
||||||
|
await page.mouse.click(1007, 400)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
// Ensure we can continue sketching
|
// Ensure we can continue sketching
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||||
await expect.poll(u.normalisedEditorCode)
|
await expect.poll(u.normalisedEditorCode)
|
||||||
.toBe(`sketch001 = startSketchOn('XZ')
|
.toBe(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt([12.34, -12.34], %)
|
profile001 = startProfileAt([12.34, -12.34], sketch001)
|
||||||
|> xLine(12.34, %)
|
|> xLine(12.34, %)
|
||||||
|> line([-12.34, 12.34], %)
|
|> line([-12.34, 12.34], %)
|
||||||
|
|
||||||
@ -216,7 +244,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], %)
|
profile001 = startProfileAt([12.34, -12.34], sketch001)
|
||||||
|> xLine(12.34, %)
|
|> xLine(12.34, %)
|
||||||
|> line([-12.34, 12.34], %)
|
|> line([-12.34, 12.34], %)
|
||||||
|> xLine(-12.34, %)
|
|> xLine(-12.34, %)
|
||||||
|
@ -479,4 +479,26 @@ test.describe('Testing Camera Movement', () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Right-click opens context menu when not dragged', async ({ page }) => {
|
||||||
|
const u = await getUtils(page)
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
await test.step(`The menu should not show if we drag the mouse`, async () => {
|
||||||
|
await page.mouse.move(900, 200)
|
||||||
|
await page.mouse.down({ button: 'right' })
|
||||||
|
await page.mouse.move(900, 300)
|
||||||
|
await page.mouse.up({ button: 'right' })
|
||||||
|
|
||||||
|
await expect(page.getByTestId('view-controls-menu')).not.toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`The menu should show if we don't drag the mouse`, async () => {
|
||||||
|
await page.mouse.move(900, 200)
|
||||||
|
await page.mouse.down({ button: 'right' })
|
||||||
|
await page.mouse.up({ button: 'right' })
|
||||||
|
|
||||||
|
await expect(page.getByTestId('view-controls-menu')).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -17,16 +17,26 @@ test.describe('Testing constraints', () => {
|
|||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'persistCode',
|
'persistCode',
|
||||||
`sketch001 = startSketchOn('XY')
|
`sketch001 = startSketchOn('XY')
|
||||||
|> startProfileAt([-10, -10], %)
|
|> startProfileAt([-10, -10], %)
|
||||||
|> line([20, 0], %)
|
|> line([20, 0], %)
|
||||||
|> line([0, 20], %)
|
|> line([0, 20], %)
|
||||||
|> xLine(-20, %)
|
|> xLine(-20, %)
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
// constants and locators
|
||||||
|
const lengthValue = {
|
||||||
|
old: '20',
|
||||||
|
new: '25',
|
||||||
|
}
|
||||||
|
const cmdBarKclInput = page
|
||||||
|
.getByTestId('cmd-bar-arg-value')
|
||||||
|
.getByRole('textbox')
|
||||||
|
const cmdBarSubmitButton = page.getByRole('button', {
|
||||||
|
name: 'arrow right Continue',
|
||||||
|
})
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
@ -36,26 +46,26 @@ test.describe('Testing constraints', () => {
|
|||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
// Click the line of code for line.
|
// Click the line of code for line.
|
||||||
await page.getByText(`line([0, 20], %)`).click() // TODO remove this and reinstate // await topHorzSegmentClick()
|
// TODO remove this and reinstate `await topHorzSegmentClick()`
|
||||||
|
await page.getByText(`line([0, ${lengthValue.old}], %)`).click()
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
// enter sketch again
|
// enter sketch again
|
||||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||||
await page.waitForTimeout(500) // wait for animation
|
await page.waitForTimeout(500) // wait for animation
|
||||||
|
|
||||||
const startXPx = 500
|
|
||||||
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
|
|
||||||
await page.keyboard.down('Shift')
|
|
||||||
await page.mouse.click(834, 244)
|
|
||||||
await page.keyboard.up('Shift')
|
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByRole('button', { name: 'dimension Length', exact: true })
|
.getByRole('button', { name: 'dimension Length', exact: true })
|
||||||
.click()
|
.click()
|
||||||
await page.getByText('Add constraining value').click()
|
await expect(cmdBarKclInput).toHaveText('20')
|
||||||
|
await cmdBarKclInput.fill(lengthValue.new)
|
||||||
|
await expect(
|
||||||
|
page.getByText(`Can't calculate`),
|
||||||
|
`Something went wrong with the KCL expression evaluation`
|
||||||
|
).not.toBeVisible()
|
||||||
|
await cmdBarSubmitButton.click()
|
||||||
|
|
||||||
await expect(page.locator('.cm-content')).toHaveText(
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
`length001 = 20sketch001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> angledLine([90, length001], %) |> xLine(-20, %)`
|
`length001 = ${lengthValue.new}sketch001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> angledLine([90, length001], %) |> xLine(-20, %)`
|
||||||
)
|
)
|
||||||
|
|
||||||
// Make sure we didn't pop out of sketch mode.
|
// Make sure we didn't pop out of sketch mode.
|
||||||
@ -66,7 +76,6 @@ test.describe('Testing constraints', () => {
|
|||||||
await page.waitForTimeout(500) // wait for animation
|
await page.waitForTimeout(500) // wait for animation
|
||||||
|
|
||||||
// Exit sketch
|
// Exit sketch
|
||||||
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
|
|
||||||
await page.keyboard.press('Escape')
|
await page.keyboard.press('Escape')
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Exit Sketch' })
|
page.getByRole('button', { name: 'Exit Sketch' })
|
||||||
@ -524,7 +533,7 @@ part002 = startSketchOn('XZ')
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
test.describe('Test Angle/Length constraint single selection', () => {
|
test.describe('Test Angle constraint single selection', () => {
|
||||||
const cases = [
|
const cases = [
|
||||||
{
|
{
|
||||||
testName: 'Angle - Add variable',
|
testName: 'Angle - Add variable',
|
||||||
@ -538,18 +547,6 @@ part002 = startSketchOn('XZ')
|
|||||||
constraint: 'angle',
|
constraint: 'angle',
|
||||||
value: '83, 78.33',
|
value: '83, 78.33',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
testName: 'Length - Add variable',
|
|
||||||
addVariable: true,
|
|
||||||
constraint: 'length',
|
|
||||||
value: '83, length001',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
testName: 'Length - No variable',
|
|
||||||
addVariable: false,
|
|
||||||
constraint: 'length',
|
|
||||||
value: '83, 78.33',
|
|
||||||
},
|
|
||||||
] as const
|
] as const
|
||||||
for (const { testName, addVariable, value, constraint } of cases) {
|
for (const { testName, addVariable, value, constraint } of cases) {
|
||||||
test(`${testName}`, async ({ page }) => {
|
test(`${testName}`, async ({ page }) => {
|
||||||
@ -608,6 +605,90 @@ part002 = startSketchOn('XZ')
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
test.describe('Test Length constraint single selection', () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
testName: 'Length - Add variable',
|
||||||
|
addVariable: true,
|
||||||
|
constraint: 'length',
|
||||||
|
value: '83, length001',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: 'Length - No variable',
|
||||||
|
addVariable: false,
|
||||||
|
constraint: 'length',
|
||||||
|
value: '83, 78.33',
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
for (const { testName, addVariable, value, constraint } of cases) {
|
||||||
|
test(`${testName}`, async ({ page }) => {
|
||||||
|
// constants and locators
|
||||||
|
const cmdBarKclInput = page
|
||||||
|
.getByTestId('cmd-bar-arg-value')
|
||||||
|
.getByRole('textbox')
|
||||||
|
const cmdBarKclVariableNameInput =
|
||||||
|
page.getByPlaceholder('Variable name')
|
||||||
|
const cmdBarSubmitButton = page.getByRole('button', {
|
||||||
|
name: 'arrow right Continue',
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.addInitScript(async () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`yo = 5
|
||||||
|
part001 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([-7.54, -26.74], %)
|
||||||
|
|> line([74.36, 130.4], %)
|
||||||
|
|> line([78.92, -120.11], %)
|
||||||
|
|> line([9.16, 77.79], %)
|
||||||
|
|> line([51.19, 48.97], %)
|
||||||
|
part002 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([299.05, 231.45], %)
|
||||||
|
|> xLine(-425.34, %, $seg_what)
|
||||||
|
|> yLine(-264.06, %)
|
||||||
|
|> xLine(segLen(seg_what), %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
const u = await getUtils(page)
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
await page.getByText('line([74.36, 130.4], %)').click()
|
||||||
|
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||||
|
|
||||||
|
const line3 = await u.getSegmentBodyCoords(
|
||||||
|
`[data-overlay-index="${2}"]`
|
||||||
|
)
|
||||||
|
|
||||||
|
await page.mouse.click(line3.x, line3.y)
|
||||||
|
await page
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'Length: open menu',
|
||||||
|
})
|
||||||
|
.click()
|
||||||
|
await page.getByTestId('dropdown-constraint-' + constraint).click()
|
||||||
|
|
||||||
|
if (!addVariable) {
|
||||||
|
await test.step(`Clear the variable input`, async () => {
|
||||||
|
await cmdBarKclVariableNameInput.clear()
|
||||||
|
await cmdBarKclVariableNameInput.press('Backspace')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await expect(cmdBarKclInput).toHaveText('78.33')
|
||||||
|
await cmdBarSubmitButton.click()
|
||||||
|
|
||||||
|
const changedCode = `|> angledLine([${value}], %)`
|
||||||
|
await expect(page.locator('.cm-content')).toContainText(changedCode)
|
||||||
|
// checking active assures the cursor is where it should be
|
||||||
|
await expect(page.locator('.cm-activeLine')).toHaveText(changedCode)
|
||||||
|
|
||||||
|
// checking the count of the overlays is a good proxy check that the client sketch scene is in a good state
|
||||||
|
await expect(page.getByTestId('segment-overlay')).toHaveCount(4)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
test.describe('Many segments - no modal constraints', () => {
|
test.describe('Many segments - no modal constraints', () => {
|
||||||
const cases = [
|
const cases = [
|
||||||
{
|
{
|
||||||
@ -868,6 +949,15 @@ part002 = startSketchOn('XZ')
|
|||||||
|> line([3.13, -2.4], %)`
|
|> line([3.13, -2.4], %)`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// constants and locators
|
||||||
|
const cmdBarKclInput = page
|
||||||
|
.getByTestId('cmd-bar-arg-value')
|
||||||
|
.getByRole('textbox')
|
||||||
|
const cmdBarSubmitButton = page.getByRole('button', {
|
||||||
|
name: 'arrow right Continue',
|
||||||
|
})
|
||||||
|
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
|
||||||
@ -928,8 +1018,8 @@ part002 = startSketchOn('XZ')
|
|||||||
// await page.getByRole('button', { name: 'length', exact: true }).click()
|
// await page.getByRole('button', { name: 'length', exact: true }).click()
|
||||||
await page.getByTestId('dropdown-constraint-length').click()
|
await page.getByTestId('dropdown-constraint-length').click()
|
||||||
|
|
||||||
await page.getByLabel('length Value').fill('10')
|
await cmdBarKclInput.fill('10')
|
||||||
await page.getByRole('button', { name: 'Add constraining value' }).click()
|
await cmdBarSubmitButton.click()
|
||||||
|
|
||||||
activeLinesContent = await page.locator('.cm-activeLine').all()
|
activeLinesContent = await page.locator('.cm-activeLine').all()
|
||||||
await expect(activeLinesContent[0]).toHaveText(`|> xLine(length001, %)`)
|
await expect(activeLinesContent[0]).toHaveText(`|> xLine(length001, %)`)
|
||||||
|
@ -91,7 +91,14 @@ test.describe('Testing segment overlays', () => {
|
|||||||
await page.getByTestId('constraint-symbol-popover').count()
|
await page.getByTestId('constraint-symbol-popover').count()
|
||||||
).toBeGreaterThan(0)
|
).toBeGreaterThan(0)
|
||||||
await unconstrainedLocator.click()
|
await unconstrainedLocator.click()
|
||||||
await page.getByText('Add variable').click()
|
await expect(
|
||||||
|
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
|
||||||
|
).toBeFocused()
|
||||||
|
await page
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'arrow right Continue',
|
||||||
|
})
|
||||||
|
.click()
|
||||||
await expect(page.locator('.cm-content')).toContainText(expectFinal)
|
await expect(page.locator('.cm-content')).toContainText(expectFinal)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,7 +158,14 @@ test.describe('Testing segment overlays', () => {
|
|||||||
await page.getByTestId('constraint-symbol-popover').count()
|
await page.getByTestId('constraint-symbol-popover').count()
|
||||||
).toBeGreaterThan(0)
|
).toBeGreaterThan(0)
|
||||||
await unconstrainedLocator.click()
|
await unconstrainedLocator.click()
|
||||||
await page.getByText('Add variable').click()
|
await expect(
|
||||||
|
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
|
||||||
|
).toBeFocused()
|
||||||
|
await page
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'arrow right Continue',
|
||||||
|
})
|
||||||
|
.click()
|
||||||
await expect(page.locator('.cm-content')).toContainText(
|
await expect(page.locator('.cm-content')).toContainText(
|
||||||
expectAfterUnconstrained
|
expectAfterUnconstrained
|
||||||
)
|
)
|
||||||
|
@ -77,30 +77,31 @@ test.describe('Testing selections', () => {
|
|||||||
const startXPx = 600
|
const startXPx = 600
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)`
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)`)
|
)
|
||||||
|
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|
||||||
|> xLine(${commonPoints.num1}, %)`)
|
|> xLine(${commonPoints.num1}, %)`)
|
||||||
|
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
commonPoints.startAt
|
||||||
|
}, sketch001)
|
||||||
|> xLine(${commonPoints.num1}, %)
|
|> xLine(${commonPoints.num1}, %)
|
||||||
|> yLine(${commonPoints.num1 + 0.01}, %)`)
|
|> yLine(${commonPoints.num1 + 0.01}, %)`)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
commonPoints.startAt
|
||||||
|
}, sketch001)
|
||||||
|> xLine(${commonPoints.num1}, %)
|
|> xLine(${commonPoints.num1}, %)
|
||||||
|> yLine(${commonPoints.num1 + 0.01}, %)
|
|> yLine(${commonPoints.num1 + 0.01}, %)
|
||||||
|> xLine(${commonPoints.num2 * -1}, %)`)
|
|> xLine(${commonPoints.num2 * -1}, %)`)
|
||||||
@ -330,6 +331,28 @@ part009 = startSketchOn('XY')
|
|||||||
|> angledLineToX({ angle = 60, to = pipeLargeDia }, %)
|
|> angledLineToX({ angle = 60, to = pipeLargeDia }, %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
rev = revolve({ axis = 'y' }, part009)
|
rev = revolve({ axis = 'y' }, part009)
|
||||||
|
sketch006 = startSketchOn('XY')
|
||||||
|
profile001 = circle({
|
||||||
|
center = [42.91, -70.42],
|
||||||
|
radius = 17.96
|
||||||
|
}, sketch006)
|
||||||
|
profile002 = startProfileAt([86.92, -63.81], sketch006)
|
||||||
|
|> angledLine([0, 63.81], %, $rectangleSegmentA001)
|
||||||
|
|> angledLine([
|
||||||
|
segAng(rectangleSegmentA001) - 90,
|
||||||
|
17.05
|
||||||
|
], %)
|
||||||
|
|> angledLine([
|
||||||
|
segAng(rectangleSegmentA001),
|
||||||
|
-segLen(rectangleSegmentA001)
|
||||||
|
], %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)
|
||||||
|
profile003 = startProfileAt([40.16, -120.48], sketch006)
|
||||||
|
|> line([26.95, 24.21], %)
|
||||||
|
|> line([20.91, -28.61], %)
|
||||||
|
|> line([32.46, 18.71], %)
|
||||||
|
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
}, KCL_DEFAULT_LENGTH)
|
}, KCL_DEFAULT_LENGTH)
|
||||||
@ -362,9 +385,10 @@ rev = revolve({ axis = 'y' }, part009)
|
|||||||
})
|
})
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
const revolve = { x: 646, y: 248 }
|
const revolve = { x: 635, y: 253 }
|
||||||
const parentExtrude = { x: 915, y: 133 }
|
const parentExtrude = { x: 915, y: 133 }
|
||||||
const solid2d = { x: 770, y: 167 }
|
const solid2d = { x: 770, y: 167 }
|
||||||
|
const individualProfile = { x: 694, y: 432 }
|
||||||
|
|
||||||
// DELETE REVOLVE
|
// DELETE REVOLVE
|
||||||
await page.mouse.click(revolve.x, revolve.y)
|
await page.mouse.click(revolve.x, revolve.y)
|
||||||
@ -430,6 +454,20 @@ rev = revolve({ axis = 'y' }, part009)
|
|||||||
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
|
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
|
||||||
await page.waitForTimeout(200)
|
await page.waitForTimeout(200)
|
||||||
await expect(u.codeLocator).not.toContainText(`sketch005 = startSketchOn({`)
|
await expect(u.codeLocator).not.toContainText(`sketch005 = startSketchOn({`)
|
||||||
|
|
||||||
|
// Delete a single profile
|
||||||
|
await page.mouse.click(individualProfile.x, individualProfile.y)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
const codeToBeDeletedSnippet =
|
||||||
|
'profile003 = startProfileAt([40.16, -120.48], sketch006)'
|
||||||
|
await expect(page.locator('.cm-activeLine')).toHaveText(
|
||||||
|
' |> line([20.91, -28.61], %)'
|
||||||
|
)
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
await page.keyboard.press('Backspace')
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
|
||||||
|
await page.waitForTimeout(200)
|
||||||
|
await expect(u.codeLocator).not.toContainText(codeToBeDeletedSnippet)
|
||||||
})
|
})
|
||||||
test("Deleting solid that the AST mod can't handle results in a toast message", async ({
|
test("Deleting solid that the AST mod can't handle results in a toast message", async ({
|
||||||
page,
|
page,
|
||||||
@ -1258,12 +1296,15 @@ extrude001 = extrude(50, sketch001)
|
|||||||
|
|
||||||
await page.waitForTimeout(600)
|
await page.waitForTimeout(600)
|
||||||
|
|
||||||
|
const firstClickCoords = { x: 650, y: 200 } as const
|
||||||
// Place a point because the line tool will exit if no points are pressed
|
// Place a point because the line tool will exit if no points are pressed
|
||||||
await page.mouse.click(650, 200)
|
await page.mouse.click(firstClickCoords.x, firstClickCoords.y)
|
||||||
await page.waitForTimeout(600)
|
await page.waitForTimeout(600)
|
||||||
|
|
||||||
// Code before exiting the tool
|
// Code before exiting the tool
|
||||||
let previousCodeContent = await page.locator('.cm-content').innerText()
|
let previousCodeContent = (
|
||||||
|
await page.locator('.cm-content').innerText()
|
||||||
|
).replace(/\s+/g, '')
|
||||||
|
|
||||||
// deselect the line tool by clicking it
|
// deselect the line tool by clicking it
|
||||||
await page.getByRole('button', { name: 'line Line', exact: true }).click()
|
await page.getByRole('button', { name: 'line Line', exact: true }).click()
|
||||||
@ -1275,14 +1316,23 @@ extrude001 = extrude(50, sketch001)
|
|||||||
await page.mouse.click(750, 200)
|
await page.mouse.click(750, 200)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
// expect no change
|
await expect
|
||||||
await expect(page.locator('.cm-content')).toHaveText(previousCodeContent)
|
.poll(async () => {
|
||||||
|
let str = await page.locator('.cm-content').innerText()
|
||||||
|
str = str.replace(/\s+/g, '')
|
||||||
|
return str
|
||||||
|
})
|
||||||
|
.toBe(previousCodeContent)
|
||||||
|
|
||||||
// select line tool again
|
// select line tool again
|
||||||
await page.getByRole('button', { name: 'line Line', exact: true }).click()
|
await page.getByRole('button', { name: 'line Line', exact: true }).click()
|
||||||
|
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
|
// Click to continue profile
|
||||||
|
await page.mouse.click(firstClickCoords.x, firstClickCoords.y)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
// line tool should work as expected again
|
// line tool should work as expected again
|
||||||
await page.mouse.click(700, 200)
|
await page.mouse.click(700, 200)
|
||||||
await expect(page.locator('.cm-content')).not.toHaveText(
|
await expect(page.locator('.cm-content')).not.toHaveText(
|
||||||
|
@ -224,8 +224,13 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
|
|||||||
// Draw a line
|
// Draw a line
|
||||||
await page.mouse.move(700, 200, { steps: 5 })
|
await page.mouse.move(700, 200, { steps: 5 })
|
||||||
await page.mouse.click(700, 200)
|
await page.mouse.click(700, 200)
|
||||||
await page.mouse.move(800, 250, { steps: 5 })
|
|
||||||
await page.mouse.click(800, 250)
|
const secondMousePosition = { x: 800, y: 250 }
|
||||||
|
|
||||||
|
await page.mouse.move(secondMousePosition.x, secondMousePosition.y, {
|
||||||
|
steps: 5,
|
||||||
|
})
|
||||||
|
await page.mouse.click(secondMousePosition.x, secondMousePosition.y)
|
||||||
// Unequip line tool
|
// Unequip line tool
|
||||||
await page.keyboard.press('Escape')
|
await page.keyboard.press('Escape')
|
||||||
// Make sure we didn't pop out of sketch mode.
|
// Make sure we didn't pop out of sketch mode.
|
||||||
@ -234,9 +239,17 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
|
|||||||
// Equip arc tool
|
// Equip arc tool
|
||||||
await page.keyboard.press('a')
|
await page.keyboard.press('a')
|
||||||
await expect(arcButton).toHaveAttribute('aria-pressed', 'true')
|
await expect(arcButton).toHaveAttribute('aria-pressed', 'true')
|
||||||
|
|
||||||
|
// click in the same position again to continue the profile
|
||||||
|
await page.mouse.move(secondMousePosition.x, secondMousePosition.y, {
|
||||||
|
steps: 5,
|
||||||
|
})
|
||||||
|
await page.mouse.click(secondMousePosition.x, secondMousePosition.y)
|
||||||
|
|
||||||
await page.mouse.move(1000, 100, { steps: 5 })
|
await page.mouse.move(1000, 100, { steps: 5 })
|
||||||
await page.mouse.click(1000, 100)
|
await page.mouse.click(1000, 100)
|
||||||
await page.keyboard.press('Escape')
|
await page.keyboard.press('Escape')
|
||||||
|
await expect(arcButton).toHaveAttribute('aria-pressed', 'false')
|
||||||
await page.keyboard.press('l')
|
await page.keyboard.press('l')
|
||||||
await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
|
await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
|
||||||
|
|
||||||
@ -537,9 +550,9 @@ test('Sketch on face', async ({ page }) => {
|
|||||||
|
|
||||||
await expect.poll(u.normalisedEditorCode).toContain(
|
await expect.poll(u.normalisedEditorCode).toContain(
|
||||||
u.normalisedCode(`sketch002 = startSketchOn(extrude001, seg01)
|
u.normalisedCode(`sketch002 = startSketchOn(extrude001, seg01)
|
||||||
|> startProfileAt([-12.94, 6.6], %)
|
profile001 = startProfileAt([-12.88, 6.66], sketch002)
|
||||||
|> line([2.45, -0.2], %)
|
|> line([2.71, -0.22], %)
|
||||||
|> line([-2.6, -1.25], %)
|
|> line([-2.87, -1.38], %)
|
||||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|> close(%)`)
|
|> close(%)`)
|
||||||
)
|
)
|
||||||
@ -554,8 +567,7 @@ test('Sketch on face', async ({ page }) => {
|
|||||||
await page.getByText('startProfileAt([-12').click()
|
await page.getByText('startProfileAt([-12').click()
|
||||||
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
|
||||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||||
await page.waitForTimeout(400)
|
await page.waitForTimeout(500)
|
||||||
await page.waitForTimeout(150)
|
|
||||||
await page.setViewportSize({ width: 1200, height: 1200 })
|
await page.setViewportSize({ width: 1200, height: 1200 })
|
||||||
await u.openAndClearDebugPanel()
|
await u.openAndClearDebugPanel()
|
||||||
await u.updateCamPosition([452, -152, 1166])
|
await u.updateCamPosition([452, -152, 1166])
|
||||||
|
@ -1,20 +1,9 @@
|
|||||||
import type { ForgeConfig } from '@electron-forge/shared-types'
|
import type { ForgeConfig } from '@electron-forge/shared-types'
|
||||||
import { MakerSquirrel } from '@electron-forge/maker-squirrel'
|
|
||||||
import { MakerZIP } from '@electron-forge/maker-zip'
|
|
||||||
import { MakerDeb } from '@electron-forge/maker-deb'
|
|
||||||
import { MakerRpm } from '@electron-forge/maker-rpm'
|
|
||||||
import { VitePlugin } from '@electron-forge/plugin-vite'
|
import { VitePlugin } from '@electron-forge/plugin-vite'
|
||||||
import { MakerWix, MakerWixConfig } from '@electron-forge/maker-wix'
|
|
||||||
import { FusesPlugin } from '@electron-forge/plugin-fuses'
|
import { FusesPlugin } from '@electron-forge/plugin-fuses'
|
||||||
import { FuseV1Options, FuseVersion } from '@electron/fuses'
|
import { FuseV1Options, FuseVersion } from '@electron/fuses'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
interface ExtendedMakerWixConfig extends MakerWixConfig {
|
|
||||||
// see https://github.com/electron/forge/issues/3673
|
|
||||||
// this is an undocumented property of electron-wix-msi
|
|
||||||
associateExtensions?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const rootDir = process.cwd()
|
const rootDir = process.cwd()
|
||||||
|
|
||||||
const config: ForgeConfig = {
|
const config: ForgeConfig = {
|
||||||
@ -39,26 +28,7 @@ const config: ForgeConfig = {
|
|||||||
extendInfo: 'Info.plist', // Information for file associations.
|
extendInfo: 'Info.plist', // Information for file associations.
|
||||||
},
|
},
|
||||||
rebuildConfig: {},
|
rebuildConfig: {},
|
||||||
makers: [
|
makers: [],
|
||||||
new MakerSquirrel({
|
|
||||||
setupIcon: path.resolve(rootDir, 'assets', 'icon.ico'),
|
|
||||||
}),
|
|
||||||
new MakerWix({
|
|
||||||
icon: path.resolve(rootDir, 'assets', 'icon.ico'),
|
|
||||||
associateExtensions: 'kcl',
|
|
||||||
} as ExtendedMakerWixConfig),
|
|
||||||
new MakerZIP({}, ['darwin']),
|
|
||||||
new MakerRpm({
|
|
||||||
options: {
|
|
||||||
icon: path.resolve(rootDir, 'assets', 'icon.png'),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
new MakerDeb({
|
|
||||||
options: {
|
|
||||||
icon: path.resolve(rootDir, 'assets', 'icon.png'),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
plugins: [
|
plugins: [
|
||||||
new VitePlugin({
|
new VitePlugin({
|
||||||
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
|
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
|
||||||
|
26
package.json
@ -39,7 +39,6 @@
|
|||||||
"chokidar": "^4.0.1",
|
"chokidar": "^4.0.1",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"decamelize": "^6.0.0",
|
"decamelize": "^6.0.0",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
|
||||||
"electron-updater": "6.3.0",
|
"electron-updater": "6.3.0",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"html2canvas-pro": "^1.5.8",
|
"html2canvas-pro": "^1.5.8",
|
||||||
@ -69,7 +68,7 @@
|
|||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite --port=3000 --host=0.0.0.0",
|
||||||
"start:prod": "vite preview --port=3000",
|
"start:prod": "vite preview --port=3000",
|
||||||
"serve": "vite serve --port=3000",
|
"serve": "vite serve --port=3000",
|
||||||
"build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build",
|
"build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build",
|
||||||
@ -104,8 +103,6 @@
|
|||||||
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
|
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
|
||||||
"tron:start": "electron-forge start",
|
"tron:start": "electron-forge start",
|
||||||
"tron:package": "electron-forge package",
|
"tron:package": "electron-forge package",
|
||||||
"tron:make": "electron-forge make",
|
|
||||||
"tron:publish": "electron-forge publish",
|
|
||||||
"tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron",
|
"tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron",
|
||||||
"tronb:vite": "vite build -c vite.main.config.ts && vite build -c vite.preload.config.ts && vite build -c vite.renderer.config.ts",
|
"tronb:vite": "vite build -c vite.main.config.ts && vite build -c vite.preload.config.ts && vite build -c vite.renderer.config.ts",
|
||||||
"tronb:package": "electron-builder --config electron-builder.yml",
|
"tronb:package": "electron-builder --config electron-builder.yml",
|
||||||
@ -148,17 +145,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-env": "^7.25.4",
|
"@babel/preset-env": "^7.25.4",
|
||||||
"@electron-forge/cli": "^7.4.0",
|
"@electron-forge/cli": "7.4.0",
|
||||||
"@electron-forge/maker-deb": "^7.4.0",
|
"@electron-forge/plugin-fuses": "7.4.0",
|
||||||
"@electron-forge/maker-rpm": "^7.4.0",
|
"@electron-forge/plugin-vite": "7.4.0",
|
||||||
"@electron-forge/maker-squirrel": "^7.4.0",
|
"@electron/fuses": "1.8.0",
|
||||||
"@electron-forge/maker-wix": "^7.5.0",
|
|
||||||
"@electron-forge/maker-zip": "^7.5.0",
|
|
||||||
"@electron-forge/plugin-auto-unpack-natives": "^7.4.0",
|
|
||||||
"@electron-forge/plugin-fuses": "^7.4.0",
|
|
||||||
"@electron-forge/plugin-vite": "^7.4.0",
|
|
||||||
"@electron/fuses": "^1.8.0",
|
|
||||||
"@electron/rebuild": "^3.6.0",
|
|
||||||
"@iarna/toml": "^2.2.5",
|
"@iarna/toml": "^2.2.5",
|
||||||
"@lezer/generator": "^1.7.1",
|
"@lezer/generator": "^1.7.1",
|
||||||
"@nabla/vite-plugin-eslint": "^2.0.5",
|
"@nabla/vite-plugin-eslint": "^2.0.5",
|
||||||
@ -188,9 +178,9 @@
|
|||||||
"@xstate/cli": "^0.5.17",
|
"@xstate/cli": "^0.5.17",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"d3-force": "^3.0.0",
|
"d3-force": "^3.0.0",
|
||||||
"electron": "^32.1.2",
|
"electron": "32.1.2",
|
||||||
"electron-builder": "^24.13.3",
|
"electron-builder": "24.13.3",
|
||||||
"electron-notarize": "^1.2.2",
|
"electron-notarize": "1.2.2",
|
||||||
"eslint": "^8.0.1",
|
"eslint": "^8.0.1",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"eslint-plugin-css-modules": "^2.12.0",
|
"eslint-plugin-css-modules": "^2.12.0",
|
||||||
|
@ -119,6 +119,11 @@
|
|||||||
"title": "Pipe and Flange Assembly",
|
"title": "Pipe and Flange Assembly",
|
||||||
"description": "A crucial component in various piping systems, designed to facilitate the connection, disconnection, and access to piping for inspection, cleaning, and modifications. This assembly combines pipes (long cylindrical conduits) with flanges (plate-like fittings) to create a secure yet detachable joint."
|
"description": "A crucial component in various piping systems, designed to facilitate the connection, disconnection, and access to piping for inspection, cleaning, and modifications. This assembly combines pipes (long cylindrical conduits) with flanges (plate-like fittings) to create a secure yet detachable joint."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"file": "pipe-with-bend.kcl",
|
||||||
|
"title": "Pipe with bend",
|
||||||
|
"description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"file": "poopy-shoe.kcl",
|
"file": "poopy-shoe.kcl",
|
||||||
"title": "Poopy Shoe",
|
"title": "Poopy Shoe",
|
||||||
|
@ -6,7 +6,6 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
|
|||||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||||
import { ActionButton } from 'components/ActionButton'
|
import { ActionButton } from 'components/ActionButton'
|
||||||
import { isSingleCursorInPipe } from 'lang/queryAst'
|
|
||||||
import { useKclContext } from 'lang/KclProvider'
|
import { useKclContext } from 'lang/KclProvider'
|
||||||
import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
|
import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
@ -22,6 +21,7 @@ import {
|
|||||||
} from 'lib/toolbar'
|
} from 'lib/toolbar'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
|
import { isCursorInFunctionDefinition } from 'lang/queryAst'
|
||||||
|
|
||||||
export function Toolbar({
|
export function Toolbar({
|
||||||
className = '',
|
className = '',
|
||||||
@ -38,7 +38,12 @@ export function Toolbar({
|
|||||||
'!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary'
|
'!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary'
|
||||||
|
|
||||||
const sketchPathId = useMemo(() => {
|
const sketchPathId = useMemo(() => {
|
||||||
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast))
|
if (
|
||||||
|
isCursorInFunctionDefinition(
|
||||||
|
kclManager.ast,
|
||||||
|
context.selectionRanges.graphSelections[0]
|
||||||
|
)
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
return isCursorInSketchCommandRange(
|
return isCursorInSketchCommandRange(
|
||||||
engineCommandManager.artifactGraph,
|
engineCommandManager.artifactGraph,
|
||||||
|
@ -105,7 +105,7 @@ export class CameraControls {
|
|||||||
pendingZoom: number | null = null
|
pendingZoom: number | null = null
|
||||||
pendingRotation: Vector2 | null = null
|
pendingRotation: Vector2 | null = null
|
||||||
pendingPan: Vector2 | null = null
|
pendingPan: Vector2 | null = null
|
||||||
interactionGuards: MouseGuard = cameraMouseDragGuards.KittyCAD
|
interactionGuards: MouseGuard = cameraMouseDragGuards.Zoo
|
||||||
isFovAnimationInProgress = false
|
isFovAnimationInProgress = false
|
||||||
perspectiveFovBeforeOrtho = 45
|
perspectiveFovBeforeOrtho = 45
|
||||||
get isPerspective() {
|
get isPerspective() {
|
||||||
|
@ -433,6 +433,8 @@ export async function deleteSegment({
|
|||||||
if (!sketchDetails) return
|
if (!sketchDetails) return
|
||||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||||
pathToNode,
|
pathToNode,
|
||||||
|
sketchDetails.sketchNodePaths,
|
||||||
|
sketchDetails.planeNodePath,
|
||||||
modifiedAst,
|
modifiedAst,
|
||||||
sketchDetails.zAxis,
|
sketchDetails.zAxis,
|
||||||
sketchDetails.yAxis,
|
sketchDetails.yAxis,
|
||||||
@ -505,7 +507,8 @@ const ConstraintSymbol = ({
|
|||||||
constrainInfo: ConstrainInfo
|
constrainInfo: ConstrainInfo
|
||||||
verticalPosition: 'top' | 'bottom'
|
verticalPosition: 'top' | 'bottom'
|
||||||
}) => {
|
}) => {
|
||||||
const { context, send } = useModelingContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
|
const { context } = useModelingContext()
|
||||||
const varNameMap: {
|
const varNameMap: {
|
||||||
[key in ConstrainInfo['type']]: {
|
[key in ConstrainInfo['type']]: {
|
||||||
varName: string
|
varName: string
|
||||||
@ -624,11 +627,18 @@ const ConstraintSymbol = ({
|
|||||||
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
|
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
|
||||||
onClick={toSync(async () => {
|
onClick={toSync(async () => {
|
||||||
if (!isConstrained) {
|
if (!isConstrained) {
|
||||||
send({
|
commandBarSend({
|
||||||
type: 'Convert to variable',
|
type: 'Find and select command',
|
||||||
data: {
|
data: {
|
||||||
pathToNode,
|
name: 'Constrain with named value',
|
||||||
variableName: varName,
|
groupId: 'modeling',
|
||||||
|
argDefaultValues: {
|
||||||
|
currentValue: {
|
||||||
|
pathToNode,
|
||||||
|
variableName: varName,
|
||||||
|
valueText: value,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else if (isConstrained) {
|
} else if (isConstrained) {
|
||||||
|
@ -691,19 +691,21 @@ export function createProfileStartHandle({
|
|||||||
scale = 1,
|
scale = 1,
|
||||||
theme,
|
theme,
|
||||||
isSelected,
|
isSelected,
|
||||||
|
size = 12,
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
from: Coords2d
|
from: Coords2d
|
||||||
scale?: number
|
scale?: number
|
||||||
theme: Themes
|
theme: Themes
|
||||||
isSelected?: boolean
|
isSelected?: boolean
|
||||||
|
size?: number
|
||||||
} & (
|
} & (
|
||||||
| { isDraft: true }
|
| { isDraft: true }
|
||||||
| { isDraft: false; id: string; pathToNode: PathToNode }
|
| { isDraft: false; id: string; pathToNode: PathToNode }
|
||||||
)) {
|
)) {
|
||||||
const group = new Group()
|
const group = new Group()
|
||||||
|
|
||||||
const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later
|
const geometry = new BoxGeometry(size, size, size) // in pixels scaled later
|
||||||
const baseColor = getThemeColorForThreeJs(theme)
|
const baseColor = getThemeColorForThreeJs(theme)
|
||||||
const color = isSelected ? 0x0000ff : baseColor
|
const color = isSelected ? 0x0000ff : baseColor
|
||||||
const body = new MeshBasicMaterial({ color })
|
const body = new MeshBasicMaterial({ color })
|
||||||
|
@ -8,11 +8,16 @@ import { getSystemTheme } from 'lib/theme'
|
|||||||
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
|
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
|
||||||
import { roundOff } from 'lib/utils'
|
import { roundOff } from 'lib/utils'
|
||||||
import { varMentions } from 'lib/varCompletionExtension'
|
import { varMentions } from 'lib/varCompletionExtension'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import styles from './CommandBarKclInput.module.css'
|
import styles from './CommandBarKclInput.module.css'
|
||||||
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
|
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
|
||||||
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
|
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
|
||||||
|
import { useSelector } from '@xstate/react'
|
||||||
|
|
||||||
|
const machineContextSelector = (snapshot?: {
|
||||||
|
context: Record<string, unknown>
|
||||||
|
}) => snapshot?.context
|
||||||
|
|
||||||
function CommandBarKclInput({
|
function CommandBarKclInput({
|
||||||
arg,
|
arg,
|
||||||
@ -31,12 +36,44 @@ function CommandBarKclInput({
|
|||||||
arg.name
|
arg.name
|
||||||
] as KclCommandValue | undefined
|
] as KclCommandValue | undefined
|
||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const defaultValue = (arg.defaultValue as string) || ''
|
const argMachineContext = useSelector(
|
||||||
|
arg.machineActor,
|
||||||
|
machineContextSelector
|
||||||
|
)
|
||||||
|
const defaultValue = useMemo(
|
||||||
|
() =>
|
||||||
|
arg.defaultValue
|
||||||
|
? arg.defaultValue instanceof Function
|
||||||
|
? arg.defaultValue(commandBarState.context, argMachineContext)
|
||||||
|
: arg.defaultValue
|
||||||
|
: '',
|
||||||
|
[arg.defaultValue, commandBarState.context, argMachineContext]
|
||||||
|
)
|
||||||
|
const initialVariableName = useMemo(() => {
|
||||||
|
// Use the configured variable name if it exists
|
||||||
|
if (arg.variableName !== undefined) {
|
||||||
|
return arg.variableName instanceof Function
|
||||||
|
? arg.variableName(commandBarState.context, argMachineContext)
|
||||||
|
: arg.variableName
|
||||||
|
}
|
||||||
|
// or derive it from the previously set value or the argument name
|
||||||
|
return previouslySetValue && 'variableName' in previouslySetValue
|
||||||
|
? previouslySetValue.variableName
|
||||||
|
: arg.name
|
||||||
|
}, [
|
||||||
|
arg.variableName,
|
||||||
|
commandBarState.context,
|
||||||
|
argMachineContext,
|
||||||
|
arg.name,
|
||||||
|
previouslySetValue,
|
||||||
|
])
|
||||||
const [value, setValue] = useState(
|
const [value, setValue] = useState(
|
||||||
previouslySetValue?.valueText || defaultValue || ''
|
previouslySetValue?.valueText || defaultValue || ''
|
||||||
)
|
)
|
||||||
const [createNewVariable, setCreateNewVariable] = useState(
|
const [createNewVariable, setCreateNewVariable] = useState(
|
||||||
previouslySetValue && 'variableName' in previouslySetValue
|
(previouslySetValue && 'variableName' in previouslySetValue) ||
|
||||||
|
arg.createVariableByDefault ||
|
||||||
|
false
|
||||||
)
|
)
|
||||||
const [canSubmit, setCanSubmit] = useState(true)
|
const [canSubmit, setCanSubmit] = useState(true)
|
||||||
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
||||||
@ -52,10 +89,7 @@ function CommandBarKclInput({
|
|||||||
isNewVariableNameUnique,
|
isNewVariableNameUnique,
|
||||||
} = useCalculateKclExpression({
|
} = useCalculateKclExpression({
|
||||||
value,
|
value,
|
||||||
initialVariableName:
|
initialVariableName,
|
||||||
previouslySetValue && 'variableName' in previouslySetValue
|
|
||||||
? previouslySetValue.variableName
|
|
||||||
: arg.name,
|
|
||||||
})
|
})
|
||||||
const varMentionData: Completion[] = prevVariables.map((v) => ({
|
const varMentionData: Completion[] = prevVariables.map((v) => ({
|
||||||
label: v.key,
|
label: v.key,
|
||||||
|
@ -1,13 +1,23 @@
|
|||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { ActionIcon, ActionIconProps } from './ActionIcon'
|
import { ActionIcon, ActionIconProps } from './ActionIcon'
|
||||||
import { RefObject, useEffect, useMemo, useRef, useState } from 'react'
|
import {
|
||||||
|
MouseEvent,
|
||||||
|
RefObject,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { Dialog } from '@headlessui/react'
|
import { Dialog } from '@headlessui/react'
|
||||||
|
|
||||||
interface ContextMenuProps
|
export interface ContextMenuProps
|
||||||
extends Omit<React.HTMLAttributes<HTMLUListElement>, 'children'> {
|
extends Omit<React.HTMLAttributes<HTMLUListElement>, 'children'> {
|
||||||
items?: React.ReactElement[]
|
items?: React.ReactElement[]
|
||||||
menuTargetElement?: RefObject<HTMLElement>
|
menuTargetElement?: RefObject<HTMLElement>
|
||||||
|
guard?: (e: globalThis.MouseEvent) => boolean
|
||||||
|
event?: 'contextmenu' | 'mouseup'
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultContextMenuItems = [
|
const DefaultContextMenuItems = [
|
||||||
@ -20,6 +30,8 @@ export function ContextMenu({
|
|||||||
items = DefaultContextMenuItems,
|
items = DefaultContextMenuItems,
|
||||||
menuTargetElement,
|
menuTargetElement,
|
||||||
className,
|
className,
|
||||||
|
guard,
|
||||||
|
event = 'contextmenu',
|
||||||
...props
|
...props
|
||||||
}: ContextMenuProps) {
|
}: ContextMenuProps) {
|
||||||
const dialogRef = useRef<HTMLDivElement>(null)
|
const dialogRef = useRef<HTMLDivElement>(null)
|
||||||
@ -32,6 +44,15 @@ export function ContextMenu({
|
|||||||
useHotkeys('esc', () => setOpen(false), {
|
useHotkeys('esc', () => setOpen(false), {
|
||||||
enabled: open,
|
enabled: open,
|
||||||
})
|
})
|
||||||
|
const handleContextMenu = useCallback(
|
||||||
|
(e: globalThis.MouseEvent) => {
|
||||||
|
if (guard && !guard(e)) return
|
||||||
|
e.preventDefault()
|
||||||
|
setPosition({ x: e.clientX, y: e.clientY })
|
||||||
|
setOpen(true)
|
||||||
|
},
|
||||||
|
[guard, setPosition, setOpen]
|
||||||
|
)
|
||||||
|
|
||||||
const dialogPositionStyle = useMemo(() => {
|
const dialogPositionStyle = useMemo(() => {
|
||||||
if (!dialogRef.current)
|
if (!dialogRef.current)
|
||||||
@ -78,21 +99,9 @@ export function ContextMenu({
|
|||||||
|
|
||||||
// Add context menu listener to target once mounted
|
// Add context menu listener to target once mounted
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleContextMenu = (e: MouseEvent) => {
|
menuTargetElement?.current?.addEventListener(event, handleContextMenu)
|
||||||
console.log('context menu', e)
|
|
||||||
e.preventDefault()
|
|
||||||
setPosition({ x: e.x, y: e.y })
|
|
||||||
setOpen(true)
|
|
||||||
}
|
|
||||||
menuTargetElement?.current?.addEventListener(
|
|
||||||
'contextmenu',
|
|
||||||
handleContextMenu
|
|
||||||
)
|
|
||||||
return () => {
|
return () => {
|
||||||
menuTargetElement?.current?.removeEventListener(
|
menuTargetElement?.current?.removeEventListener(event, handleContextMenu)
|
||||||
'contextmenu',
|
|
||||||
handleContextMenu
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}, [menuTargetElement?.current])
|
}, [menuTargetElement?.current])
|
||||||
|
|
||||||
@ -100,7 +109,10 @@ export function ContextMenu({
|
|||||||
<Dialog open={open} onClose={() => setOpen(false)}>
|
<Dialog open={open} onClose={() => setOpen(false)}>
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-50 w-screen h-screen"
|
className="fixed inset-0 z-50 w-screen h-screen"
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setPosition({ x: e.clientX, y: e.clientY })
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Dialog.Backdrop className="fixed z-10 inset-0" />
|
<Dialog.Backdrop className="fixed z-10 inset-0" />
|
||||||
<Dialog.Panel
|
<Dialog.Panel
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { SceneInfra } from 'clientSideScene/sceneInfra'
|
import { SceneInfra } from 'clientSideScene/sceneInfra'
|
||||||
import { sceneInfra } from 'lib/singletons'
|
import { sceneInfra } from 'lib/singletons'
|
||||||
import { MutableRefObject, useEffect, useMemo, useRef } from 'react'
|
import { MutableRefObject, useEffect, useRef } from 'react'
|
||||||
import {
|
import {
|
||||||
WebGLRenderer,
|
WebGLRenderer,
|
||||||
Scene,
|
Scene,
|
||||||
@ -19,16 +19,14 @@ import {
|
|||||||
Intersection,
|
Intersection,
|
||||||
Object3D,
|
Object3D,
|
||||||
} from 'three'
|
} from 'three'
|
||||||
import {
|
|
||||||
ContextMenu,
|
|
||||||
ContextMenuDivider,
|
|
||||||
ContextMenuItem,
|
|
||||||
ContextMenuItemRefresh,
|
|
||||||
} from './ContextMenu'
|
|
||||||
import { Popover } from '@headlessui/react'
|
import { Popover } from '@headlessui/react'
|
||||||
import { CustomIcon } from './CustomIcon'
|
import { CustomIcon } from './CustomIcon'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import {
|
||||||
|
useViewControlMenuItems,
|
||||||
|
ViewControlContextMenu,
|
||||||
|
} from './ViewControlMenu'
|
||||||
|
import { AxisNames } from 'lib/constants'
|
||||||
|
|
||||||
const CANVAS_SIZE = 80
|
const CANVAS_SIZE = 80
|
||||||
const FRUSTUM_SIZE = 0.5
|
const FRUSTUM_SIZE = 0.5
|
||||||
@ -40,64 +38,14 @@ enum AxisColors {
|
|||||||
Z = '#6689ef',
|
Z = '#6689ef',
|
||||||
Gray = '#c6c7c2',
|
Gray = '#c6c7c2',
|
||||||
}
|
}
|
||||||
enum AxisNames {
|
|
||||||
X = 'x',
|
|
||||||
Y = 'y',
|
|
||||||
Z = 'z',
|
|
||||||
NEG_X = '-x',
|
|
||||||
NEG_Y = '-y',
|
|
||||||
NEG_Z = '-z',
|
|
||||||
}
|
|
||||||
const axisNamesSemantic: Record<AxisNames, string> = {
|
|
||||||
[AxisNames.X]: 'Right',
|
|
||||||
[AxisNames.Y]: 'Back',
|
|
||||||
[AxisNames.Z]: 'Top',
|
|
||||||
[AxisNames.NEG_X]: 'Left',
|
|
||||||
[AxisNames.NEG_Y]: 'Front',
|
|
||||||
[AxisNames.NEG_Z]: 'Bottom',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Gizmo() {
|
export default function Gizmo() {
|
||||||
|
const menuItems = useViewControlMenuItems()
|
||||||
const wrapperRef = useRef<HTMLDivElement | null>(null)
|
const wrapperRef = useRef<HTMLDivElement | null>(null)
|
||||||
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
||||||
const raycasterIntersect = useRef<Intersection<Object3D> | null>(null)
|
const raycasterIntersect = useRef<Intersection<Object3D> | null>(null)
|
||||||
const cameraPassiveUpdateTimer = useRef(0)
|
const cameraPassiveUpdateTimer = useRef(0)
|
||||||
const raycasterPassiveUpdateTimer = useRef(0)
|
const raycasterPassiveUpdateTimer = useRef(0)
|
||||||
const { send: modelingSend } = useModelingContext()
|
|
||||||
const menuItems = useMemo(
|
|
||||||
() => [
|
|
||||||
...Object.entries(axisNamesSemantic).map(([axisName, axisSemantic]) => (
|
|
||||||
<ContextMenuItem
|
|
||||||
key={axisName}
|
|
||||||
onClick={() => {
|
|
||||||
sceneInfra.camControls
|
|
||||||
.updateCameraToAxis(axisName as AxisNames)
|
|
||||||
.catch(reportRejection)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{axisSemantic} view
|
|
||||||
</ContextMenuItem>
|
|
||||||
)),
|
|
||||||
<ContextMenuDivider />,
|
|
||||||
<ContextMenuItem
|
|
||||||
onClick={() => {
|
|
||||||
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Reset view
|
|
||||||
</ContextMenuItem>,
|
|
||||||
<ContextMenuItem
|
|
||||||
onClick={() => {
|
|
||||||
modelingSend({ type: 'Center camera on selection' })
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Center view on selection
|
|
||||||
</ContextMenuItem>,
|
|
||||||
<ContextMenuDivider />,
|
|
||||||
<ContextMenuItemRefresh />,
|
|
||||||
],
|
|
||||||
[axisNamesSemantic]
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canvasRef.current) return
|
if (!canvasRef.current) return
|
||||||
@ -161,7 +109,7 @@ export default function Gizmo() {
|
|||||||
className="grid place-content-center rounded-full overflow-hidden border border-solid border-primary/50 pointer-events-auto bg-chalkboard-10/70 dark:bg-chalkboard-100/80 backdrop-blur-sm"
|
className="grid place-content-center rounded-full overflow-hidden border border-solid border-primary/50 pointer-events-auto bg-chalkboard-10/70 dark:bg-chalkboard-100/80 backdrop-blur-sm"
|
||||||
>
|
>
|
||||||
<canvas ref={canvasRef} />
|
<canvas ref={canvasRef} />
|
||||||
<ContextMenu menuTargetElement={wrapperRef} items={menuItems} />
|
<ViewControlContextMenu menuTargetElement={wrapperRef} />
|
||||||
</div>
|
</div>
|
||||||
<GizmoDropdown items={menuItems} />
|
<GizmoDropdown items={menuItems} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { APP_VERSION } from 'routes/Settings'
|
import { APP_VERSION, getReleaseUrl } from 'routes/Settings'
|
||||||
import { CustomIcon } from 'components/CustomIcon'
|
import { CustomIcon } from 'components/CustomIcon'
|
||||||
import Tooltip from 'components/Tooltip'
|
import Tooltip from 'components/Tooltip'
|
||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
@ -72,10 +72,8 @@ export function LowerRightControls({
|
|||||||
<menu className="flex items-center justify-end gap-3 pointer-events-auto">
|
<menu className="flex items-center justify-end gap-3 pointer-events-auto">
|
||||||
{!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />}
|
{!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />}
|
||||||
<a
|
<a
|
||||||
onClick={openExternalBrowserIfDesktop(
|
onClick={openExternalBrowserIfDesktop(getReleaseUrl())}
|
||||||
`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`
|
href={getReleaseUrl()}
|
||||||
)}
|
|
||||||
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`}
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className={'!no-underline font-mono text-xs ' + linkOverrideClassName}
|
className={'!no-underline font-mono text-xs ' + linkOverrideClassName}
|
||||||
|
@ -69,14 +69,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
const [isKclLspReady, setIsKclLspReady] = useState(false)
|
const [isKclLspReady, setIsKclLspReady] = useState(false)
|
||||||
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
|
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
|
||||||
|
|
||||||
const {
|
const { auth } = useSettingsAuthContext()
|
||||||
auth,
|
|
||||||
settings: {
|
|
||||||
context: {
|
|
||||||
modeling: { defaultUnit },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} = useSettingsAuthContext()
|
|
||||||
const token = auth?.context.token
|
const token = auth?.context.token
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
@ -92,7 +85,6 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
const initEvent: KclWorkerOptions = {
|
const initEvent: KclWorkerOptions = {
|
||||||
wasmUrl: wasmUrl(),
|
wasmUrl: wasmUrl(),
|
||||||
token: token,
|
token: token,
|
||||||
baseUnit: defaultUnit.current,
|
|
||||||
apiBaseUrl: VITE_KC_API_BASE_URL,
|
apiBaseUrl: VITE_KC_API_BASE_URL,
|
||||||
}
|
}
|
||||||
lspWorker.postMessage({
|
lspWorker.postMessage({
|
||||||
|
@ -24,7 +24,7 @@ import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
|
|||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import {
|
import {
|
||||||
isCursorInSketchCommandRange,
|
isCursorInSketchCommandRange,
|
||||||
updatePathToNodeFromMap,
|
updateSketchDetailsNodePaths,
|
||||||
} from 'lang/util'
|
} from 'lang/util'
|
||||||
import {
|
import {
|
||||||
kclManager,
|
kclManager,
|
||||||
@ -41,7 +41,10 @@ import {
|
|||||||
angleBetweenInfo,
|
angleBetweenInfo,
|
||||||
applyConstraintAngleBetween,
|
applyConstraintAngleBetween,
|
||||||
} from './Toolbar/SetAngleBetween'
|
} from './Toolbar/SetAngleBetween'
|
||||||
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
|
import {
|
||||||
|
applyConstraintAngleLength,
|
||||||
|
applyConstraintLength,
|
||||||
|
} from './Toolbar/setAngleLength'
|
||||||
import {
|
import {
|
||||||
canSweepSelection,
|
canSweepSelection,
|
||||||
handleSelectionBatch,
|
handleSelectionBatch,
|
||||||
@ -51,6 +54,8 @@ import {
|
|||||||
Selections,
|
Selections,
|
||||||
updateSelections,
|
updateSelections,
|
||||||
canLoftSelection,
|
canLoftSelection,
|
||||||
|
canRevolveSelection,
|
||||||
|
canShellSelection,
|
||||||
} from 'lib/selections'
|
} from 'lib/selections'
|
||||||
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
||||||
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
||||||
@ -62,16 +67,28 @@ import {
|
|||||||
getSketchOrientationDetails,
|
getSketchOrientationDetails,
|
||||||
} from 'clientSideScene/sceneEntities'
|
} from 'clientSideScene/sceneEntities'
|
||||||
import {
|
import {
|
||||||
moveValueIntoNewVariablePath,
|
insertNamedConstant,
|
||||||
|
replaceValueAtNodePath,
|
||||||
sketchOnExtrudedFace,
|
sketchOnExtrudedFace,
|
||||||
sketchOnOffsetPlane,
|
sketchOnOffsetPlane,
|
||||||
|
splitPipedProfile,
|
||||||
startSketchOnDefault,
|
startSketchOnDefault,
|
||||||
} from 'lang/modifyAst'
|
} from 'lang/modifyAst'
|
||||||
import { Program, parse, recast, resultIsOk } from 'lang/wasm'
|
|
||||||
import {
|
import {
|
||||||
|
PathToNode,
|
||||||
|
Program,
|
||||||
|
VariableDeclaration,
|
||||||
|
parse,
|
||||||
|
recast,
|
||||||
|
resultIsOk,
|
||||||
|
} from 'lang/wasm'
|
||||||
|
import {
|
||||||
|
doesSceneHaveExtrudedSketch,
|
||||||
doesSceneHaveSweepableSketch,
|
doesSceneHaveSweepableSketch,
|
||||||
getNodePathFromSourceRange,
|
doesSketchPipeNeedSplitting,
|
||||||
isSingleCursorInPipe,
|
getNodeFromPath,
|
||||||
|
isCursorInFunctionDefinition,
|
||||||
|
traverse,
|
||||||
} from 'lang/queryAst'
|
} from 'lang/queryAst'
|
||||||
import { exportFromEngine } from 'lib/exportFromEngine'
|
import { exportFromEngine } from 'lib/exportFromEngine'
|
||||||
import { Models } from '@kittycad/lib/dist/types/src'
|
import { Models } from '@kittycad/lib/dist/types/src'
|
||||||
@ -79,8 +96,7 @@ import toast from 'react-hot-toast'
|
|||||||
import { EditorSelection, Transaction } from '@codemirror/state'
|
import { EditorSelection, Transaction } from '@codemirror/state'
|
||||||
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
||||||
import { getVarNameModal } from 'hooks/useToolbarGuards'
|
import { err, reportRejection, trap, reject } from 'lib/trap'
|
||||||
import { err, reportRejection, trap } from 'lib/trap'
|
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { modelingMachineEvent } from 'editor/manager'
|
import { modelingMachineEvent } from 'editor/manager'
|
||||||
import { hasValidEdgeTreatmentSelection } from 'lang/modifyAst/addEdgeTreatment'
|
import { hasValidEdgeTreatmentSelection } from 'lang/modifyAst/addEdgeTreatment'
|
||||||
@ -94,6 +110,10 @@ import { useFileContext } from 'hooks/useFileContext'
|
|||||||
import { uuidv4 } from 'lib/utils'
|
import { uuidv4 } from 'lib/utils'
|
||||||
import { IndexLoaderData } from 'lib/types'
|
import { IndexLoaderData } from 'lib/types'
|
||||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
|
import {
|
||||||
|
getPathsFromArtifact,
|
||||||
|
getPlaneFromArtifact,
|
||||||
|
} from 'lang/std/artifactGraph'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -284,7 +304,7 @@ export const ModelingMachineProvider = ({
|
|||||||
return {
|
return {
|
||||||
sketchDetails: {
|
sketchDetails: {
|
||||||
...sketchDetails,
|
...sketchDetails,
|
||||||
sketchPathToNode: event.data,
|
sketchEntryNodePath: event.data,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@ -407,9 +427,17 @@ export const ModelingMachineProvider = ({
|
|||||||
selectionRanges: setSelections.selection,
|
selectionRanges: setSelections.selection,
|
||||||
sketchDetails: {
|
sketchDetails: {
|
||||||
...sketchDetails,
|
...sketchDetails,
|
||||||
sketchPathToNode:
|
sketchEntryNodePath:
|
||||||
setSelections.updatedPathToNode ||
|
setSelections.updatedSketchEntryNodePath ||
|
||||||
sketchDetails?.sketchPathToNode ||
|
sketchDetails?.sketchEntryNodePath ||
|
||||||
|
[],
|
||||||
|
sketchNodePaths:
|
||||||
|
setSelections.updatedSketchNodePaths ||
|
||||||
|
sketchDetails?.sketchNodePaths ||
|
||||||
|
[],
|
||||||
|
planeNodePath:
|
||||||
|
setSelections.updatedPlaneNodePath ||
|
||||||
|
sketchDetails?.planeNodePath ||
|
||||||
[],
|
[],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -570,6 +598,26 @@ export const ModelingMachineProvider = ({
|
|||||||
if (err(canSweep)) return false
|
if (err(canSweep)) return false
|
||||||
return canSweep
|
return canSweep
|
||||||
},
|
},
|
||||||
|
'has valid revolve selection': ({ context: { selectionRanges } }) => {
|
||||||
|
// A user can begin extruding if they either have 1+ faces selected or nothing selected
|
||||||
|
// TODO: I believe this guard only allows for extruding a single face at a time
|
||||||
|
const hasNoSelection =
|
||||||
|
selectionRanges.graphSelections.length === 0 ||
|
||||||
|
isRangeBetweenCharacters(selectionRanges) ||
|
||||||
|
isSelectionLastLine(selectionRanges, codeManager.code)
|
||||||
|
|
||||||
|
if (hasNoSelection) {
|
||||||
|
// they have no selection, we should enable the button
|
||||||
|
// so they can select the face through the cmdbar
|
||||||
|
// BUT only if there's extrudable geometry
|
||||||
|
return doesSceneHaveSweepableSketch(kclManager.ast)
|
||||||
|
}
|
||||||
|
if (!isSketchPipe(selectionRanges)) return false
|
||||||
|
|
||||||
|
const canSweep = canRevolveSelection(selectionRanges)
|
||||||
|
if (err(canSweep)) return false
|
||||||
|
return canSweep
|
||||||
|
},
|
||||||
'has valid loft selection': ({ context: { selectionRanges } }) => {
|
'has valid loft selection': ({ context: { selectionRanges } }) => {
|
||||||
const hasNoSelection =
|
const hasNoSelection =
|
||||||
selectionRanges.graphSelections.length === 0 ||
|
selectionRanges.graphSelections.length === 0 ||
|
||||||
@ -585,6 +633,23 @@ export const ModelingMachineProvider = ({
|
|||||||
if (err(canLoft)) return false
|
if (err(canLoft)) return false
|
||||||
return canLoft
|
return canLoft
|
||||||
},
|
},
|
||||||
|
'has valid shell selection': ({
|
||||||
|
context: { selectionRanges },
|
||||||
|
event,
|
||||||
|
}) => {
|
||||||
|
const hasNoSelection =
|
||||||
|
selectionRanges.graphSelections.length === 0 ||
|
||||||
|
isRangeBetweenCharacters(selectionRanges) ||
|
||||||
|
isSelectionLastLine(selectionRanges, codeManager.code)
|
||||||
|
|
||||||
|
if (hasNoSelection) {
|
||||||
|
return doesSceneHaveExtrudedSketch(kclManager.ast)
|
||||||
|
}
|
||||||
|
|
||||||
|
const canShell = canShellSelection(selectionRanges)
|
||||||
|
if (err(canShell)) return false
|
||||||
|
return canShell
|
||||||
|
},
|
||||||
'has valid selection for deletion': ({
|
'has valid selection for deletion': ({
|
||||||
context: { selectionRanges },
|
context: { selectionRanges },
|
||||||
}) => {
|
}) => {
|
||||||
@ -604,7 +669,12 @@ export const ModelingMachineProvider = ({
|
|||||||
'Selection is on face': ({ context: { selectionRanges }, event }) => {
|
'Selection is on face': ({ context: { selectionRanges }, event }) => {
|
||||||
if (event.type !== 'Enter sketch') return false
|
if (event.type !== 'Enter sketch') return false
|
||||||
if (event.data?.forceNewSketch) return false
|
if (event.data?.forceNewSketch) return false
|
||||||
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast))
|
if (
|
||||||
|
isCursorInFunctionDefinition(
|
||||||
|
kclManager.ast,
|
||||||
|
selectionRanges.graphSelections[0]
|
||||||
|
)
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
return !!isCursorInSketchCommandRange(
|
return !!isCursorInSketchCommandRange(
|
||||||
engineCommandManager.artifactGraph,
|
engineCommandManager.artifactGraph,
|
||||||
@ -635,10 +705,32 @@ export const ModelingMachineProvider = ({
|
|||||||
// this assumes no changes have been made to the sketch besides what we did when entering the sketch
|
// this assumes no changes have been made to the sketch besides what we did when entering the sketch
|
||||||
// i.e. doesn't account for user's adding code themselves, maybe we need store a flag userEditedSinceSketchMode?
|
// i.e. doesn't account for user's adding code themselves, maybe we need store a flag userEditedSinceSketchMode?
|
||||||
const newAst = structuredClone(kclManager.ast)
|
const newAst = structuredClone(kclManager.ast)
|
||||||
const varDecIndex = sketchDetails.sketchPathToNode[1][0]
|
const varDecIndex = sketchDetails.planeNodePath[1][0]
|
||||||
|
|
||||||
|
const varDec = getNodeFromPath<VariableDeclaration>(
|
||||||
|
newAst,
|
||||||
|
sketchDetails.planeNodePath,
|
||||||
|
'VariableDeclaration'
|
||||||
|
)
|
||||||
|
if (err(varDec)) return reject(new Error('No varDec'))
|
||||||
|
const variableName = varDec.node.declaration.id.name
|
||||||
|
let isIdentifierUsed = false
|
||||||
|
traverse(newAst, {
|
||||||
|
enter: (node) => {
|
||||||
|
if (
|
||||||
|
node.type === 'Identifier' &&
|
||||||
|
node.name === variableName
|
||||||
|
) {
|
||||||
|
isIdentifierUsed = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (isIdentifierUsed) return
|
||||||
|
|
||||||
// remove body item at varDecIndex
|
// remove body item at varDecIndex
|
||||||
newAst.body = newAst.body.filter((_, i) => i !== varDecIndex)
|
newAst.body = newAst.body.filter((_, i) => i !== varDecIndex)
|
||||||
await kclManager.executeAstMock(newAst)
|
await kclManager.executeAstMock(newAst)
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(newAst)
|
||||||
}
|
}
|
||||||
sceneInfra.setCallbacks({
|
sceneInfra.setCallbacks({
|
||||||
onClick: () => {},
|
onClick: () => {},
|
||||||
@ -648,7 +740,7 @@ export const ModelingMachineProvider = ({
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
'animate-to-face': fromPromise(async ({ input }) => {
|
'animate-to-face': fromPromise(async ({ input }) => {
|
||||||
if (!input) return undefined
|
if (!input) return null
|
||||||
if (input.type === 'extrudeFace' || input.type === 'offsetPlane') {
|
if (input.type === 'extrudeFace' || input.type === 'offsetPlane') {
|
||||||
const sketched =
|
const sketched =
|
||||||
input.type === 'extrudeFace'
|
input.type === 'extrudeFace'
|
||||||
@ -675,7 +767,9 @@ export const ModelingMachineProvider = ({
|
|||||||
await letEngineAnimateAndSyncCamAfter(engineCommandManager, id)
|
await letEngineAnimateAndSyncCamAfter(engineCommandManager, id)
|
||||||
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||||||
return {
|
return {
|
||||||
sketchPathToNode: pathToNewSketchNode,
|
sketchEntryNodePath: [],
|
||||||
|
planeNodePath: pathToNewSketchNode,
|
||||||
|
sketchNodePaths: [],
|
||||||
zAxis: input.zAxis,
|
zAxis: input.zAxis,
|
||||||
yAxis: input.yAxis,
|
yAxis: input.yAxis,
|
||||||
origin: input.position,
|
origin: input.position,
|
||||||
@ -695,7 +789,9 @@ export const ModelingMachineProvider = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sketchPathToNode: pathToNode,
|
sketchEntryNodePath: [],
|
||||||
|
planeNodePath: pathToNode,
|
||||||
|
sketchNodePaths: [],
|
||||||
zAxis: input.zAxis,
|
zAxis: input.zAxis,
|
||||||
yAxis: input.yAxis,
|
yAxis: input.yAxis,
|
||||||
origin: [0, 0, 0],
|
origin: [0, 0, 0],
|
||||||
@ -703,12 +799,14 @@ export const ModelingMachineProvider = ({
|
|||||||
}),
|
}),
|
||||||
'animate-to-sketch': fromPromise(
|
'animate-to-sketch': fromPromise(
|
||||||
async ({ input: { selectionRanges } }) => {
|
async ({ input: { selectionRanges } }) => {
|
||||||
const sourceRange =
|
const sketchPathToNode =
|
||||||
selectionRanges.graphSelections[0]?.codeRef?.range
|
selectionRanges.graphSelections[0]?.codeRef?.pathToNode
|
||||||
const sketchPathToNode = getNodePathFromSourceRange(
|
const plane = getPlaneFromArtifact(
|
||||||
kclManager.ast,
|
selectionRanges.graphSelections[0].artifact,
|
||||||
sourceRange
|
engineCommandManager.artifactGraph
|
||||||
)
|
)
|
||||||
|
if (err(plane)) return Promise.reject(plane)
|
||||||
|
|
||||||
const info = await getSketchOrientationDetails(
|
const info = await getSketchOrientationDetails(
|
||||||
sketchPathToNode || []
|
sketchPathToNode || []
|
||||||
)
|
)
|
||||||
@ -716,8 +814,17 @@ export const ModelingMachineProvider = ({
|
|||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
info?.sketchDetails?.faceId || ''
|
info?.sketchDetails?.faceId || ''
|
||||||
)
|
)
|
||||||
return {
|
const sketchPaths = getPathsFromArtifact({
|
||||||
|
artifact: selectionRanges.graphSelections[0].artifact,
|
||||||
sketchPathToNode: sketchPathToNode || [],
|
sketchPathToNode: sketchPathToNode || [],
|
||||||
|
})
|
||||||
|
if (err(sketchPaths)) return Promise.reject(sketchPaths)
|
||||||
|
if (!plane.codeRef)
|
||||||
|
return Promise.reject(new Error('No plane codeRef'))
|
||||||
|
return {
|
||||||
|
sketchEntryNodePath: sketchPathToNode || [],
|
||||||
|
sketchNodePaths: sketchPaths,
|
||||||
|
planeNodePath: plane.codeRef.pathToNode,
|
||||||
zAxis: info.sketchDetails.zAxis || null,
|
zAxis: info.sketchDetails.zAxis || null,
|
||||||
yAxis: info.sketchDetails.yAxis || null,
|
yAxis: info.sketchDetails.yAxis || null,
|
||||||
origin: info.sketchDetails.origin.map(
|
origin: info.sketchDetails.origin.map(
|
||||||
@ -729,7 +836,7 @@ export const ModelingMachineProvider = ({
|
|||||||
|
|
||||||
'Get horizontal info': fromPromise(
|
'Get horizontal info': fromPromise(
|
||||||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||||||
const { modifiedAst, pathToNodeMap } =
|
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
|
||||||
await applyConstraintHorzVertDistance({
|
await applyConstraintHorzVertDistance({
|
||||||
constraint: 'setHorzDistance',
|
constraint: 'setHorzDistance',
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -741,13 +848,23 @@ export const ModelingMachineProvider = ({
|
|||||||
|
|
||||||
if (!sketchDetails)
|
if (!sketchDetails)
|
||||||
return Promise.reject(new Error('No sketch details'))
|
return Promise.reject(new Error('No sketch details'))
|
||||||
const updatedPathToNode = updatePathToNodeFromMap(
|
|
||||||
sketchDetails.sketchPathToNode,
|
const {
|
||||||
pathToNodeMap
|
updatedSketchEntryNodePath,
|
||||||
)
|
updatedSketchNodePaths,
|
||||||
|
updatedPlaneNodePath,
|
||||||
|
} = updateSketchDetailsNodePaths({
|
||||||
|
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||||||
|
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||||||
|
planeNodePath: sketchDetails.planeNodePath,
|
||||||
|
exprInsertIndex,
|
||||||
|
})
|
||||||
|
|
||||||
const updatedAst =
|
const updatedAst =
|
||||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||||
updatedPathToNode,
|
updatedSketchEntryNodePath,
|
||||||
|
updatedSketchNodePaths,
|
||||||
|
updatedPlaneNodePath,
|
||||||
_modifiedAst,
|
_modifiedAst,
|
||||||
sketchDetails.zAxis,
|
sketchDetails.zAxis,
|
||||||
sketchDetails.yAxis,
|
sketchDetails.yAxis,
|
||||||
@ -768,13 +885,15 @@ export const ModelingMachineProvider = ({
|
|||||||
return {
|
return {
|
||||||
selectionType: 'completeSelection',
|
selectionType: 'completeSelection',
|
||||||
selection,
|
selection,
|
||||||
updatedPathToNode,
|
updatedSketchEntryNodePath,
|
||||||
|
updatedSketchNodePaths,
|
||||||
|
updatedPlaneNodePath,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
'Get vertical info': fromPromise(
|
'Get vertical info': fromPromise(
|
||||||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||||||
const { modifiedAst, pathToNodeMap } =
|
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
|
||||||
await applyConstraintHorzVertDistance({
|
await applyConstraintHorzVertDistance({
|
||||||
constraint: 'setVertDistance',
|
constraint: 'setVertDistance',
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -785,13 +904,23 @@ export const ModelingMachineProvider = ({
|
|||||||
const _modifiedAst = pResult.program
|
const _modifiedAst = pResult.program
|
||||||
if (!sketchDetails)
|
if (!sketchDetails)
|
||||||
return Promise.reject(new Error('No sketch details'))
|
return Promise.reject(new Error('No sketch details'))
|
||||||
const updatedPathToNode = updatePathToNodeFromMap(
|
|
||||||
sketchDetails.sketchPathToNode,
|
const {
|
||||||
pathToNodeMap
|
updatedSketchEntryNodePath,
|
||||||
)
|
updatedSketchNodePaths,
|
||||||
|
updatedPlaneNodePath,
|
||||||
|
} = updateSketchDetailsNodePaths({
|
||||||
|
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||||||
|
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||||||
|
planeNodePath: sketchDetails.planeNodePath,
|
||||||
|
exprInsertIndex,
|
||||||
|
})
|
||||||
|
|
||||||
const updatedAst =
|
const updatedAst =
|
||||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||||
updatedPathToNode,
|
updatedSketchEntryNodePath,
|
||||||
|
updatedSketchNodePaths,
|
||||||
|
updatedPlaneNodePath,
|
||||||
_modifiedAst,
|
_modifiedAst,
|
||||||
sketchDetails.zAxis,
|
sketchDetails.zAxis,
|
||||||
sketchDetails.yAxis,
|
sketchDetails.yAxis,
|
||||||
@ -812,7 +941,9 @@ export const ModelingMachineProvider = ({
|
|||||||
return {
|
return {
|
||||||
selectionType: 'completeSelection',
|
selectionType: 'completeSelection',
|
||||||
selection,
|
selection,
|
||||||
updatedPathToNode,
|
updatedSketchEntryNodePath,
|
||||||
|
updatedSketchNodePaths,
|
||||||
|
updatedPlaneNodePath,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@ -822,14 +953,15 @@ export const ModelingMachineProvider = ({
|
|||||||
selectionRanges,
|
selectionRanges,
|
||||||
})
|
})
|
||||||
if (err(info)) return Promise.reject(info)
|
if (err(info)) return Promise.reject(info)
|
||||||
const { modifiedAst, pathToNodeMap } = await (info.enabled
|
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
|
||||||
? applyConstraintAngleBetween({
|
await (info.enabled
|
||||||
selectionRanges,
|
? applyConstraintAngleBetween({
|
||||||
})
|
selectionRanges,
|
||||||
: applyConstraintAngleLength({
|
})
|
||||||
selectionRanges,
|
: applyConstraintAngleLength({
|
||||||
angleOrLength: 'setAngle',
|
selectionRanges,
|
||||||
}))
|
angleOrLength: 'setAngle',
|
||||||
|
}))
|
||||||
const pResult = parse(recast(modifiedAst))
|
const pResult = parse(recast(modifiedAst))
|
||||||
if (trap(pResult) || !resultIsOk(pResult))
|
if (trap(pResult) || !resultIsOk(pResult))
|
||||||
return Promise.reject(new Error('Unexpected compilation error'))
|
return Promise.reject(new Error('Unexpected compilation error'))
|
||||||
@ -838,13 +970,23 @@ export const ModelingMachineProvider = ({
|
|||||||
|
|
||||||
if (!sketchDetails)
|
if (!sketchDetails)
|
||||||
return Promise.reject(new Error('No sketch details'))
|
return Promise.reject(new Error('No sketch details'))
|
||||||
const updatedPathToNode = updatePathToNodeFromMap(
|
|
||||||
sketchDetails.sketchPathToNode,
|
const {
|
||||||
pathToNodeMap
|
updatedSketchEntryNodePath,
|
||||||
)
|
updatedSketchNodePaths,
|
||||||
|
updatedPlaneNodePath,
|
||||||
|
} = updateSketchDetailsNodePaths({
|
||||||
|
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||||||
|
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||||||
|
planeNodePath: sketchDetails.planeNodePath,
|
||||||
|
exprInsertIndex,
|
||||||
|
})
|
||||||
|
|
||||||
const updatedAst =
|
const updatedAst =
|
||||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||||
updatedPathToNode,
|
updatedSketchEntryNodePath,
|
||||||
|
updatedSketchNodePaths,
|
||||||
|
updatedPlaneNodePath,
|
||||||
_modifiedAst,
|
_modifiedAst,
|
||||||
sketchDetails.zAxis,
|
sketchDetails.zAxis,
|
||||||
sketchDetails.yAxis,
|
sketchDetails.yAxis,
|
||||||
@ -865,29 +1007,47 @@ export const ModelingMachineProvider = ({
|
|||||||
return {
|
return {
|
||||||
selectionType: 'completeSelection',
|
selectionType: 'completeSelection',
|
||||||
selection,
|
selection,
|
||||||
updatedPathToNode,
|
updatedSketchEntryNodePath,
|
||||||
|
updatedSketchNodePaths,
|
||||||
|
updatedPlaneNodePath,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
'Get length info': fromPromise(
|
astConstrainLength: fromPromise(
|
||||||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
async ({
|
||||||
const { modifiedAst, pathToNodeMap } =
|
input: { selectionRanges, sketchDetails, lengthValue },
|
||||||
await applyConstraintAngleLength({
|
}) => {
|
||||||
selectionRanges,
|
if (!lengthValue)
|
||||||
})
|
return Promise.reject(new Error('No length value'))
|
||||||
|
const constraintResult = await applyConstraintLength({
|
||||||
|
selectionRanges,
|
||||||
|
length: lengthValue,
|
||||||
|
})
|
||||||
|
if (err(constraintResult)) return Promise.reject(constraintResult)
|
||||||
|
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
|
||||||
|
constraintResult
|
||||||
const pResult = parse(recast(modifiedAst))
|
const pResult = parse(recast(modifiedAst))
|
||||||
if (trap(pResult) || !resultIsOk(pResult))
|
if (trap(pResult) || !resultIsOk(pResult))
|
||||||
return Promise.reject(new Error('Unexpected compilation error'))
|
return Promise.reject(new Error('Unexpected compilation error'))
|
||||||
const _modifiedAst = pResult.program
|
const _modifiedAst = pResult.program
|
||||||
if (!sketchDetails)
|
if (!sketchDetails)
|
||||||
return Promise.reject(new Error('No sketch details'))
|
return Promise.reject(new Error('No sketch details'))
|
||||||
const updatedPathToNode = updatePathToNodeFromMap(
|
|
||||||
sketchDetails.sketchPathToNode,
|
const {
|
||||||
pathToNodeMap
|
updatedSketchEntryNodePath,
|
||||||
)
|
updatedSketchNodePaths,
|
||||||
|
updatedPlaneNodePath,
|
||||||
|
} = updateSketchDetailsNodePaths({
|
||||||
|
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||||||
|
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||||||
|
planeNodePath: sketchDetails.planeNodePath,
|
||||||
|
exprInsertIndex,
|
||||||
|
})
|
||||||
const updatedAst =
|
const updatedAst =
|
||||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||||
updatedPathToNode,
|
updatedSketchEntryNodePath,
|
||||||
|
updatedSketchNodePaths,
|
||||||
|
updatedPlaneNodePath,
|
||||||
_modifiedAst,
|
_modifiedAst,
|
||||||
sketchDetails.zAxis,
|
sketchDetails.zAxis,
|
||||||
sketchDetails.yAxis,
|
sketchDetails.yAxis,
|
||||||
@ -908,13 +1068,15 @@ export const ModelingMachineProvider = ({
|
|||||||
return {
|
return {
|
||||||
selectionType: 'completeSelection',
|
selectionType: 'completeSelection',
|
||||||
selection,
|
selection,
|
||||||
updatedPathToNode,
|
updatedSketchEntryNodePath,
|
||||||
|
updatedSketchNodePaths,
|
||||||
|
updatedPlaneNodePath,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
'Get perpendicular distance info': fromPromise(
|
'Get perpendicular distance info': fromPromise(
|
||||||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||||||
const { modifiedAst, pathToNodeMap } =
|
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
|
||||||
await applyConstraintIntersect({
|
await applyConstraintIntersect({
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
})
|
})
|
||||||
@ -924,13 +1086,22 @@ export const ModelingMachineProvider = ({
|
|||||||
const _modifiedAst = pResult.program
|
const _modifiedAst = pResult.program
|
||||||
if (!sketchDetails)
|
if (!sketchDetails)
|
||||||
return Promise.reject(new Error('No sketch details'))
|
return Promise.reject(new Error('No sketch details'))
|
||||||
const updatedPathToNode = updatePathToNodeFromMap(
|
|
||||||
sketchDetails.sketchPathToNode,
|
const {
|
||||||
pathToNodeMap
|
updatedSketchEntryNodePath,
|
||||||
)
|
updatedSketchNodePaths,
|
||||||
|
updatedPlaneNodePath,
|
||||||
|
} = updateSketchDetailsNodePaths({
|
||||||
|
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||||||
|
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||||||
|
planeNodePath: sketchDetails.planeNodePath,
|
||||||
|
exprInsertIndex,
|
||||||
|
})
|
||||||
const updatedAst =
|
const updatedAst =
|
||||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||||
updatedPathToNode,
|
updatedSketchEntryNodePath,
|
||||||
|
updatedSketchNodePaths,
|
||||||
|
updatedPlaneNodePath,
|
||||||
_modifiedAst,
|
_modifiedAst,
|
||||||
sketchDetails.zAxis,
|
sketchDetails.zAxis,
|
||||||
sketchDetails.yAxis,
|
sketchDetails.yAxis,
|
||||||
@ -951,13 +1122,15 @@ export const ModelingMachineProvider = ({
|
|||||||
return {
|
return {
|
||||||
selectionType: 'completeSelection',
|
selectionType: 'completeSelection',
|
||||||
selection,
|
selection,
|
||||||
updatedPathToNode,
|
updatedSketchEntryNodePath,
|
||||||
|
updatedSketchNodePaths,
|
||||||
|
updatedPlaneNodePath,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
'Get ABS X info': fromPromise(
|
'Get ABS X info': fromPromise(
|
||||||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||||||
const { modifiedAst, pathToNodeMap } =
|
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
|
||||||
await applyConstraintAbsDistance({
|
await applyConstraintAbsDistance({
|
||||||
constraint: 'xAbs',
|
constraint: 'xAbs',
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -968,13 +1141,22 @@ export const ModelingMachineProvider = ({
|
|||||||
const _modifiedAst = pResult.program
|
const _modifiedAst = pResult.program
|
||||||
if (!sketchDetails)
|
if (!sketchDetails)
|
||||||
return Promise.reject(new Error('No sketch details'))
|
return Promise.reject(new Error('No sketch details'))
|
||||||
const updatedPathToNode = updatePathToNodeFromMap(
|
|
||||||
sketchDetails.sketchPathToNode,
|
const {
|
||||||
pathToNodeMap
|
updatedSketchEntryNodePath,
|
||||||
)
|
updatedSketchNodePaths,
|
||||||
|
updatedPlaneNodePath,
|
||||||
|
} = updateSketchDetailsNodePaths({
|
||||||
|
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||||||
|
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||||||
|
planeNodePath: sketchDetails.planeNodePath,
|
||||||
|
exprInsertIndex,
|
||||||
|
})
|
||||||
const updatedAst =
|
const updatedAst =
|
||||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||||
updatedPathToNode,
|
updatedSketchEntryNodePath,
|
||||||
|
updatedSketchNodePaths,
|
||||||
|
updatedPlaneNodePath,
|
||||||
_modifiedAst,
|
_modifiedAst,
|
||||||
sketchDetails.zAxis,
|
sketchDetails.zAxis,
|
||||||
sketchDetails.yAxis,
|
sketchDetails.yAxis,
|
||||||
@ -995,13 +1177,15 @@ export const ModelingMachineProvider = ({
|
|||||||
return {
|
return {
|
||||||
selectionType: 'completeSelection',
|
selectionType: 'completeSelection',
|
||||||
selection,
|
selection,
|
||||||
updatedPathToNode,
|
updatedSketchEntryNodePath,
|
||||||
|
updatedSketchNodePaths,
|
||||||
|
updatedPlaneNodePath,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
'Get ABS Y info': fromPromise(
|
'Get ABS Y info': fromPromise(
|
||||||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||||||
const { modifiedAst, pathToNodeMap } =
|
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
|
||||||
await applyConstraintAbsDistance({
|
await applyConstraintAbsDistance({
|
||||||
constraint: 'yAbs',
|
constraint: 'yAbs',
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -1012,13 +1196,22 @@ export const ModelingMachineProvider = ({
|
|||||||
const _modifiedAst = pResult.program
|
const _modifiedAst = pResult.program
|
||||||
if (!sketchDetails)
|
if (!sketchDetails)
|
||||||
return Promise.reject(new Error('No sketch details'))
|
return Promise.reject(new Error('No sketch details'))
|
||||||
const updatedPathToNode = updatePathToNodeFromMap(
|
|
||||||
sketchDetails.sketchPathToNode,
|
const {
|
||||||
pathToNodeMap
|
updatedSketchEntryNodePath,
|
||||||
)
|
updatedSketchNodePaths,
|
||||||
|
updatedPlaneNodePath,
|
||||||
|
} = updateSketchDetailsNodePaths({
|
||||||
|
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||||||
|
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||||||
|
planeNodePath: sketchDetails.planeNodePath,
|
||||||
|
exprInsertIndex,
|
||||||
|
})
|
||||||
const updatedAst =
|
const updatedAst =
|
||||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||||
updatedPathToNode,
|
updatedSketchEntryNodePath,
|
||||||
|
updatedSketchNodePaths,
|
||||||
|
updatedPlaneNodePath,
|
||||||
_modifiedAst,
|
_modifiedAst,
|
||||||
sketchDetails.zAxis,
|
sketchDetails.zAxis,
|
||||||
sketchDetails.yAxis,
|
sketchDetails.yAxis,
|
||||||
@ -1039,42 +1232,109 @@ export const ModelingMachineProvider = ({
|
|||||||
return {
|
return {
|
||||||
selectionType: 'completeSelection',
|
selectionType: 'completeSelection',
|
||||||
selection,
|
selection,
|
||||||
updatedPathToNode,
|
updatedSketchEntryNodePath,
|
||||||
|
updatedSketchNodePaths,
|
||||||
|
updatedPlaneNodePath,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
'Get convert to variable info': fromPromise(
|
'Apply named value constraint': fromPromise(
|
||||||
async ({ input: { selectionRanges, sketchDetails, data } }) => {
|
async ({ input: { selectionRanges, sketchDetails, data } }) => {
|
||||||
if (!sketchDetails)
|
if (!sketchDetails) {
|
||||||
return Promise.reject(new Error('No sketch details'))
|
return Promise.reject(new Error('No sketch details'))
|
||||||
const { variableName } = await getVarNameModal({
|
}
|
||||||
valueName: data?.variableName || 'var',
|
if (!data) {
|
||||||
})
|
return Promise.reject(new Error('No data from command flow'))
|
||||||
|
}
|
||||||
let pResult = parse(recast(kclManager.ast))
|
let pResult = parse(recast(kclManager.ast))
|
||||||
if (trap(pResult) || !resultIsOk(pResult))
|
if (trap(pResult) || !resultIsOk(pResult))
|
||||||
return Promise.reject(new Error('Unexpected compilation error'))
|
return Promise.reject(new Error('Unexpected compilation error'))
|
||||||
let parsed = pResult.program
|
let parsed = pResult.program
|
||||||
|
|
||||||
const { modifiedAst: _modifiedAst, pathToReplacedNode } =
|
let result: {
|
||||||
moveValueIntoNewVariablePath(
|
modifiedAst: Node<Program>
|
||||||
parsed,
|
pathToReplaced: PathToNode | null
|
||||||
kclManager.programMemory,
|
exprInsertIndex: number
|
||||||
data?.pathToNode || [],
|
} = {
|
||||||
variableName
|
modifiedAst: parsed,
|
||||||
|
pathToReplaced: null,
|
||||||
|
exprInsertIndex: -1,
|
||||||
|
}
|
||||||
|
// If the user provided a constant name,
|
||||||
|
// we need to insert the named constant
|
||||||
|
// and then replace the node with the constant's name.
|
||||||
|
if ('variableName' in data.namedValue) {
|
||||||
|
const astAfterReplacement = replaceValueAtNodePath({
|
||||||
|
ast: parsed,
|
||||||
|
pathToNode: data.currentValue.pathToNode,
|
||||||
|
newExpressionString: data.namedValue.variableName,
|
||||||
|
})
|
||||||
|
if (trap(astAfterReplacement)) {
|
||||||
|
return Promise.reject(astAfterReplacement)
|
||||||
|
}
|
||||||
|
const parseResultAfterInsertion = parse(
|
||||||
|
recast(
|
||||||
|
insertNamedConstant({
|
||||||
|
node: astAfterReplacement.modifiedAst,
|
||||||
|
newExpression: data.namedValue,
|
||||||
|
})
|
||||||
|
)
|
||||||
)
|
)
|
||||||
pResult = parse(recast(_modifiedAst))
|
if (
|
||||||
|
trap(parseResultAfterInsertion) ||
|
||||||
|
!resultIsOk(parseResultAfterInsertion)
|
||||||
|
)
|
||||||
|
return Promise.reject(parseResultAfterInsertion)
|
||||||
|
result = {
|
||||||
|
modifiedAst: parseResultAfterInsertion.program,
|
||||||
|
pathToReplaced: astAfterReplacement.pathToReplaced,
|
||||||
|
exprInsertIndex: astAfterReplacement.exprInsertIndex,
|
||||||
|
}
|
||||||
|
} else if ('valueText' in data.namedValue) {
|
||||||
|
// If they didn't provide a constant name,
|
||||||
|
// just replace the node with the value.
|
||||||
|
const astAfterReplacement = replaceValueAtNodePath({
|
||||||
|
ast: parsed,
|
||||||
|
pathToNode: data.currentValue.pathToNode,
|
||||||
|
newExpressionString: data.namedValue.valueText,
|
||||||
|
})
|
||||||
|
if (trap(astAfterReplacement)) {
|
||||||
|
return Promise.reject(astAfterReplacement)
|
||||||
|
}
|
||||||
|
// The `replacer` function returns a pathToNode that assumes
|
||||||
|
// an identifier is also being inserted into the AST, creating an off-by-one error.
|
||||||
|
// This corrects that error, but TODO we should fix this upstream
|
||||||
|
// to avoid this kind of error in the future.
|
||||||
|
astAfterReplacement.pathToReplaced[1][0] =
|
||||||
|
(astAfterReplacement.pathToReplaced[1][0] as number) - 1
|
||||||
|
result = astAfterReplacement
|
||||||
|
}
|
||||||
|
|
||||||
|
pResult = parse(recast(result.modifiedAst))
|
||||||
if (trap(pResult) || !resultIsOk(pResult))
|
if (trap(pResult) || !resultIsOk(pResult))
|
||||||
return Promise.reject(new Error('Unexpected compilation error'))
|
return Promise.reject(new Error('Unexpected compilation error'))
|
||||||
parsed = pResult.program
|
parsed = pResult.program
|
||||||
|
|
||||||
if (trap(parsed)) return Promise.reject(parsed)
|
if (trap(parsed)) return Promise.reject(parsed)
|
||||||
parsed = parsed as Node<Program>
|
parsed = parsed as Node<Program>
|
||||||
if (!pathToReplacedNode)
|
if (!result.pathToReplaced)
|
||||||
return Promise.reject(new Error('No path to replaced node'))
|
return Promise.reject(new Error('No path to replaced node'))
|
||||||
|
const {
|
||||||
|
updatedSketchEntryNodePath,
|
||||||
|
updatedSketchNodePaths,
|
||||||
|
updatedPlaneNodePath,
|
||||||
|
} = updateSketchDetailsNodePaths({
|
||||||
|
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||||||
|
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||||||
|
planeNodePath: sketchDetails.planeNodePath,
|
||||||
|
exprInsertIndex: result.exprInsertIndex,
|
||||||
|
})
|
||||||
|
|
||||||
const updatedAst =
|
const updatedAst =
|
||||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||||
pathToReplacedNode || [],
|
updatedSketchEntryNodePath,
|
||||||
|
updatedSketchNodePaths,
|
||||||
|
updatedPlaneNodePath,
|
||||||
parsed,
|
parsed,
|
||||||
sketchDetails.zAxis,
|
sketchDetails.zAxis,
|
||||||
sketchDetails.yAxis,
|
sketchDetails.yAxis,
|
||||||
@ -1087,7 +1347,7 @@ export const ModelingMachineProvider = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const selection = updateSelections(
|
const selection = updateSelections(
|
||||||
{ 0: pathToReplacedNode },
|
{ 0: result.pathToReplaced },
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
updatedAst.newAst
|
updatedAst.newAst
|
||||||
)
|
)
|
||||||
@ -1095,7 +1355,140 @@ export const ModelingMachineProvider = ({
|
|||||||
return {
|
return {
|
||||||
selectionType: 'completeSelection',
|
selectionType: 'completeSelection',
|
||||||
selection,
|
selection,
|
||||||
updatedPathToNode: pathToReplacedNode,
|
updatedSketchEntryNodePath,
|
||||||
|
updatedSketchNodePaths,
|
||||||
|
updatedPlaneNodePath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
'set-up-draft-circle': fromPromise(
|
||||||
|
async ({ input: { sketchDetails, data } }) => {
|
||||||
|
if (!sketchDetails || !data)
|
||||||
|
return reject('No sketch details or data')
|
||||||
|
await sceneEntitiesManager.tearDownSketch({ removeAxis: false })
|
||||||
|
|
||||||
|
const result = await sceneEntitiesManager.setupDraftCircle(
|
||||||
|
sketchDetails.sketchEntryNodePath,
|
||||||
|
sketchDetails.sketchNodePaths,
|
||||||
|
sketchDetails.planeNodePath,
|
||||||
|
sketchDetails.zAxis,
|
||||||
|
sketchDetails.yAxis,
|
||||||
|
sketchDetails.origin,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
if (err(result)) return reject(result)
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
),
|
||||||
|
'set-up-draft-rectangle': fromPromise(
|
||||||
|
async ({ input: { sketchDetails, data } }) => {
|
||||||
|
if (!sketchDetails || !data)
|
||||||
|
return reject('No sketch details or data')
|
||||||
|
await sceneEntitiesManager.tearDownSketch({ removeAxis: false })
|
||||||
|
|
||||||
|
const result = await sceneEntitiesManager.setupDraftRectangle(
|
||||||
|
sketchDetails.sketchEntryNodePath,
|
||||||
|
sketchDetails.sketchNodePaths,
|
||||||
|
sketchDetails.planeNodePath,
|
||||||
|
sketchDetails.zAxis,
|
||||||
|
sketchDetails.yAxis,
|
||||||
|
sketchDetails.origin,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
if (err(result)) return reject(result)
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
),
|
||||||
|
'set-up-draft-center-rectangle': fromPromise(
|
||||||
|
async ({ input: { sketchDetails, data } }) => {
|
||||||
|
if (!sketchDetails || !data)
|
||||||
|
return reject('No sketch details or data')
|
||||||
|
await sceneEntitiesManager.tearDownSketch({ removeAxis: false })
|
||||||
|
const result = await sceneEntitiesManager.setupDraftCenterRectangle(
|
||||||
|
sketchDetails.sketchEntryNodePath,
|
||||||
|
sketchDetails.sketchNodePaths,
|
||||||
|
sketchDetails.planeNodePath,
|
||||||
|
sketchDetails.zAxis,
|
||||||
|
sketchDetails.yAxis,
|
||||||
|
sketchDetails.origin,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
if (err(result)) return reject(result)
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
),
|
||||||
|
'setup-client-side-sketch-segments': fromPromise(
|
||||||
|
async ({ input: { sketchDetails, selectionRanges } }) => {
|
||||||
|
if (!sketchDetails) return
|
||||||
|
if (!sketchDetails.sketchEntryNodePath.length) return
|
||||||
|
if (Object.keys(sceneEntitiesManager.activeSegments).length > 0) {
|
||||||
|
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
|
||||||
|
}
|
||||||
|
sceneInfra.resetMouseListeners()
|
||||||
|
await sceneEntitiesManager.setupSketch({
|
||||||
|
sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [],
|
||||||
|
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||||||
|
forward: sketchDetails.zAxis,
|
||||||
|
up: sketchDetails.yAxis,
|
||||||
|
position: sketchDetails.origin,
|
||||||
|
maybeModdedAst: kclManager.ast,
|
||||||
|
selectionRanges,
|
||||||
|
})
|
||||||
|
sceneInfra.resetMouseListeners()
|
||||||
|
|
||||||
|
sceneEntitiesManager.setupSketchIdleCallbacks({
|
||||||
|
sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [],
|
||||||
|
forward: sketchDetails.zAxis,
|
||||||
|
up: sketchDetails.yAxis,
|
||||||
|
position: sketchDetails.origin,
|
||||||
|
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||||||
|
planeNodePath: sketchDetails.planeNodePath,
|
||||||
|
})
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
),
|
||||||
|
'split-sketch-pipe-if-needed': fromPromise(
|
||||||
|
async ({ input: { sketchDetails } }) => {
|
||||||
|
if (!sketchDetails) return reject('No sketch details')
|
||||||
|
const existingSketchInfoNoOp = {
|
||||||
|
updatedEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||||||
|
updatedSketchNodePaths: sketchDetails.sketchNodePaths,
|
||||||
|
updatedPlaneNodePath: sketchDetails.planeNodePath,
|
||||||
|
} as const
|
||||||
|
if (
|
||||||
|
!sketchDetails.sketchNodePaths.length &&
|
||||||
|
sketchDetails.planeNodePath.length
|
||||||
|
) {
|
||||||
|
// new sketch, no profiles yet
|
||||||
|
return existingSketchInfoNoOp
|
||||||
|
}
|
||||||
|
const doesNeedSplitting = doesSketchPipeNeedSplitting(
|
||||||
|
kclManager.ast,
|
||||||
|
sketchDetails.sketchEntryNodePath
|
||||||
|
)
|
||||||
|
if (err(doesNeedSplitting)) return reject(doesNeedSplitting)
|
||||||
|
if (!doesNeedSplitting) return existingSketchInfoNoOp
|
||||||
|
|
||||||
|
const splitResult = splitPipedProfile(
|
||||||
|
kclManager.ast,
|
||||||
|
sketchDetails.sketchEntryNodePath
|
||||||
|
)
|
||||||
|
if (err(splitResult)) return reject(splitResult)
|
||||||
|
|
||||||
|
await kclManager.executeAstMock(splitResult.modifiedAst)
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||||
|
splitResult.modifiedAst
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
updatedEntryNodePath: splitResult.pathToProfile,
|
||||||
|
updatedSketchNodePaths: [splitResult.pathToProfile],
|
||||||
|
updatedPlaneNodePath: sketchDetails.planeNodePath,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
@ -6,6 +6,7 @@ import Tooltip from 'components/Tooltip'
|
|||||||
import { CustomIconName } from 'components/CustomIcon'
|
import { CustomIconName } from 'components/CustomIcon'
|
||||||
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { ActionIcon } from 'components/ActionIcon'
|
import { ActionIcon } from 'components/ActionIcon'
|
||||||
|
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||||
|
|
||||||
export interface ModelingPaneProps {
|
export interface ModelingPaneProps {
|
||||||
id: string
|
id: string
|
||||||
@ -70,13 +71,13 @@ export const ModelingPane = ({
|
|||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const onboardingStatus = settings.context.app.onboardingStatus
|
const onboardingStatus = settings.context.app.onboardingStatus
|
||||||
const pointerEventsCssClass =
|
const pointerEventsCssClass =
|
||||||
onboardingStatus.current === 'camera'
|
onboardingStatus.current === onboardingPaths.CAMERA
|
||||||
? 'pointer-events-none '
|
? 'pointer-events-none '
|
||||||
: 'pointer-events-auto '
|
: 'pointer-events-auto '
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
{...props}
|
{...props}
|
||||||
title={title && typeof title === 'string' ? title : ''}
|
aria-label={title && typeof title === 'string' ? title : ''}
|
||||||
data-testid={detailsTestId}
|
data-testid={detailsTestId}
|
||||||
id={id}
|
id={id}
|
||||||
className={
|
className={
|
||||||
|
@ -19,6 +19,7 @@ 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 { onboardingPaths } from 'routes/Onboarding/paths'
|
||||||
|
|
||||||
interface ModelingSidebarProps {
|
interface ModelingSidebarProps {
|
||||||
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
||||||
@ -41,7 +42,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
const onboardingStatus = settings.context.app.onboardingStatus
|
const onboardingStatus = settings.context.app.onboardingStatus
|
||||||
const { send, context } = useModelingContext()
|
const { send, context } = useModelingContext()
|
||||||
const pointerEventsCssClass =
|
const pointerEventsCssClass =
|
||||||
onboardingStatus.current === 'camera' ||
|
onboardingStatus.current === onboardingPaths.CAMERA ||
|
||||||
context.store?.openPanes.length === 0
|
context.store?.openPanes.length === 0
|
||||||
? 'pointer-events-none '
|
? 'pointer-events-none '
|
||||||
: 'pointer-events-auto '
|
: 'pointer-events-auto '
|
||||||
|
@ -10,7 +10,7 @@ interface AllKeybindingsFieldsProps {}
|
|||||||
|
|
||||||
export const AllKeybindingsFields = forwardRef(
|
export const AllKeybindingsFields = forwardRef(
|
||||||
(
|
(
|
||||||
props: AllKeybindingsFieldsProps,
|
_props: AllKeybindingsFieldsProps,
|
||||||
scrollRef: ForwardedRef<HTMLDivElement>
|
scrollRef: ForwardedRef<HTMLDivElement>
|
||||||
) => {
|
) => {
|
||||||
// This is how we will get the interaction map from the context
|
// This is how we will get the interaction map from the context
|
||||||
@ -25,7 +25,7 @@ export const AllKeybindingsFields = forwardRef(
|
|||||||
.map(([category, categoryItems]) => (
|
.map(([category, categoryItems]) => (
|
||||||
<div className="flex flex-col gap-4 px-2 pr-4">
|
<div className="flex flex-col gap-4 px-2 pr-4">
|
||||||
<h2
|
<h2
|
||||||
id={`category-${category}`}
|
id={`category-${category.replaceAll(/\s/g, '-')}`}
|
||||||
className="text-xl mt-6 first-of-type:mt-0 capitalize font-bold"
|
className="text-xl mt-6 first-of-type:mt-0 capitalize font-bold"
|
||||||
>
|
>
|
||||||
{category}
|
{category}
|
||||||
|
@ -13,7 +13,7 @@ import { isDesktop } from 'lib/isDesktop'
|
|||||||
import { ActionButton } from 'components/ActionButton'
|
import { ActionButton } from 'components/ActionButton'
|
||||||
import { SettingsFieldInput } from './SettingsFieldInput'
|
import { SettingsFieldInput } from './SettingsFieldInput'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { APP_VERSION, PACKAGE_NAME } from 'routes/Settings'
|
import { APP_VERSION, IS_NIGHTLY, getReleaseUrl } from 'routes/Settings'
|
||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
import {
|
import {
|
||||||
createAndOpenNewTutorialProject,
|
createAndOpenNewTutorialProject,
|
||||||
@ -246,10 +246,8 @@ export const AllSettingsFields = forwardRef(
|
|||||||
to inject the version from package.json */}
|
to inject the version from package.json */}
|
||||||
App version {APP_VERSION}.{' '}
|
App version {APP_VERSION}.{' '}
|
||||||
<a
|
<a
|
||||||
onClick={openExternalBrowserIfDesktop(
|
onClick={openExternalBrowserIfDesktop(getReleaseUrl())}
|
||||||
`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`
|
href={getReleaseUrl()}
|
||||||
)}
|
|
||||||
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`}
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
@ -271,7 +269,7 @@ export const AllSettingsFields = forwardRef(
|
|||||||
, and start a discussion if you don't see it! Your feedback will
|
, and start a discussion if you don't see it! Your feedback will
|
||||||
help us prioritize what to build next.
|
help us prioritize what to build next.
|
||||||
</p>
|
</p>
|
||||||
{PACKAGE_NAME.indexOf('-nightly') === -1 && (
|
{!IS_NIGHTLY && (
|
||||||
<p className="max-w-2xl mt-6">
|
<p className="max-w-2xl mt-6">
|
||||||
Want to experience the latest and (hopefully) greatest from our
|
Want to experience the latest and (hopefully) greatest from our
|
||||||
main development branch?{' '}
|
main development branch?{' '}
|
||||||
|
@ -19,7 +19,7 @@ export function KeybindingsSectionsList({
|
|||||||
key={category}
|
key={category}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
scrollRef.current
|
scrollRef.current
|
||||||
?.querySelector(`#category-${category}`)
|
?.querySelector(`#category-${category.replaceAll(/\s/g, '-')}`)
|
||||||
?.scrollIntoView({
|
?.scrollIntoView({
|
||||||
block: 'center',
|
block: 'center',
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { trap } from 'lib/trap'
|
import { trap } from 'lib/trap'
|
||||||
import { useMachine } from '@xstate/react'
|
import { useMachine, useSelector } from '@xstate/react'
|
||||||
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
|
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
|
||||||
import { PATHS, BROWSER_PATH } from 'lib/paths'
|
import { PATHS, BROWSER_PATH } from 'lib/paths'
|
||||||
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
|
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
|
||||||
@ -23,7 +23,6 @@ import {
|
|||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
sceneEntitiesManager,
|
sceneEntitiesManager,
|
||||||
} from 'lib/singletons'
|
} from 'lib/singletons'
|
||||||
import { uuidv4 } from 'lib/utils'
|
|
||||||
import { IndexLoaderData } from 'lib/types'
|
import { IndexLoaderData } from 'lib/types'
|
||||||
import { settings } from 'lib/settings/initialSettings'
|
import { settings } from 'lib/settings/initialSettings'
|
||||||
import {
|
import {
|
||||||
@ -55,11 +54,15 @@ type SettingsAuthContextType = {
|
|||||||
settings: MachineContext<typeof settingsMachine>
|
settings: MachineContext<typeof settingsMachine>
|
||||||
}
|
}
|
||||||
|
|
||||||
// a little hacky for sure, open to changing it
|
/**
|
||||||
// this implies that we should only even have one instance of this provider mounted at any one time
|
* This variable is used to store the last snapshot of the settings context
|
||||||
// but I think that's a safe assumption
|
* for use outside of React, such as in `wasm.ts`. It is updated every time
|
||||||
let settingsStateRef: ContextFrom<typeof settingsMachine> | undefined
|
* the settings machine changes with `useSelector`.
|
||||||
export const getSettingsState = () => settingsStateRef
|
* TODO: when we decouple XState from React, we can just subscribe to the actor directly from `wasm.ts`
|
||||||
|
*/
|
||||||
|
export let lastSettingsContextSnapshot:
|
||||||
|
| ContextFrom<typeof settingsMachine>
|
||||||
|
| undefined
|
||||||
|
|
||||||
export const SettingsAuthContext = createContext({} as SettingsAuthContextType)
|
export const SettingsAuthContext = createContext({} as SettingsAuthContextType)
|
||||||
|
|
||||||
@ -129,27 +132,11 @@ export const SettingsAuthProviderBase = ({
|
|||||||
.setTheme(context.app.theme.current)
|
.setTheme(context.app.theme.current)
|
||||||
.catch(reportRejection)
|
.catch(reportRejection)
|
||||||
},
|
},
|
||||||
setEngineScaleGridVisibility: ({ context }) => {
|
|
||||||
engineCommandManager.setScaleGridVisibility(
|
|
||||||
context.modeling.showScaleGrid.current
|
|
||||||
)
|
|
||||||
},
|
|
||||||
setClientTheme: ({ context }) => {
|
setClientTheme: ({ context }) => {
|
||||||
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
||||||
sceneInfra.theme = opposingTheme
|
sceneInfra.theme = opposingTheme
|
||||||
sceneEntitiesManager.updateSegmentBaseColor(opposingTheme)
|
sceneEntitiesManager.updateSegmentBaseColor(opposingTheme)
|
||||||
},
|
},
|
||||||
setEngineEdges: ({ context }) => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
engineCommandManager.sendSceneCommand({
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd: {
|
|
||||||
type: 'edge_lines_visible' as any, // TODO update kittycad.ts to get this new command type
|
|
||||||
hidden: !context.modeling.highlightEdges.current,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
toastSuccess: ({ event }) => {
|
toastSuccess: ({ event }) => {
|
||||||
if (!('data' in event)) return
|
if (!('data' in event)) return
|
||||||
const eventParts = event.type.replace(/^set./, '').split('.') as [
|
const eventParts = event.type.replace(/^set./, '').split('.') as [
|
||||||
@ -175,17 +162,27 @@ export const SettingsAuthProviderBase = ({
|
|||||||
},
|
},
|
||||||
'Execute AST': ({ context, event }) => {
|
'Execute AST': ({ context, event }) => {
|
||||||
try {
|
try {
|
||||||
|
const relevantSetting = (s: typeof settings) => {
|
||||||
|
return (
|
||||||
|
s.modeling?.defaultUnit?.current !==
|
||||||
|
context.modeling.defaultUnit.current ||
|
||||||
|
s.modeling.showScaleGrid.current !==
|
||||||
|
context.modeling.showScaleGrid.current ||
|
||||||
|
s.modeling?.highlightEdges.current !==
|
||||||
|
context.modeling.highlightEdges.current
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const allSettingsIncludesUnitChange =
|
const allSettingsIncludesUnitChange =
|
||||||
event.type === 'Set all settings' &&
|
event.type === 'Set all settings' &&
|
||||||
event.settings?.modeling?.defaultUnit?.current !==
|
relevantSetting(event.settings)
|
||||||
context.modeling.defaultUnit.current
|
|
||||||
const resetSettingsIncludesUnitChange =
|
const resetSettingsIncludesUnitChange =
|
||||||
event.type === 'Reset settings' &&
|
event.type === 'Reset settings' && relevantSetting(settings)
|
||||||
context.modeling.defaultUnit.current !==
|
|
||||||
settings?.modeling?.defaultUnit?.default
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
event.type === 'set.modeling.defaultUnit' ||
|
event.type === 'set.modeling.defaultUnit' ||
|
||||||
|
event.type === 'set.modeling.showScaleGrid' ||
|
||||||
|
event.type === 'set.modeling.highlightEdges' ||
|
||||||
allSettingsIncludesUnitChange ||
|
allSettingsIncludesUnitChange ||
|
||||||
resetSettingsIncludesUnitChange
|
resetSettingsIncludesUnitChange
|
||||||
) {
|
) {
|
||||||
@ -214,7 +211,10 @@ export const SettingsAuthProviderBase = ({
|
|||||||
}),
|
}),
|
||||||
{ input: loadedSettings }
|
{ input: loadedSettings }
|
||||||
)
|
)
|
||||||
settingsStateRef = settingsState.context
|
// Any time the actor changes, update the settings state for external use
|
||||||
|
useSelector(settingsActor, (s) => {
|
||||||
|
lastSettingsContextSnapshot = s.context
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDesktop()) return
|
if (!isDesktop()) return
|
||||||
|
@ -20,6 +20,7 @@ import { IndexLoaderData } from 'lib/types'
|
|||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { err, reportRejection } from 'lib/trap'
|
import { err, reportRejection } from 'lib/trap'
|
||||||
import { getArtifactOfTypes } from 'lang/std/artifactGraph'
|
import { getArtifactOfTypes } from 'lang/std/artifactGraph'
|
||||||
|
import { ViewControlContextMenu } from './ViewControlMenu'
|
||||||
|
|
||||||
enum StreamState {
|
enum StreamState {
|
||||||
Playing = 'playing',
|
Playing = 'playing',
|
||||||
@ -30,6 +31,7 @@ enum StreamState {
|
|||||||
|
|
||||||
export const Stream = () => {
|
export const Stream = () => {
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const videoWrapperRef = useRef<HTMLDivElement>(null)
|
||||||
const videoRef = useRef<HTMLVideoElement>(null)
|
const videoRef = useRef<HTMLVideoElement>(null)
|
||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const { state, send } = useModelingContext()
|
const { state, send } = useModelingContext()
|
||||||
@ -258,7 +260,7 @@ export const Stream = () => {
|
|||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}, [mediaStream])
|
}, [mediaStream])
|
||||||
|
|
||||||
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
|
const handleClick: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||||
// If we've got no stream or connection, don't do anything
|
// If we've got no stream or connection, don't do anything
|
||||||
if (!isNetworkOkay) return
|
if (!isNetworkOkay) return
|
||||||
if (!videoRef.current) return
|
if (!videoRef.current) return
|
||||||
@ -320,10 +322,11 @@ export const Stream = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={videoWrapperRef}
|
||||||
className="absolute inset-0 z-0"
|
className="absolute inset-0 z-0"
|
||||||
id="stream"
|
id="stream"
|
||||||
data-testid="stream"
|
data-testid="stream"
|
||||||
onClick={handleMouseUp}
|
onClick={handleClick}
|
||||||
onDoubleClick={enterSketchModeIfSelectingSketch}
|
onDoubleClick={enterSketchModeIfSelectingSketch}
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
onContextMenuCapture={(e) => e.preventDefault()}
|
onContextMenuCapture={(e) => e.preventDefault()}
|
||||||
@ -384,6 +387,14 @@ export const Stream = () => {
|
|||||||
</Loading>
|
</Loading>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<ViewControlContextMenu
|
||||||
|
event="mouseup"
|
||||||
|
guard={(e) =>
|
||||||
|
sceneInfra.camControls.wasDragging === false &&
|
||||||
|
btnName(e).right === true
|
||||||
|
}
|
||||||
|
menuTargetElement={videoWrapperRef}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import toast from 'react-hot-toast'
|
|||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
import { Marked } from '@ts-stack/markdown'
|
import { Marked } from '@ts-stack/markdown'
|
||||||
|
import { getReleaseUrl } from 'routes/Settings'
|
||||||
|
|
||||||
export function ToastUpdate({
|
export function ToastUpdate({
|
||||||
version,
|
version,
|
||||||
@ -32,10 +33,8 @@ export function ToastUpdate({
|
|||||||
A new update has downloaded and will be available next time you
|
A new update has downloaded and will be available next time you
|
||||||
start the app. You can view the release notes{' '}
|
start the app. You can view the release notes{' '}
|
||||||
<a
|
<a
|
||||||
onClick={openExternalBrowserIfDesktop(
|
onClick={openExternalBrowserIfDesktop(getReleaseUrl(version))}
|
||||||
`https://github.com/KittyCAD/modeling-app/releases/tag/v${version}`
|
href={getReleaseUrl(version)}
|
||||||
)}
|
|
||||||
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${version}`}
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
|
@ -136,6 +136,7 @@ export async function applyConstraintIntersect({
|
|||||||
}): Promise<{
|
}): Promise<{
|
||||||
modifiedAst: Node<Program>
|
modifiedAst: Node<Program>
|
||||||
pathToNodeMap: PathToNodeMap
|
pathToNodeMap: PathToNodeMap
|
||||||
|
exprInsertIndex: number
|
||||||
}> {
|
}> {
|
||||||
const info = intersectInfo({
|
const info = intersectInfo({
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -174,6 +175,7 @@ export async function applyConstraintIntersect({
|
|||||||
return {
|
return {
|
||||||
modifiedAst,
|
modifiedAst,
|
||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
|
exprInsertIndex: -1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// transform again but forcing certain values
|
// transform again but forcing certain values
|
||||||
@ -192,6 +194,7 @@ export async function applyConstraintIntersect({
|
|||||||
const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } =
|
const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } =
|
||||||
transform2
|
transform2
|
||||||
|
|
||||||
|
let exprInsertIndex = -1
|
||||||
if (variableName) {
|
if (variableName) {
|
||||||
const newBody = [..._modifiedAst.body]
|
const newBody = [..._modifiedAst.body]
|
||||||
newBody.splice(
|
newBody.splice(
|
||||||
@ -204,9 +207,11 @@ export async function applyConstraintIntersect({
|
|||||||
const index = pathToNode.findIndex((a) => a[0] === 'body') + 1
|
const index = pathToNode.findIndex((a) => a[0] === 'body') + 1
|
||||||
pathToNode[index][0] = Number(pathToNode[index][0]) + 1
|
pathToNode[index][0] = Number(pathToNode[index][0]) + 1
|
||||||
})
|
})
|
||||||
|
exprInsertIndex = newVariableInsertIndex
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
modifiedAst: _modifiedAst,
|
modifiedAst: _modifiedAst,
|
||||||
pathToNodeMap: _pathToNodeMap,
|
pathToNodeMap: _pathToNodeMap,
|
||||||
|
exprInsertIndex,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ export function removeConstrainingValuesInfo({
|
|||||||
| Error {
|
| Error {
|
||||||
const _nodes = selectionRanges.graphSelections.map(({ codeRef }) => {
|
const _nodes = selectionRanges.graphSelections.map(({ codeRef }) => {
|
||||||
const tmp = getNodeFromPath<Expr>(kclManager.ast, codeRef.pathToNode)
|
const tmp = getNodeFromPath<Expr>(kclManager.ast, codeRef.pathToNode)
|
||||||
if (err(tmp)) return tmp
|
if (tmp instanceof Error) return tmp
|
||||||
return tmp.node
|
return tmp.node
|
||||||
})
|
})
|
||||||
const _err1 = _nodes.find(err)
|
const _err1 = _nodes.find(err)
|
||||||
|
@ -93,6 +93,7 @@ export async function applyConstraintAbsDistance({
|
|||||||
}): Promise<{
|
}): Promise<{
|
||||||
modifiedAst: Program
|
modifiedAst: Program
|
||||||
pathToNodeMap: PathToNodeMap
|
pathToNodeMap: PathToNodeMap
|
||||||
|
exprInsertIndex: number
|
||||||
}> {
|
}> {
|
||||||
const info = absDistanceInfo({
|
const info = absDistanceInfo({
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -132,6 +133,7 @@ export async function applyConstraintAbsDistance({
|
|||||||
if (err(transform2)) return Promise.reject(transform2)
|
if (err(transform2)) return Promise.reject(transform2)
|
||||||
const { modifiedAst: _modifiedAst, pathToNodeMap } = transform2
|
const { modifiedAst: _modifiedAst, pathToNodeMap } = transform2
|
||||||
|
|
||||||
|
let exprInsertIndex = -1
|
||||||
if (variableName) {
|
if (variableName) {
|
||||||
const newBody = [..._modifiedAst.body]
|
const newBody = [..._modifiedAst.body]
|
||||||
newBody.splice(
|
newBody.splice(
|
||||||
@ -144,8 +146,9 @@ export async function applyConstraintAbsDistance({
|
|||||||
const index = pathToNode.findIndex((a) => a[0] === 'body') + 1
|
const index = pathToNode.findIndex((a) => a[0] === 'body') + 1
|
||||||
pathToNode[index][0] = Number(pathToNode[index][0]) + 1
|
pathToNode[index][0] = Number(pathToNode[index][0]) + 1
|
||||||
})
|
})
|
||||||
|
exprInsertIndex = newVariableInsertIndex
|
||||||
}
|
}
|
||||||
return { modifiedAst: _modifiedAst, pathToNodeMap }
|
return { modifiedAst: _modifiedAst, pathToNodeMap, exprInsertIndex }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyConstraintAxisAlign({
|
export function applyConstraintAxisAlign({
|
||||||
|
@ -86,6 +86,7 @@ export async function applyConstraintAngleBetween({
|
|||||||
}): Promise<{
|
}): Promise<{
|
||||||
modifiedAst: Program
|
modifiedAst: Program
|
||||||
pathToNodeMap: PathToNodeMap
|
pathToNodeMap: PathToNodeMap
|
||||||
|
exprInsertIndex: number
|
||||||
}> {
|
}> {
|
||||||
const info = angleBetweenInfo({ selectionRanges })
|
const info = angleBetweenInfo({ selectionRanges })
|
||||||
if (err(info)) return Promise.reject(info)
|
if (err(info)) return Promise.reject(info)
|
||||||
@ -122,6 +123,7 @@ export async function applyConstraintAngleBetween({
|
|||||||
return {
|
return {
|
||||||
modifiedAst,
|
modifiedAst,
|
||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
|
exprInsertIndex: -1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,6 +143,7 @@ export async function applyConstraintAngleBetween({
|
|||||||
const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } =
|
const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } =
|
||||||
transformed2
|
transformed2
|
||||||
|
|
||||||
|
let exprInsertIndex = -1
|
||||||
if (variableName) {
|
if (variableName) {
|
||||||
const newBody = [..._modifiedAst.body]
|
const newBody = [..._modifiedAst.body]
|
||||||
newBody.splice(
|
newBody.splice(
|
||||||
@ -153,9 +156,11 @@ export async function applyConstraintAngleBetween({
|
|||||||
const index = pathToNode.findIndex((a) => a[0] === 'body') + 1
|
const index = pathToNode.findIndex((a) => a[0] === 'body') + 1
|
||||||
pathToNode[index][0] = Number(pathToNode[index][0]) + 1
|
pathToNode[index][0] = Number(pathToNode[index][0]) + 1
|
||||||
})
|
})
|
||||||
|
exprInsertIndex = newVariableInsertIndex
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
modifiedAst: _modifiedAst,
|
modifiedAst: _modifiedAst,
|
||||||
pathToNodeMap: _pathToNodeMap,
|
pathToNodeMap: _pathToNodeMap,
|
||||||
|
exprInsertIndex,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,15 +87,13 @@ export function horzVertDistanceInfo({
|
|||||||
export async function applyConstraintHorzVertDistance({
|
export async function applyConstraintHorzVertDistance({
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
constraint,
|
constraint,
|
||||||
// TODO align will always be false (covered by synconous applyConstraintHorzVertAlign), remove it
|
|
||||||
isAlign = false,
|
|
||||||
}: {
|
}: {
|
||||||
selectionRanges: Selections
|
selectionRanges: Selections
|
||||||
constraint: 'setHorzDistance' | 'setVertDistance'
|
constraint: 'setHorzDistance' | 'setVertDistance'
|
||||||
isAlign?: false
|
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
modifiedAst: Program
|
modifiedAst: Program
|
||||||
pathToNodeMap: PathToNodeMap
|
pathToNodeMap: PathToNodeMap
|
||||||
|
exprInsertIndex: number
|
||||||
}> {
|
}> {
|
||||||
const info = horzVertDistanceInfo({
|
const info = horzVertDistanceInfo({
|
||||||
selectionRanges: selectionRanges,
|
selectionRanges: selectionRanges,
|
||||||
@ -133,13 +131,12 @@ export async function applyConstraintHorzVertDistance({
|
|||||||
return {
|
return {
|
||||||
modifiedAst,
|
modifiedAst,
|
||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
|
exprInsertIndex: -1,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!isExprBinaryPart(valueNode))
|
if (!isExprBinaryPart(valueNode))
|
||||||
return Promise.reject('Invalid valueNode, is not a BinaryPart')
|
return Promise.reject('Invalid valueNode, is not a BinaryPart')
|
||||||
let finalValue = isAlign
|
let finalValue = removeDoubleNegatives(valueNode, sign, variableName)
|
||||||
? createLiteral(0)
|
|
||||||
: removeDoubleNegatives(valueNode, sign, variableName)
|
|
||||||
// transform again but forcing certain values
|
// transform again but forcing certain values
|
||||||
const transformed = transformSecondarySketchLinesTagFirst({
|
const transformed = transformSecondarySketchLinesTagFirst({
|
||||||
ast: kclManager.ast,
|
ast: kclManager.ast,
|
||||||
@ -152,6 +149,7 @@ export async function applyConstraintHorzVertDistance({
|
|||||||
|
|
||||||
if (err(transformed)) return Promise.reject(transformed)
|
if (err(transformed)) return Promise.reject(transformed)
|
||||||
const { modifiedAst: _modifiedAst, pathToNodeMap } = transformed
|
const { modifiedAst: _modifiedAst, pathToNodeMap } = transformed
|
||||||
|
let exprInsertIndex = -1
|
||||||
if (variableName) {
|
if (variableName) {
|
||||||
const newBody = [..._modifiedAst.body]
|
const newBody = [..._modifiedAst.body]
|
||||||
newBody.splice(
|
newBody.splice(
|
||||||
@ -164,10 +162,12 @@ export async function applyConstraintHorzVertDistance({
|
|||||||
const index = pathToNode.findIndex((a) => a[0] === 'body') + 1
|
const index = pathToNode.findIndex((a) => a[0] === 'body') + 1
|
||||||
pathToNode[index][0] = Number(pathToNode[index][0]) + 1
|
pathToNode[index][0] = Number(pathToNode[index][0]) + 1
|
||||||
})
|
})
|
||||||
|
exprInsertIndex = newVariableInsertIndex
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
modifiedAst: _modifiedAst,
|
modifiedAst: _modifiedAst,
|
||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
|
exprInsertIndex,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
|||||||
import { normaliseAngle } from '../../lib/utils'
|
import { normaliseAngle } from '../../lib/utils'
|
||||||
import { kclManager } from 'lib/singletons'
|
import { kclManager } from 'lib/singletons'
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
|
import { KclCommandValue } from 'lib/commandTypes'
|
||||||
|
|
||||||
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
|
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
|
||||||
|
|
||||||
@ -63,6 +64,67 @@ export function angleLengthInfo({
|
|||||||
return { enabled, transforms }
|
return { enabled, transforms }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function applyConstraintLength({
|
||||||
|
length,
|
||||||
|
selectionRanges,
|
||||||
|
}: {
|
||||||
|
length: KclCommandValue
|
||||||
|
selectionRanges: Selections
|
||||||
|
}): Promise<{
|
||||||
|
modifiedAst: Program
|
||||||
|
pathToNodeMap: PathToNodeMap
|
||||||
|
exprInsertIndex: number
|
||||||
|
}> {
|
||||||
|
const ast = kclManager.ast
|
||||||
|
const angleLength = angleLengthInfo({ selectionRanges })
|
||||||
|
if (err(angleLength)) return Promise.reject(angleLength)
|
||||||
|
const { transforms } = angleLength
|
||||||
|
|
||||||
|
let distanceExpression: Expr = length.valueAst
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To be "constrained", the value must be a binary expression, a named value, or a function call.
|
||||||
|
* If it has a variable name, we need to insert a variable declaration at the correct index.
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
'variableName' in length &&
|
||||||
|
length.variableName &&
|
||||||
|
length.insertIndex !== undefined
|
||||||
|
) {
|
||||||
|
const newBody = [...ast.body]
|
||||||
|
newBody.splice(length.insertIndex, 0, length.variableDeclarationAst)
|
||||||
|
ast.body = newBody
|
||||||
|
distanceExpression = createIdentifier(length.variableName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isExprBinaryPart(distanceExpression)) {
|
||||||
|
return Promise.reject('Invalid valueNode, is not a BinaryPart')
|
||||||
|
}
|
||||||
|
|
||||||
|
const retval = transformAstSketchLines({
|
||||||
|
ast,
|
||||||
|
selectionRanges,
|
||||||
|
transformInfos: transforms,
|
||||||
|
programMemory: kclManager.programMemory,
|
||||||
|
referenceSegName: '',
|
||||||
|
forceValueUsedInTransform: distanceExpression,
|
||||||
|
})
|
||||||
|
if (err(retval)) return Promise.reject(retval)
|
||||||
|
|
||||||
|
const { modifiedAst: _modifiedAst, pathToNodeMap } = retval
|
||||||
|
|
||||||
|
return {
|
||||||
|
modifiedAst: _modifiedAst,
|
||||||
|
pathToNodeMap,
|
||||||
|
exprInsertIndex:
|
||||||
|
'variableName' in length &&
|
||||||
|
length.variableName &&
|
||||||
|
length.insertIndex !== undefined
|
||||||
|
? length.insertIndex
|
||||||
|
: -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function applyConstraintAngleLength({
|
export async function applyConstraintAngleLength({
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
angleOrLength = 'setLength',
|
angleOrLength = 'setLength',
|
||||||
@ -72,6 +134,7 @@ export async function applyConstraintAngleLength({
|
|||||||
}): Promise<{
|
}): Promise<{
|
||||||
modifiedAst: Program
|
modifiedAst: Program
|
||||||
pathToNodeMap: PathToNodeMap
|
pathToNodeMap: PathToNodeMap
|
||||||
|
exprInsertIndex: number
|
||||||
}> {
|
}> {
|
||||||
const angleLength = angleLengthInfo({ selectionRanges, angleOrLength })
|
const angleLength = angleLengthInfo({ selectionRanges, angleOrLength })
|
||||||
if (err(angleLength)) return Promise.reject(angleLength)
|
if (err(angleLength)) return Promise.reject(angleLength)
|
||||||
@ -156,5 +219,6 @@ export async function applyConstraintAngleLength({
|
|||||||
return {
|
return {
|
||||||
modifiedAst: _modifiedAst,
|
modifiedAst: _modifiedAst,
|
||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
|
exprInsertIndex: variableName ? newVariableInsertIndex : -1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,10 @@ export function UnitsMenu() {
|
|||||||
close()
|
close()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{baseUnitLabels[unit]}
|
<span className="flex-1">{baseUnitLabels[unit]}</span>
|
||||||
|
{unit === settings.context.modeling.defaultUnit.current && (
|
||||||
|
<span className="text-chalkboard-60">current</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
66
src/components/ViewControlMenu.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuDivider,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuItemRefresh,
|
||||||
|
ContextMenuProps,
|
||||||
|
} from './ContextMenu'
|
||||||
|
import { AxisNames, VIEW_NAMES_SEMANTIC } from 'lib/constants'
|
||||||
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { sceneInfra } from 'lib/singletons'
|
||||||
|
|
||||||
|
export function useViewControlMenuItems() {
|
||||||
|
const { send: modelingSend } = useModelingContext()
|
||||||
|
const menuItems = useMemo(
|
||||||
|
() => [
|
||||||
|
...Object.entries(VIEW_NAMES_SEMANTIC).map(([axisName, axisSemantic]) => (
|
||||||
|
<ContextMenuItem
|
||||||
|
key={axisName}
|
||||||
|
onClick={() => {
|
||||||
|
sceneInfra.camControls
|
||||||
|
.updateCameraToAxis(axisName as AxisNames)
|
||||||
|
.catch(reportRejection)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{axisSemantic} view
|
||||||
|
</ContextMenuItem>
|
||||||
|
)),
|
||||||
|
<ContextMenuDivider />,
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset view
|
||||||
|
</ContextMenuItem>,
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
modelingSend({ type: 'Center camera on selection' })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Center view on selection
|
||||||
|
</ContextMenuItem>,
|
||||||
|
<ContextMenuDivider />,
|
||||||
|
<ContextMenuItemRefresh />,
|
||||||
|
],
|
||||||
|
[VIEW_NAMES_SEMANTIC]
|
||||||
|
)
|
||||||
|
return menuItems
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ViewControlContextMenu({
|
||||||
|
menuTargetElement: wrapperRef,
|
||||||
|
...props
|
||||||
|
}: ContextMenuProps) {
|
||||||
|
const menuItems = useViewControlMenuItems()
|
||||||
|
return (
|
||||||
|
<ContextMenu
|
||||||
|
data-testid="view-controls-menu"
|
||||||
|
menuTargetElement={wrapperRef}
|
||||||
|
items={menuItems}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
327
src/editor/plugins/lsp/kcl/colors.ts
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
import {
|
||||||
|
EditorView,
|
||||||
|
WidgetType,
|
||||||
|
ViewUpdate,
|
||||||
|
ViewPlugin,
|
||||||
|
DecorationSet,
|
||||||
|
Decoration,
|
||||||
|
} from '@codemirror/view'
|
||||||
|
import { Range, Extension, Text } from '@codemirror/state'
|
||||||
|
import { NodeProp, Tree } from '@lezer/common'
|
||||||
|
import { language, syntaxTree } from '@codemirror/language'
|
||||||
|
|
||||||
|
interface PickerState {
|
||||||
|
from: number
|
||||||
|
to: number
|
||||||
|
alpha: string
|
||||||
|
colorType: ColorType
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WidgetOptions extends PickerState {
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ColorData = Omit<WidgetOptions, 'from' | 'to'>
|
||||||
|
|
||||||
|
const pickerState = new WeakMap<HTMLInputElement, PickerState>()
|
||||||
|
|
||||||
|
export enum ColorType {
|
||||||
|
hex = 'HEX',
|
||||||
|
}
|
||||||
|
|
||||||
|
const hexRegex = /(^|\b)(#[0-9a-f]{3,9})(\b|$)/i
|
||||||
|
|
||||||
|
function discoverColorsInKCL(
|
||||||
|
syntaxTree: Tree,
|
||||||
|
from: number,
|
||||||
|
to: number,
|
||||||
|
typeName: string,
|
||||||
|
doc: Text,
|
||||||
|
language?: string
|
||||||
|
): WidgetOptions | Array<WidgetOptions> | null {
|
||||||
|
switch (typeName) {
|
||||||
|
case 'Program':
|
||||||
|
case 'VariableDeclaration':
|
||||||
|
case 'CallExpression':
|
||||||
|
case 'ObjectExpression':
|
||||||
|
case 'ObjectProperty':
|
||||||
|
case 'ArgumentList':
|
||||||
|
case 'PipeExpression': {
|
||||||
|
let innerTree = syntaxTree.resolveInner(from, 0).tree
|
||||||
|
|
||||||
|
if (!innerTree) {
|
||||||
|
innerTree = syntaxTree.resolveInner(from, 1).tree
|
||||||
|
if (!innerTree) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlayTree = innerTree.prop(NodeProp.mounted)?.tree
|
||||||
|
|
||||||
|
if (overlayTree?.type.name !== 'Styles') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const ret: Array<WidgetOptions> = []
|
||||||
|
overlayTree.iterate({
|
||||||
|
from: 0,
|
||||||
|
to: overlayTree.length,
|
||||||
|
enter: ({ type, from: overlayFrom, to: overlayTo }) => {
|
||||||
|
const maybeWidgetOptions = discoverColorsInKCL(
|
||||||
|
syntaxTree,
|
||||||
|
// We add one because the tree doesn't include the
|
||||||
|
// quotation mark from the style tag
|
||||||
|
from + 1 + overlayFrom,
|
||||||
|
from + 1 + overlayTo,
|
||||||
|
type.name,
|
||||||
|
doc,
|
||||||
|
language
|
||||||
|
)
|
||||||
|
|
||||||
|
if (maybeWidgetOptions) {
|
||||||
|
if (Array.isArray(maybeWidgetOptions)) {
|
||||||
|
console.error('Unexpected nested overlays')
|
||||||
|
ret.push(...maybeWidgetOptions)
|
||||||
|
} else {
|
||||||
|
ret.push(maybeWidgetOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'String': {
|
||||||
|
const result = parseColorLiteral(doc.sliceString(from, to))
|
||||||
|
if (!result) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseColorLiteral(colorLiteral: string): ColorData | null {
|
||||||
|
const literal = colorLiteral.replace(/"/g, '')
|
||||||
|
const match = hexRegex.exec(literal)
|
||||||
|
if (!match) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const [color, alpha] = toFullHex(literal)
|
||||||
|
|
||||||
|
return {
|
||||||
|
colorType: ColorType.hex,
|
||||||
|
color,
|
||||||
|
alpha,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function colorPickersDecorations(
|
||||||
|
view: EditorView,
|
||||||
|
discoverColors: typeof discoverColorsInKCL
|
||||||
|
) {
|
||||||
|
const widgets: Array<Range<Decoration>> = []
|
||||||
|
|
||||||
|
const st = syntaxTree(view.state)
|
||||||
|
|
||||||
|
for (const range of view.visibleRanges) {
|
||||||
|
st.iterate({
|
||||||
|
from: range.from,
|
||||||
|
to: range.to,
|
||||||
|
enter: ({ type, from, to }) => {
|
||||||
|
const maybeWidgetOptions = discoverColors(
|
||||||
|
st,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
type.name,
|
||||||
|
view.state.doc,
|
||||||
|
view.state.facet(language)?.name
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!maybeWidgetOptions) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(maybeWidgetOptions)) {
|
||||||
|
widgets.push(
|
||||||
|
Decoration.widget({
|
||||||
|
widget: new ColorPickerWidget(maybeWidgetOptions),
|
||||||
|
side: 1,
|
||||||
|
}).range(maybeWidgetOptions.from)
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const wo of maybeWidgetOptions) {
|
||||||
|
widgets.push(
|
||||||
|
Decoration.widget({
|
||||||
|
widget: new ColorPickerWidget(wo),
|
||||||
|
side: 1,
|
||||||
|
}).range(wo.from)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return Decoration.set(widgets)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toFullHex(color: string): string[] {
|
||||||
|
if (color.length === 4) {
|
||||||
|
// 3-char hex
|
||||||
|
return [
|
||||||
|
`#${color[1].repeat(2)}${color[2].repeat(2)}${color[3].repeat(2)}`,
|
||||||
|
'',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (color.length === 5) {
|
||||||
|
// 4-char hex (alpha)
|
||||||
|
return [
|
||||||
|
`#${color[1].repeat(2)}${color[2].repeat(2)}${color[3].repeat(2)}`,
|
||||||
|
color[4].repeat(2),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (color.length === 9) {
|
||||||
|
// 8-char hex (alpha)
|
||||||
|
return [`#${color.slice(1, -2)}`, color.slice(-2)]
|
||||||
|
}
|
||||||
|
|
||||||
|
return [color, '']
|
||||||
|
}
|
||||||
|
|
||||||
|
export const wrapperClassName = 'cm-css-color-picker-wrapper'
|
||||||
|
|
||||||
|
class ColorPickerWidget extends WidgetType {
|
||||||
|
private readonly state: PickerState
|
||||||
|
private readonly color: string
|
||||||
|
|
||||||
|
constructor({ color, ...state }: WidgetOptions) {
|
||||||
|
super()
|
||||||
|
this.state = state
|
||||||
|
this.color = color
|
||||||
|
}
|
||||||
|
|
||||||
|
eq(other: ColorPickerWidget) {
|
||||||
|
return (
|
||||||
|
other.state.colorType === this.state.colorType &&
|
||||||
|
other.color === this.color &&
|
||||||
|
other.state.from === this.state.from &&
|
||||||
|
other.state.to === this.state.to &&
|
||||||
|
other.state.alpha === this.state.alpha
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
toDOM() {
|
||||||
|
const picker = document.createElement('input')
|
||||||
|
pickerState.set(picker, this.state)
|
||||||
|
picker.type = 'color'
|
||||||
|
picker.value = this.color
|
||||||
|
|
||||||
|
const wrapper = document.createElement('span')
|
||||||
|
wrapper.appendChild(picker)
|
||||||
|
wrapper.className = wrapperClassName
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoreEvent() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const colorPickerTheme = EditorView.baseTheme({
|
||||||
|
[`.${wrapperClassName}`]: {
|
||||||
|
display: 'inline-block',
|
||||||
|
outline: '1px solid #eee',
|
||||||
|
marginRight: '0.6ch',
|
||||||
|
height: '1em',
|
||||||
|
width: '1em',
|
||||||
|
transform: 'translateY(1px)',
|
||||||
|
},
|
||||||
|
[`.${wrapperClassName} input[type="color"]`]: {
|
||||||
|
cursor: 'pointer',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
padding: 0,
|
||||||
|
border: 'none',
|
||||||
|
'&::-webkit-color-swatch-wrapper': {
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
'&::-webkit-color-swatch': {
|
||||||
|
border: 'none',
|
||||||
|
},
|
||||||
|
'&::-moz-color-swatch': {
|
||||||
|
border: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
interface IFactoryOptions {
|
||||||
|
discoverColors: typeof discoverColorsInKCL
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeColorPicker = (options: IFactoryOptions) =>
|
||||||
|
ViewPlugin.fromClass(
|
||||||
|
class ColorPickerViewPlugin {
|
||||||
|
decorations: DecorationSet
|
||||||
|
|
||||||
|
constructor(view: EditorView) {
|
||||||
|
this.decorations = colorPickersDecorations(view, options.discoverColors)
|
||||||
|
}
|
||||||
|
|
||||||
|
update(update: ViewUpdate) {
|
||||||
|
if (update.docChanged || update.viewportChanged) {
|
||||||
|
this.decorations = colorPickersDecorations(
|
||||||
|
update.view,
|
||||||
|
options.discoverColors
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
decorations: (v) => v.decorations,
|
||||||
|
eventHandlers: {
|
||||||
|
change: (e, view) => {
|
||||||
|
const target = e.target as HTMLInputElement
|
||||||
|
if (
|
||||||
|
target.nodeName !== 'INPUT' ||
|
||||||
|
!target.parentElement ||
|
||||||
|
!target.parentElement.classList.contains(wrapperClassName)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = pickerState.get(target)!
|
||||||
|
|
||||||
|
let converted = '"' + target.value + data.alpha + '"'
|
||||||
|
|
||||||
|
view.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: data.from,
|
||||||
|
to: data.to,
|
||||||
|
insert: converted,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const colorPicker: Extension = [
|
||||||
|
makeColorPicker({ discoverColors: discoverColorsInKCL }),
|
||||||
|
colorPickerTheme,
|
||||||
|
]
|
@ -17,6 +17,7 @@ import { kclPlugin } from '.'
|
|||||||
import type * as LSP from 'vscode-languageserver-protocol'
|
import type * as LSP from 'vscode-languageserver-protocol'
|
||||||
// @ts-ignore: No types available
|
// @ts-ignore: No types available
|
||||||
import { parser } from './kcl.grammar'
|
import { parser } from './kcl.grammar'
|
||||||
|
import { colorPicker } from './colors'
|
||||||
|
|
||||||
export interface LanguageOptions {
|
export interface LanguageOptions {
|
||||||
workspaceFolders: LSP.WorkspaceFolder[]
|
workspaceFolders: LSP.WorkspaceFolder[]
|
||||||
@ -54,14 +55,14 @@ export const KclLanguage = LRLanguage.define({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export function kcl(options: LanguageOptions) {
|
export function kcl(options: LanguageOptions) {
|
||||||
return new LanguageSupport(
|
return new LanguageSupport(KclLanguage, [
|
||||||
KclLanguage,
|
colorPicker,
|
||||||
kclPlugin({
|
kclPlugin({
|
||||||
documentUri: options.documentUri,
|
documentUri: options.documentUri,
|
||||||
workspaceFolders: options.workspaceFolders,
|
workspaceFolders: options.workspaceFolders,
|
||||||
allowHTMLContent: true,
|
allowHTMLContent: true,
|
||||||
client: options.client,
|
client: options.client,
|
||||||
processLspNotification: options.processLspNotification,
|
processLspNotification: options.processLspNotification,
|
||||||
})
|
}),
|
||||||
)
|
])
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { LspWorkerEventType } from '@kittycad/codemirror-lsp-client'
|
import { LspWorkerEventType } from '@kittycad/codemirror-lsp-client'
|
||||||
|
|
||||||
import { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
|
|
||||||
|
|
||||||
export enum LspWorker {
|
export enum LspWorker {
|
||||||
Kcl = 'kcl',
|
Kcl = 'kcl',
|
||||||
Copilot = 'copilot',
|
Copilot = 'copilot',
|
||||||
@ -9,7 +7,6 @@ export enum LspWorker {
|
|||||||
export interface KclWorkerOptions {
|
export interface KclWorkerOptions {
|
||||||
wasmUrl: string
|
wasmUrl: string
|
||||||
token: string
|
token: string
|
||||||
baseUnit: UnitLength
|
|
||||||
apiBaseUrl: string
|
apiBaseUrl: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,6 @@ import {
|
|||||||
KclWorkerOptions,
|
KclWorkerOptions,
|
||||||
CopilotWorkerOptions,
|
CopilotWorkerOptions,
|
||||||
} from 'editor/plugins/lsp/types'
|
} from 'editor/plugins/lsp/types'
|
||||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
|
||||||
import { err, reportRejection } from 'lib/trap'
|
import { err, reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
const intoServer: IntoServer = new IntoServer()
|
const intoServer: IntoServer = new IntoServer()
|
||||||
@ -46,14 +45,12 @@ export async function copilotLspRun(
|
|||||||
|
|
||||||
export async function kclLspRun(
|
export async function kclLspRun(
|
||||||
config: ServerConfig,
|
config: ServerConfig,
|
||||||
engineCommandManager: EngineCommandManager | null,
|
|
||||||
token: string,
|
token: string,
|
||||||
baseUnit: string,
|
|
||||||
baseUrl: string
|
baseUrl: string
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
console.log('start kcl lsp')
|
console.log('start kcl lsp')
|
||||||
await kcl_lsp_run(config, engineCommandManager, baseUnit, token, baseUrl)
|
await kcl_lsp_run(config, null, undefined, token, baseUrl)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.log('kcl lsp failed', e)
|
console.log('kcl lsp failed', e)
|
||||||
// We can't restart here because a moved value, we should do this another way.
|
// We can't restart here because a moved value, we should do this another way.
|
||||||
@ -82,13 +79,7 @@ onmessage = function (event: MessageEvent) {
|
|||||||
switch (worker) {
|
switch (worker) {
|
||||||
case LspWorker.Kcl:
|
case LspWorker.Kcl:
|
||||||
const kclData = eventData as KclWorkerOptions
|
const kclData = eventData as KclWorkerOptions
|
||||||
await kclLspRun(
|
await kclLspRun(config, kclData.token, kclData.apiBaseUrl)
|
||||||
config,
|
|
||||||
null,
|
|
||||||
kclData.token,
|
|
||||||
kclData.baseUnit,
|
|
||||||
kclData.apiBaseUrl
|
|
||||||
)
|
|
||||||
break
|
break
|
||||||
case LspWorker.Copilot:
|
case LspWorker.Copilot:
|
||||||
let copilotData = eventData as CopilotWorkerOptions
|
let copilotData = eventData as CopilotWorkerOptions
|
||||||
|
@ -2,7 +2,7 @@ import { useLayoutEffect, useEffect, useRef } from 'react'
|
|||||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||||
import { deferExecution } from 'lib/utils'
|
import { deferExecution } from 'lib/utils'
|
||||||
import { Themes } from 'lib/theme'
|
import { Themes } from 'lib/theme'
|
||||||
import { makeDefaultPlanes, modifyGrid } from 'lang/wasm'
|
import { makeDefaultPlanes } from 'lang/wasm'
|
||||||
import { useModelingContext } from './useModelingContext'
|
import { useModelingContext } from './useModelingContext'
|
||||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
import { useAppState, useAppStream } from 'AppState'
|
import { useAppState, useAppStream } from 'AppState'
|
||||||
@ -56,9 +56,6 @@ export function useSetupEngineManager(
|
|||||||
makeDefaultPlanes: () => {
|
makeDefaultPlanes: () => {
|
||||||
return makeDefaultPlanes(kclManager.engineCommandManager)
|
return makeDefaultPlanes(kclManager.engineCommandManager)
|
||||||
},
|
},
|
||||||
modifyGrid: (hidden: boolean) => {
|
|
||||||
return modifyGrid(kclManager.engineCommandManager, hidden)
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
hasSetNonZeroDimensions.current = true
|
hasSetNonZeroDimensions.current = true
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,8 @@ export function useConvertToVariable(range?: SourceRange) {
|
|||||||
}, [enable])
|
}, [enable])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Return early if there are no selection ranges for whatever reason
|
||||||
|
if (!context.selectionRanges) return
|
||||||
const parsed = ast
|
const parsed = ast
|
||||||
|
|
||||||
const meta = isNodeSafeToReplace(
|
const meta = isNodeSafeToReplace(
|
||||||
|
@ -317,3 +317,8 @@ code {
|
|||||||
#code-mirror-override .cm-editor {
|
#code-mirror-override .cm-editor {
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Can't use #code-mirror-override here as we're outside of this div */
|
||||||
|
.body-bg .cm-diagnosticAction {
|
||||||
|
@apply bg-primary;
|
||||||
|
}
|
||||||
|
@ -311,8 +311,6 @@ export class KclManager {
|
|||||||
// Do not send send scene commands if the program was interrupted, go to clean up
|
// Do not send send scene commands if the program was interrupted, go to clean up
|
||||||
if (!isInterrupted) {
|
if (!isInterrupted) {
|
||||||
this.addDiagnostics(await lintAst({ ast: ast }))
|
this.addDiagnostics(await lintAst({ ast: ast }))
|
||||||
|
|
||||||
sceneInfra.modelingSend({ type: 'code edit during sketch' })
|
|
||||||
setSelectionFilterToDefault(execState.memory, this.engineCommandManager)
|
setSelectionFilterToDefault(execState.memory, this.engineCommandManager)
|
||||||
|
|
||||||
if (args.zoomToFit) {
|
if (args.zoomToFit) {
|
||||||
@ -358,7 +356,11 @@ export class KclManager {
|
|||||||
this.lastSuccessfulProgramMemory = execState.memory
|
this.lastSuccessfulProgramMemory = execState.memory
|
||||||
}
|
}
|
||||||
this.ast = { ...ast }
|
this.ast = { ...ast }
|
||||||
|
// updateArtifactGraph relies on updated executeState/programMemory
|
||||||
|
await this.engineCommandManager.updateArtifactGraph(this.ast)
|
||||||
this._executeCallback()
|
this._executeCallback()
|
||||||
|
if (!isInterrupted)
|
||||||
|
sceneInfra.modelingSend({ type: 'code edit during sketch' })
|
||||||
this.engineCommandManager.addCommandLog({
|
this.engineCommandManager.addCommandLog({
|
||||||
type: 'execution-done',
|
type: 'execution-done',
|
||||||
data: null,
|
data: null,
|
||||||
@ -400,6 +402,7 @@ export class KclManager {
|
|||||||
|
|
||||||
this._logs = logs
|
this._logs = logs
|
||||||
this.addDiagnostics(kclErrorsToDiagnostics(errors))
|
this.addDiagnostics(kclErrorsToDiagnostics(errors))
|
||||||
|
|
||||||
this._execState = execState
|
this._execState = execState
|
||||||
this._programMemory = execState.memory
|
this._programMemory = execState.memory
|
||||||
if (!errors.length) {
|
if (!errors.length) {
|
||||||
@ -411,7 +414,7 @@ export class KclManager {
|
|||||||
// problem this solves, but either way we should strive to remove it.
|
// problem this solves, but either way we should strive to remove it.
|
||||||
Array.from(this.engineCommandManager.artifactGraph).forEach(
|
Array.from(this.engineCommandManager.artifactGraph).forEach(
|
||||||
([commandId, artifact]) => {
|
([commandId, artifact]) => {
|
||||||
if (!('codeRef' in artifact)) return
|
if (!('codeRef' in artifact && artifact.codeRef)) return
|
||||||
const _node1 = getNodeFromPath<Node<CallExpression>>(
|
const _node1 = getNodeFromPath<Node<CallExpression>>(
|
||||||
this.ast,
|
this.ast,
|
||||||
artifact.codeRef.pathToNode,
|
artifact.codeRef.pathToNode,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Program,
|
Program,
|
||||||
_executor,
|
executor,
|
||||||
ProgramMemory,
|
ProgramMemory,
|
||||||
kclLint,
|
kclLint,
|
||||||
emptyExecState,
|
emptyExecState,
|
||||||
@ -64,12 +64,9 @@ export async function executeAst({
|
|||||||
try {
|
try {
|
||||||
const execState = await (programMemoryOverride
|
const execState = await (programMemoryOverride
|
||||||
? enginelessExecutor(ast, programMemoryOverride)
|
? enginelessExecutor(ast, programMemoryOverride)
|
||||||
: _executor(ast, engineCommandManager))
|
: executor(ast, engineCommandManager))
|
||||||
|
|
||||||
await engineCommandManager.waitForAllCommands(
|
|
||||||
programMemoryOverride !== undefined
|
|
||||||
)
|
|
||||||
|
|
||||||
|
await engineCommandManager.waitForAllCommands()
|
||||||
return {
|
return {
|
||||||
logs: [],
|
logs: [],
|
||||||
errors: [],
|
errors: [],
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
deleteSegmentFromPipeExpression,
|
deleteSegmentFromPipeExpression,
|
||||||
removeSingleConstraintInfo,
|
removeSingleConstraintInfo,
|
||||||
deleteFromSelection,
|
deleteFromSelection,
|
||||||
|
splitPipedProfile,
|
||||||
} from './modifyAst'
|
} from './modifyAst'
|
||||||
import { enginelessExecutor } from '../lib/testHelpers'
|
import { enginelessExecutor } from '../lib/testHelpers'
|
||||||
import { findUsesOfTagInPipe, getNodePathFromSourceRange } from './queryAst'
|
import { findUsesOfTagInPipe, getNodePathFromSourceRange } from './queryAst'
|
||||||
@ -918,3 +919,63 @@ sketch002 = startSketchOn({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Testing splitPipedProfile', () => {
|
||||||
|
it('should split the pipe expression correctly', () => {
|
||||||
|
const codeBefore = `part001 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([1, 2], %)
|
||||||
|
|> line([3, 4], %)
|
||||||
|
|> line([5, 6], %)
|
||||||
|
|> close(%)
|
||||||
|
extrude001 = extrude(5, part001)
|
||||||
|
`
|
||||||
|
|
||||||
|
const expectedCodeAfter = `sketch001 = startSketchOn('XZ')
|
||||||
|
part001 = startProfileAt([1, 2], sketch001)
|
||||||
|
|> line([3, 4], %)
|
||||||
|
|> line([5, 6], %)
|
||||||
|
|> close(%)
|
||||||
|
extrude001 = extrude(5, part001)
|
||||||
|
`
|
||||||
|
|
||||||
|
const ast = assertParse(codeBefore)
|
||||||
|
|
||||||
|
const codeOfInterest = `startSketchOn('XZ')`
|
||||||
|
const range: [number, number, boolean] = [
|
||||||
|
codeBefore.indexOf(codeOfInterest),
|
||||||
|
codeBefore.indexOf(codeOfInterest) + codeOfInterest.length,
|
||||||
|
true,
|
||||||
|
]
|
||||||
|
const pathToPipe = getNodePathFromSourceRange(ast, range)
|
||||||
|
|
||||||
|
const result = splitPipedProfile(ast, pathToPipe)
|
||||||
|
|
||||||
|
if (err(result)) throw result
|
||||||
|
|
||||||
|
const newCode = recast(result.modifiedAst)
|
||||||
|
if (err(newCode)) throw newCode
|
||||||
|
expect(newCode.trim()).toBe(expectedCodeAfter.trim())
|
||||||
|
})
|
||||||
|
it('should return error for already split pipe', () => {
|
||||||
|
const codeBefore = `sketch001 = startSketchOn('XZ')
|
||||||
|
part001 = startProfileAt([1, 2], sketch001)
|
||||||
|
|> line([3, 4], %)
|
||||||
|
|> line([5, 6], %)
|
||||||
|
|> close(%)
|
||||||
|
extrude001 = extrude(5, part001)
|
||||||
|
`
|
||||||
|
|
||||||
|
const ast = assertParse(codeBefore)
|
||||||
|
|
||||||
|
const codeOfInterest = `startProfileAt([1, 2], sketch001)`
|
||||||
|
const range: [number, number, boolean] = [
|
||||||
|
codeBefore.indexOf(codeOfInterest),
|
||||||
|
codeBefore.indexOf(codeOfInterest) + codeOfInterest.length,
|
||||||
|
true,
|
||||||
|
]
|
||||||
|
const pathToPipe = getNodePathFromSourceRange(ast, range)
|
||||||
|
|
||||||
|
const result = splitPipedProfile(ast, pathToPipe)
|
||||||
|
expect(result instanceof Error).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
@ -29,6 +29,8 @@ import {
|
|||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
isNodeSafeToReplace,
|
isNodeSafeToReplace,
|
||||||
traverse,
|
traverse,
|
||||||
|
getBodyIndex,
|
||||||
|
isCallExprWithName,
|
||||||
} from './queryAst'
|
} from './queryAst'
|
||||||
import { addTagForSketchOnFace, getConstraintInfo } from './std/sketch'
|
import { addTagForSketchOnFace, getConstraintInfo } from './std/sketch'
|
||||||
import {
|
import {
|
||||||
@ -45,6 +47,8 @@ import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
|
|||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import { ExtrudeFacePlane } from 'machines/modelingMachine'
|
import { ExtrudeFacePlane } from 'machines/modelingMachine'
|
||||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
|
import { KclExpressionWithVariable } from 'lib/commandTypes'
|
||||||
|
import { Artifact, getPathsFromArtifact } from './std/artifactGraph'
|
||||||
|
|
||||||
export function startSketchOnDefault(
|
export function startSketchOnDefault(
|
||||||
node: Node<Program>,
|
node: Node<Program>,
|
||||||
@ -77,41 +81,54 @@ export function startSketchOnDefault(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addStartProfileAt(
|
export function insertNewStartProfileAt(
|
||||||
node: Node<Program>,
|
node: Node<Program>,
|
||||||
pathToNode: PathToNode,
|
sketchEntryNodePath: PathToNode,
|
||||||
at: [number, number]
|
sketchNodePaths: PathToNode[],
|
||||||
): { modifiedAst: Node<Program>; pathToNode: PathToNode } | Error {
|
planeNodePath: PathToNode,
|
||||||
const _node1 = getNodeFromPath<VariableDeclaration>(
|
at: [number, number],
|
||||||
|
insertType: 'start' | 'end' = 'end'
|
||||||
|
):
|
||||||
|
| {
|
||||||
|
modifiedAst: Node<Program>
|
||||||
|
updatedSketchNodePaths: PathToNode[]
|
||||||
|
updatedEntryNodePath: PathToNode
|
||||||
|
}
|
||||||
|
| Error {
|
||||||
|
const varDec = getNodeFromPath<VariableDeclarator>(
|
||||||
node,
|
node,
|
||||||
pathToNode,
|
planeNodePath,
|
||||||
'VariableDeclaration'
|
'VariableDeclarator'
|
||||||
)
|
)
|
||||||
if (err(_node1)) return _node1
|
if (err(varDec)) return varDec
|
||||||
const variableDeclaration = _node1.node
|
if (varDec.node.type !== 'VariableDeclarator') return new Error('not a var')
|
||||||
if (variableDeclaration.type !== 'VariableDeclaration') {
|
|
||||||
return new Error('variableDeclaration.init.type !== PipeExpression')
|
const newExpression = createVariableDeclaration(
|
||||||
}
|
findUniqueName(node, 'profile'),
|
||||||
const _node = { ...node }
|
createCallExpressionStdLib('startProfileAt', [
|
||||||
const init = variableDeclaration.declaration.init
|
createArrayExpression([
|
||||||
const startProfileAt = createCallExpressionStdLib('startProfileAt', [
|
createLiteral(roundOff(at[0])),
|
||||||
createArrayExpression([
|
createLiteral(roundOff(at[1])),
|
||||||
createLiteral(roundOff(at[0])),
|
]),
|
||||||
createLiteral(roundOff(at[1])),
|
createIdentifier(varDec.node.id.name),
|
||||||
]),
|
|
||||||
createPipeSubstitution(),
|
|
||||||
])
|
|
||||||
if (init.type === 'PipeExpression') {
|
|
||||||
init.body.splice(1, 0, startProfileAt)
|
|
||||||
} else {
|
|
||||||
variableDeclaration.declaration.init = createPipeExpression([
|
|
||||||
init,
|
|
||||||
startProfileAt,
|
|
||||||
])
|
])
|
||||||
}
|
)
|
||||||
|
const insertIndex = getInsertIndex(sketchNodePaths, planeNodePath, insertType)
|
||||||
|
|
||||||
|
const _node = structuredClone(node)
|
||||||
|
// TODO the rest of this function will not be robust to work for sketches defined within a function declaration
|
||||||
|
_node.body.splice(insertIndex, 0, newExpression)
|
||||||
|
|
||||||
|
const { updatedEntryNodePath, updatedSketchNodePaths } =
|
||||||
|
updateSketchNodePathsWithInsertIndex({
|
||||||
|
insertIndex,
|
||||||
|
insertType,
|
||||||
|
sketchNodePaths,
|
||||||
|
})
|
||||||
return {
|
return {
|
||||||
modifiedAst: _node,
|
modifiedAst: _node,
|
||||||
pathToNode,
|
updatedSketchNodePaths,
|
||||||
|
updatedEntryNodePath,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,7 +269,7 @@ export function mutateObjExpProp(
|
|||||||
export function extrudeSketch(
|
export function extrudeSketch(
|
||||||
node: Node<Program>,
|
node: Node<Program>,
|
||||||
pathToNode: PathToNode,
|
pathToNode: PathToNode,
|
||||||
shouldPipe = false,
|
artifact?: Artifact,
|
||||||
distance: Expr = createLiteral(4)
|
distance: Expr = createLiteral(4)
|
||||||
):
|
):
|
||||||
| {
|
| {
|
||||||
@ -261,10 +278,14 @@ export function extrudeSketch(
|
|||||||
pathToExtrudeArg: PathToNode
|
pathToExtrudeArg: PathToNode
|
||||||
}
|
}
|
||||||
| Error {
|
| Error {
|
||||||
|
const orderedSketchNodePaths = getPathsFromArtifact({
|
||||||
|
artifact: artifact,
|
||||||
|
sketchPathToNode: pathToNode,
|
||||||
|
})
|
||||||
|
if (err(orderedSketchNodePaths)) return orderedSketchNodePaths
|
||||||
const _node = structuredClone(node)
|
const _node = structuredClone(node)
|
||||||
const _node1 = getNodeFromPath(_node, pathToNode)
|
const _node1 = getNodeFromPath(_node, pathToNode)
|
||||||
if (err(_node1)) return _node1
|
if (err(_node1)) return _node1
|
||||||
const { node: sketchExpression } = _node1
|
|
||||||
|
|
||||||
// determine if sketchExpression is in a pipeExpression or not
|
// determine if sketchExpression is in a pipeExpression or not
|
||||||
const _node2 = getNodeFromPath<PipeExpression>(
|
const _node2 = getNodeFromPath<PipeExpression>(
|
||||||
@ -273,9 +294,6 @@ export function extrudeSketch(
|
|||||||
'PipeExpression'
|
'PipeExpression'
|
||||||
)
|
)
|
||||||
if (err(_node2)) return _node2
|
if (err(_node2)) return _node2
|
||||||
const { node: pipeExpression } = _node2
|
|
||||||
|
|
||||||
const isInPipeExpression = pipeExpression.type === 'PipeExpression'
|
|
||||||
|
|
||||||
const _node3 = getNodeFromPath<VariableDeclarator>(
|
const _node3 = getNodeFromPath<VariableDeclarator>(
|
||||||
_node,
|
_node,
|
||||||
@ -283,49 +301,23 @@ export function extrudeSketch(
|
|||||||
'VariableDeclarator'
|
'VariableDeclarator'
|
||||||
)
|
)
|
||||||
if (err(_node3)) return _node3
|
if (err(_node3)) return _node3
|
||||||
const { node: variableDeclarator, shallowPath: pathToDecleration } = _node3
|
const { node: variableDeclarator } = _node3
|
||||||
|
|
||||||
const extrudeCall = createCallExpressionStdLib('extrude', [
|
const extrudeCall = createCallExpressionStdLib('extrude', [
|
||||||
distance,
|
distance,
|
||||||
shouldPipe
|
createIdentifier(variableDeclarator.id.name),
|
||||||
? createPipeSubstitution()
|
|
||||||
: createIdentifier(variableDeclarator.id.name),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
if (shouldPipe) {
|
|
||||||
const pipeChain = createPipeExpression(
|
|
||||||
isInPipeExpression
|
|
||||||
? [...pipeExpression.body, extrudeCall]
|
|
||||||
: [sketchExpression as any, extrudeCall]
|
|
||||||
)
|
|
||||||
|
|
||||||
variableDeclarator.init = pipeChain
|
|
||||||
const pathToExtrudeArg: PathToNode = [
|
|
||||||
...pathToDecleration,
|
|
||||||
['init', 'VariableDeclarator'],
|
|
||||||
['body', ''],
|
|
||||||
[pipeChain.body.length - 1, 'index'],
|
|
||||||
['arguments', 'CallExpression'],
|
|
||||||
[0, 'index'],
|
|
||||||
]
|
|
||||||
|
|
||||||
return {
|
|
||||||
modifiedAst: _node,
|
|
||||||
pathToNode,
|
|
||||||
pathToExtrudeArg,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We're not creating a pipe expression,
|
// We're not creating a pipe expression,
|
||||||
// but rather a separate constant for the extrusion
|
// but rather a separate constant for the extrusion
|
||||||
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.EXTRUDE)
|
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.EXTRUDE)
|
||||||
const VariableDeclaration = createVariableDeclaration(name, extrudeCall)
|
const VariableDeclaration = createVariableDeclaration(name, extrudeCall)
|
||||||
|
|
||||||
const sketchIndexInPathToNode =
|
const lastSketchNodePath =
|
||||||
pathToDecleration.findIndex((a) => a[0] === 'body') + 1
|
orderedSketchNodePaths[orderedSketchNodePaths.length - 1]
|
||||||
const sketchIndexInBody = pathToDecleration[
|
|
||||||
sketchIndexInPathToNode
|
console.log('lastSketchNodePath', lastSketchNodePath, orderedSketchNodePaths)
|
||||||
][0] as number
|
const sketchIndexInBody = Number(lastSketchNodePath[1][0])
|
||||||
_node.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)
|
_node.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)
|
||||||
|
|
||||||
const pathToExtrudeArg: PathToNode = [
|
const pathToExtrudeArg: PathToNode = [
|
||||||
@ -590,6 +582,25 @@ export function addOffsetPlane({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a modified clone of an AST with a named constant inserted into the body
|
||||||
|
*/
|
||||||
|
export function insertNamedConstant({
|
||||||
|
node,
|
||||||
|
newExpression,
|
||||||
|
}: {
|
||||||
|
node: Node<Program>
|
||||||
|
newExpression: KclExpressionWithVariable
|
||||||
|
}): Node<Program> {
|
||||||
|
const ast = structuredClone(node)
|
||||||
|
ast.body.splice(
|
||||||
|
newExpression.insertIndex,
|
||||||
|
0,
|
||||||
|
newExpression.variableDeclarationAst
|
||||||
|
)
|
||||||
|
return ast
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modify the AST to create a new sketch using the variable declaration
|
* Modify the AST to create a new sketch using the variable declaration
|
||||||
* of an offset plane. The new sketch just has to come after the offset
|
* of an offset plane. The new sketch just has to come after the offset
|
||||||
@ -933,6 +944,31 @@ export function giveSketchFnCallTag(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace a
|
||||||
|
*/
|
||||||
|
export function replaceValueAtNodePath({
|
||||||
|
ast,
|
||||||
|
pathToNode,
|
||||||
|
newExpressionString,
|
||||||
|
}: {
|
||||||
|
ast: Node<Program>
|
||||||
|
pathToNode: PathToNode
|
||||||
|
newExpressionString: string
|
||||||
|
}) {
|
||||||
|
const replaceCheckResult = isNodeSafeToReplacePath(ast, pathToNode)
|
||||||
|
if (err(replaceCheckResult)) {
|
||||||
|
return replaceCheckResult
|
||||||
|
}
|
||||||
|
const { isSafe, value, replacer } = replaceCheckResult
|
||||||
|
|
||||||
|
if (!isSafe || value.type === 'Identifier') {
|
||||||
|
return new Error('Not safe to replace')
|
||||||
|
}
|
||||||
|
|
||||||
|
return replacer(ast, newExpressionString)
|
||||||
|
}
|
||||||
|
|
||||||
export function moveValueIntoNewVariablePath(
|
export function moveValueIntoNewVariablePath(
|
||||||
ast: Node<Program>,
|
ast: Node<Program>,
|
||||||
programMemory: ProgramMemory,
|
programMemory: ProgramMemory,
|
||||||
@ -1250,7 +1286,8 @@ export async function deleteFromSelection(
|
|||||||
const pipeBody = varDec.node.init.body
|
const pipeBody = varDec.node.init.body
|
||||||
if (
|
if (
|
||||||
pipeBody[0].type === 'CallExpression' &&
|
pipeBody[0].type === 'CallExpression' &&
|
||||||
pipeBody[0].callee.name === 'startSketchOn'
|
(pipeBody[0].callee.name === 'startSketchOn' ||
|
||||||
|
pipeBody[0].callee.name === 'startProfileAt')
|
||||||
) {
|
) {
|
||||||
// remove varDec
|
// remove varDec
|
||||||
const varDecIndex = varDec.shallowPath[1][0] as number
|
const varDecIndex = varDec.shallowPath[1][0] as number
|
||||||
@ -1265,3 +1302,149 @@ export async function deleteFromSelection(
|
|||||||
const nonCodeMetaEmpty = () => {
|
const nonCodeMetaEmpty = () => {
|
||||||
return { nonCodeNodes: {}, startNodes: [], start: 0, end: 0 }
|
return { nonCodeNodes: {}, startNodes: [], start: 0, end: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getInsertIndex(
|
||||||
|
sketchNodePaths: PathToNode[],
|
||||||
|
planeNodePath: PathToNode,
|
||||||
|
insertType: 'start' | 'end'
|
||||||
|
) {
|
||||||
|
let minIndex = 0
|
||||||
|
let maxIndex = 0
|
||||||
|
for (const path of sketchNodePaths) {
|
||||||
|
const index = Number(path[1][0])
|
||||||
|
if (index < minIndex) minIndex = index
|
||||||
|
if (index > maxIndex) maxIndex = index
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertIndex = !sketchNodePaths.length
|
||||||
|
? Number(planeNodePath[1][0]) + 1
|
||||||
|
: insertType === 'start'
|
||||||
|
? minIndex
|
||||||
|
: maxIndex + 1
|
||||||
|
return insertIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateSketchNodePathsWithInsertIndex({
|
||||||
|
insertIndex,
|
||||||
|
insertType,
|
||||||
|
sketchNodePaths,
|
||||||
|
}: {
|
||||||
|
insertIndex: number
|
||||||
|
insertType: 'start' | 'end'
|
||||||
|
sketchNodePaths: PathToNode[]
|
||||||
|
}): {
|
||||||
|
updatedEntryNodePath: PathToNode
|
||||||
|
updatedSketchNodePaths: PathToNode[]
|
||||||
|
} {
|
||||||
|
// TODO the rest of this function will not be robust to work for sketches defined within a function declaration
|
||||||
|
const newExpressionPathToNode: PathToNode = [
|
||||||
|
['body', ''],
|
||||||
|
[insertIndex, 'index'],
|
||||||
|
['declaration', 'VariableDeclaration'],
|
||||||
|
['init', 'VariableDeclarator'],
|
||||||
|
]
|
||||||
|
let updatedSketchNodePaths = structuredClone(sketchNodePaths)
|
||||||
|
if (insertType === 'start') {
|
||||||
|
updatedSketchNodePaths = updatedSketchNodePaths.map((path) => {
|
||||||
|
path[1][0] = Number(path[1][0]) + 1
|
||||||
|
return path
|
||||||
|
})
|
||||||
|
updatedSketchNodePaths.unshift(newExpressionPathToNode)
|
||||||
|
} else {
|
||||||
|
updatedSketchNodePaths.push(newExpressionPathToNode)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
updatedSketchNodePaths,
|
||||||
|
updatedEntryNodePath: newExpressionPathToNode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Split the following pipe expression into
|
||||||
|
* ```ts
|
||||||
|
* part001 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([1, 2], %)
|
||||||
|
|> line([3, 4], %)
|
||||||
|
|> line([5, 6], %)
|
||||||
|
|> close(%)
|
||||||
|
extrude001 = extrude(5, part001)
|
||||||
|
```
|
||||||
|
into
|
||||||
|
```ts
|
||||||
|
sketch001 = startSketchOn('XZ')
|
||||||
|
part001 = startProfileAt([1, 2], sketch001)
|
||||||
|
|> line([3, 4], %)
|
||||||
|
|> line([5, 6], %)
|
||||||
|
|> close(%)
|
||||||
|
extrude001 = extrude(5, part001)
|
||||||
|
```
|
||||||
|
Notice that the `startSketchOn` is what gets the new variable name, this is so part001 still has the same data as before
|
||||||
|
making it safe for later code that uses part001 (the extrude in this example)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export function splitPipedProfile(
|
||||||
|
ast: Program,
|
||||||
|
pathToPipe: PathToNode
|
||||||
|
):
|
||||||
|
| {
|
||||||
|
modifiedAst: Program
|
||||||
|
pathToProfile: PathToNode
|
||||||
|
pathToPlane: PathToNode
|
||||||
|
}
|
||||||
|
| Error {
|
||||||
|
const _ast = structuredClone(ast)
|
||||||
|
const varDec = getNodeFromPath<VariableDeclaration>(
|
||||||
|
_ast,
|
||||||
|
pathToPipe,
|
||||||
|
'VariableDeclaration'
|
||||||
|
)
|
||||||
|
if (err(varDec)) return varDec
|
||||||
|
if (
|
||||||
|
varDec.node.type !== 'VariableDeclaration' ||
|
||||||
|
varDec.node.declaration.init.type !== 'PipeExpression'
|
||||||
|
) {
|
||||||
|
return new Error('pathToNode does not point to pipe')
|
||||||
|
}
|
||||||
|
const init = varDec.node.declaration.init
|
||||||
|
const firstCall = init.body[0]
|
||||||
|
if (!isCallExprWithName(firstCall, 'startSketchOn'))
|
||||||
|
return new Error('First call is not startSketchOn')
|
||||||
|
const secondCall = init.body[1]
|
||||||
|
if (!isCallExprWithName(secondCall, 'startProfileAt'))
|
||||||
|
return new Error('Second call is not startProfileAt')
|
||||||
|
|
||||||
|
const varName = varDec.node.declaration.id.name
|
||||||
|
const newVarName = findUniqueName(_ast, 'sketch')
|
||||||
|
const secondCallArgs = structuredClone(secondCall.arguments)
|
||||||
|
secondCallArgs[1] = createIdentifier(newVarName)
|
||||||
|
const firstCallOfNewPipe = createCallExpression(
|
||||||
|
'startProfileAt',
|
||||||
|
secondCallArgs
|
||||||
|
)
|
||||||
|
const newSketch = createVariableDeclaration(
|
||||||
|
newVarName,
|
||||||
|
varDec.node.declaration.init.body[0]
|
||||||
|
)
|
||||||
|
const newProfile = createVariableDeclaration(
|
||||||
|
varName,
|
||||||
|
varDec.node.declaration.init.body.length <= 2
|
||||||
|
? firstCallOfNewPipe
|
||||||
|
: createPipeExpression([
|
||||||
|
firstCallOfNewPipe,
|
||||||
|
...varDec.node.declaration.init.body.slice(2),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
const index = getBodyIndex(pathToPipe)
|
||||||
|
if (err(index)) return index
|
||||||
|
_ast.body.splice(index, 1, newSketch, newProfile)
|
||||||
|
const pathToPlane = structuredClone(pathToPipe)
|
||||||
|
const pathToProfile = structuredClone(pathToPipe)
|
||||||
|
pathToProfile[1][0] = index + 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
modifiedAst: _ast,
|
||||||
|
pathToProfile,
|
||||||
|
pathToPlane,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -22,7 +22,7 @@ import {
|
|||||||
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
|
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
|
||||||
import { createLiteral } from 'lang/modifyAst'
|
import { createLiteral } from 'lang/modifyAst'
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selection, Selections } from 'lib/selections'
|
||||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||||
import { VITE_KC_DEV_TOKEN } from 'env'
|
import { VITE_KC_DEV_TOKEN } from 'env'
|
||||||
import { isOverlap } from 'lib/utils'
|
import { isOverlap } from 'lib/utils'
|
||||||
@ -40,7 +40,6 @@ beforeAll(async () => {
|
|||||||
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
|
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
|
||||||
setMediaStream: () => {},
|
setMediaStream: () => {},
|
||||||
setIsStreamReady: () => {},
|
setIsStreamReady: () => {},
|
||||||
modifyGrid: async () => {},
|
|
||||||
callbackOnEngineLiteConnect: () => {
|
callbackOnEngineLiteConnect: () => {
|
||||||
resolve(true)
|
resolve(true)
|
||||||
},
|
},
|
||||||
@ -118,13 +117,8 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
|
|||||||
code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length,
|
code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length,
|
||||||
true,
|
true,
|
||||||
]
|
]
|
||||||
const selection: Selections = {
|
const selection: Selection = {
|
||||||
graphSelections: [
|
codeRef: codeRefFromRange(segmentRange, ast),
|
||||||
{
|
|
||||||
codeRef: codeRefFromRange(segmentRange, ast),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
otherSelections: [],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// executeAst and artifactGraph
|
// executeAst and artifactGraph
|
||||||
@ -253,7 +247,7 @@ extrude003 = extrude(-15, sketch003)`
|
|||||||
selectedSegmentSnippet,
|
selectedSegmentSnippet,
|
||||||
expectedExtrudeSnippet
|
expectedExtrudeSnippet
|
||||||
)
|
)
|
||||||
})
|
}, 5_000)
|
||||||
})
|
})
|
||||||
|
|
||||||
const runModifyAstCloneWithEdgeTreatmentAndTag = async (
|
const runModifyAstCloneWithEdgeTreatmentAndTag = async (
|
||||||
@ -281,7 +275,7 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async (
|
|||||||
const selection: Selections = {
|
const selection: Selections = {
|
||||||
graphSelections: segmentRanges.map((segmentRange) => {
|
graphSelections: segmentRanges.map((segmentRange) => {
|
||||||
const maybeArtifact = [...artifactGraph].find(([, a]) => {
|
const maybeArtifact = [...artifactGraph].find(([, a]) => {
|
||||||
if (!('codeRef' in a)) return false
|
if (!('codeRef' in a && a.codeRef)) return false
|
||||||
return isOverlap(a.codeRef.range, segmentRange)
|
return isOverlap(a.codeRef.range, segmentRange)
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
@ -483,7 +477,7 @@ extrude001 = extrude(-15, sketch001)
|
|||||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
extrude001 = extrude(-15, sketch001)
|
extrude001 = extrude(-15, sketch001)
|
||||||
|> chamfer({ length: 5, tags: [seg01] }, %)`
|
|> chamfer({ length = 5, tags = [seg01] }, %)`
|
||||||
const segmentSnippets = ['line([-20, 0], %)']
|
const segmentSnippets = ['line([-20, 0], %)']
|
||||||
const expectedCode = `sketch001 = startSketchOn('XY')
|
const expectedCode = `sketch001 = startSketchOn('XY')
|
||||||
|> startProfileAt([-10, 10], %)
|
|> startProfileAt([-10, 10], %)
|
||||||
@ -493,8 +487,8 @@ extrude001 = extrude(-15, sketch001)
|
|||||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
extrude001 = extrude(-15, sketch001)
|
extrude001 = extrude(-15, sketch001)
|
||||||
|> chamfer({ length: 5, tags: [seg01] }, %)
|
|> chamfer({ length = 5, tags = [seg01] }, %)
|
||||||
|> ${edgeTreatmentType}({ ${parameterName}: 3, tags: [seg02] }, %)`
|
|> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg02] }, %)`
|
||||||
|
|
||||||
await runModifyAstCloneWithEdgeTreatmentAndTag(
|
await runModifyAstCloneWithEdgeTreatmentAndTag(
|
||||||
code,
|
code,
|
||||||
|
@ -29,7 +29,7 @@ import {
|
|||||||
sketchLineHelperMap,
|
sketchLineHelperMap,
|
||||||
} from '../std/sketch'
|
} from '../std/sketch'
|
||||||
import { err, trap } from 'lib/trap'
|
import { err, trap } from 'lib/trap'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selection, Selections } from 'lib/selections'
|
||||||
import { KclCommandValue } from 'lib/commandTypes'
|
import { KclCommandValue } from 'lib/commandTypes'
|
||||||
import {
|
import {
|
||||||
Artifact,
|
Artifact,
|
||||||
@ -99,14 +99,9 @@ export function modifyAstWithEdgeTreatmentAndTag(
|
|||||||
const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison
|
const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison
|
||||||
|
|
||||||
for (const selection of selections.graphSelections) {
|
for (const selection of selections.graphSelections) {
|
||||||
const singleSelection = {
|
|
||||||
graphSelections: [selection],
|
|
||||||
otherSelections: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = getPathToExtrudeForSegmentSelection(
|
const result = getPathToExtrudeForSegmentSelection(
|
||||||
clonedAstForGetExtrude,
|
clonedAstForGetExtrude,
|
||||||
singleSelection,
|
selection,
|
||||||
artifactGraph
|
artifactGraph
|
||||||
)
|
)
|
||||||
if (err(result)) return result
|
if (err(result)) return result
|
||||||
@ -259,12 +254,12 @@ function insertParametersIntoAst(
|
|||||||
|
|
||||||
export function getPathToExtrudeForSegmentSelection(
|
export function getPathToExtrudeForSegmentSelection(
|
||||||
ast: Program,
|
ast: Program,
|
||||||
selection: Selections,
|
selection: Selection,
|
||||||
artifactGraph: ArtifactGraph
|
artifactGraph: ArtifactGraph
|
||||||
): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error {
|
): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error {
|
||||||
const pathToSegmentNode = getNodePathFromSourceRange(
|
const pathToSegmentNode = getNodePathFromSourceRange(
|
||||||
ast,
|
ast,
|
||||||
selection.graphSelections[0]?.codeRef?.range
|
selection.codeRef?.range
|
||||||
)
|
)
|
||||||
|
|
||||||
const varDecNode = getNodeFromPath<VariableDeclaration>(
|
const varDecNode = getNodeFromPath<VariableDeclaration>(
|
||||||
@ -308,7 +303,7 @@ async function updateAstAndFocus(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mutateAstWithTagForSketchSegment(
|
export function mutateAstWithTagForSketchSegment(
|
||||||
astClone: Node<Program>,
|
astClone: Node<Program>,
|
||||||
pathToSegmentNode: PathToNode
|
pathToSegmentNode: PathToNode
|
||||||
): { modifiedAst: Program; tag: string } | Error {
|
): { modifiedAst: Program; tag: string } | Error {
|
||||||
@ -340,7 +335,7 @@ function mutateAstWithTagForSketchSegment(
|
|||||||
return { modifiedAst: astClone, tag }
|
return { modifiedAst: astClone, tag }
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEdgeTagCall(
|
export function getEdgeTagCall(
|
||||||
tag: string,
|
tag: string,
|
||||||
artifact: Artifact
|
artifact: Artifact
|
||||||
): Node<Identifier | CallExpression> {
|
): Node<Identifier | CallExpression> {
|
||||||
|