Compare commits
55 Commits
fix-editor
...
achalmers/
Author | SHA1 | Date | |
---|---|---|---|
bf5287faab | |||
73bca2dcfc | |||
c6a50a3cdf | |||
b81c9d04cc | |||
9d8a7064da | |||
b0e6140e9f | |||
f9df7ff885 | |||
aec9637d7a | |||
e4c5fad8c7 | |||
cc0d601294 | |||
69cefafc19 | |||
b187ca3422 | |||
1edadcaa0f | |||
95c0ded8cf | |||
0ebb4e2cad | |||
f3e0939057 | |||
f5e233d8a0 | |||
1cab3e628f | |||
2ca6ba52b6 | |||
f741ea2e09 | |||
9f2a7781fc | |||
990f2b4154 | |||
0af0f15281 | |||
b558548b94 | |||
29e0f9a270 | |||
9385c32cfb | |||
ce3fb5c353 | |||
f920490518 | |||
d681e667ee | |||
5c6515a60e | |||
eb8a33312d | |||
d351b3bbe4 | |||
47d40eb801 | |||
adc4b6148d | |||
27d0d4a28b | |||
fb609c19ef | |||
8666989c85 | |||
bdf49c2084 | |||
a06b9d560a | |||
b81ff66f2b | |||
c0e6947170 | |||
65ebde0b34 | |||
0d6618b60a | |||
f0c44d11b3 | |||
44e71cd4bc | |||
a9f716dad8 | |||
a2455832e7 | |||
8f5034f997 | |||
af1c2c7ae1 | |||
ff38ae091e | |||
1dd7c95b8c | |||
20042ec87c | |||
fccf3508a7 | |||
8dab5527b8 | |||
f72eb0e8a7 |
@ -3,4 +3,3 @@ VITE_KC_API_BASE_URL=https://api.dev.zoo.dev
|
||||
VITE_KC_SITE_BASE_URL=https://dev.zoo.dev
|
||||
VITE_KC_SKIP_AUTH=false
|
||||
VITE_KC_CONNECTION_TIMEOUT_MS=5000
|
||||
VITE_KC_SENTRY_DSN=
|
||||
|
@ -3,4 +3,3 @@ VITE_KC_API_BASE_URL=https://api.zoo.dev
|
||||
VITE_KC_SITE_BASE_URL=https://zoo.dev
|
||||
VITE_KC_SKIP_AUTH=false
|
||||
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
||||
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224
|
||||
|
85
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,85 @@
|
||||
name: Bug Report
|
||||
description: File a bug report for the Zoo Modeling App
|
||||
title: "[BUG]: "
|
||||
labels: ["bug"]
|
||||
assignees: []
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "Thank you for taking the time to report a bug. Please provide as much information as possible to help us resolve it."
|
||||
|
||||
- type: textarea
|
||||
id: describe-bug
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of what the bug is.
|
||||
placeholder: "Explain the bug..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduce-bug
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Steps to reproduce the behavior.
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: Description of what you expected to happen.
|
||||
placeholder: "I expected that..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots and Recordings
|
||||
description: If applicable, add screenshots to help explain your problem. Maximum upload size is 10MB.
|
||||
placeholder: "You can attach images or video recordings here."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: desktop-os
|
||||
attributes:
|
||||
label: Desktop OS
|
||||
description: "Your operating system"
|
||||
placeholder: "example: Windows 10, MacOS Big Sur"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: browser
|
||||
attributes:
|
||||
label: Browser
|
||||
description: "If you are using the web version, please specify the browser you are using."
|
||||
placeholder: "example: Chrome, Safari"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: "The version of the Zoo Modeling App you're using."
|
||||
placeholder: "example: v0.15.0. You can find this in the settings."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context about the problem here.
|
||||
placeholder: "Anything else you want to add..."
|
||||
validations:
|
||||
required: false
|
16
.github/workflows/cargo-test.yml
vendored
@ -40,6 +40,20 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
- name: Install vector
|
||||
run: |
|
||||
curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh
|
||||
chmod +x /tmp/vector.sh
|
||||
/tmp/vector.sh -y -no-modify-path
|
||||
mkdir -p /tmp/vector
|
||||
cp .github/workflows/vector.toml /tmp/vector.toml
|
||||
sed -i "s#GITHUB_WORKFLOW#${GITHUB_WORKFLOW}#g" /tmp/vector.toml
|
||||
sed -i "s#GITHUB_REPOSITORY#${GITHUB_REPOSITORY}#g" /tmp/vector.toml
|
||||
sed -i "s#GITHUB_SHA#${GITHUB_SHA}#g" /tmp/vector.toml
|
||||
sed -i "s#GITHUB_REF_NAME#${GITHUB_REF_NAME}#g" /tmp/vector.toml
|
||||
sed -i "s#GH_ACTIONS_AXIOM_TOKEN#${{secrets.GH_ACTIONS_AXIOM_TOKEN}}#g" /tmp/vector.toml
|
||||
cat /tmp/vector.toml
|
||||
${HOME}/.vector/bin/vector --config /tmp/vector.toml &
|
||||
- uses: taiki-e/install-action@cargo-llvm-cov
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- name: Rust Cache
|
||||
@ -48,7 +62,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |-
|
||||
cd "${{ matrix.dir }}"
|
||||
cargo nextest run --workspace --no-fail-fast -P ci
|
||||
cargo nextest run --workspace --no-fail-fast -P ci 2>&1 | tee /tmp/github-actions.log
|
||||
env:
|
||||
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
|
||||
RUST_MIN_STACK: 10485760000
|
||||
|
3
.github/workflows/ci.yml
vendored
@ -336,7 +336,7 @@ jobs:
|
||||
cat last_download.json
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: 'google-github-actions/auth@v2.1.1'
|
||||
uses: 'google-github-actions/auth@v2.1.2'
|
||||
with:
|
||||
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
|
||||
|
||||
@ -374,6 +374,7 @@ jobs:
|
||||
announce_release:
|
||||
needs: [publish-apps-release]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'release'
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
7
.github/workflows/playwright.yml
vendored
@ -4,6 +4,11 @@ on:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
playwright-ubuntu:
|
||||
timeout-minutes: 60
|
||||
@ -14,7 +19,7 @@ jobs:
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
- uses: KittyCAD/action-install-cli@v0.2.16
|
||||
- uses: KittyCAD/action-install-cli@v0.2.21
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
- name: Install Playwright Browsers
|
||||
|
21
.github/workflows/vector.toml
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
[sources.github-actions-file]
|
||||
type = "file"
|
||||
data_dir = "/tmp/vector"
|
||||
include = ["/tmp/github-actions.log"]
|
||||
|
||||
# Modify the logs to include the action name.
|
||||
[transforms.add-action-name]
|
||||
type = "remap"
|
||||
inputs = [ "github-actions-file" ]
|
||||
source = '''
|
||||
.action = "GITHUB_WORKFLOW"
|
||||
.repo = "GITHUB_REPOSITORY"
|
||||
.sha = "GITHUB_SHA"
|
||||
.ref = "GITHUB_REF_NAME"
|
||||
'''
|
||||
|
||||
[sinks.axiom]
|
||||
type = "axiom"
|
||||
inputs = ["add-action-name"]
|
||||
token = "GH_ACTIONS_AXIOM_TOKEN"
|
||||
dataset = "github-actions"
|
2
.gitignore
vendored
@ -54,3 +54,5 @@ e2e/playwright/export-snapshots/*embedded.gltf
|
||||
|
||||
## generated files
|
||||
src/**/*.typegen.ts
|
||||
|
||||
src/wasm-lib/grackle/stdlib_cube_partial.json
|
||||
|
@ -6,11 +6,9 @@ once fixed in engine will just start working here with no language changes.
|
||||
- **Sketch on Face**: If your sketch is outside the edges of the face (on which you
|
||||
are sketching) you will get multiple models returned instead of one single
|
||||
model for that sketch and its underlying 3D object.
|
||||
If you see a red line around your model, it means this is happening.
|
||||
|
||||
- **Patterns**: If you try and pass a pattern to `hole` currently only the first
|
||||
item in the pattern is being subtracted. This is an engine bug that is being
|
||||
worked on.
|
||||
|
||||
- **Import**: Right now you can import a file, even if that file has brep data
|
||||
you cannot edit it. You also cannot move or transform the imported objects at
|
||||
all. In the future, after v1, the engine will account for this.
|
||||
you cannot edit it, after v1, the engine will account for this. You also cannot
|
||||
currently move or transform the imported objects at all, once we have assemblies
|
||||
this will work.
|
||||
|
6817
docs/kcl/std.json
1100
docs/kcl/std.md
Before Width: | Height: | Size: 259 KiB After Width: | Height: | Size: 259 KiB |
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 220 KiB |
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 220 KiB |
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 220 KiB |
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 221 KiB |
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 221 KiB |
@ -3,6 +3,7 @@ import { secrets } from './secrets'
|
||||
import { getUtils } from './test-utils'
|
||||
import waitOn from 'wait-on'
|
||||
import { Themes } from '../../src/lib/theme'
|
||||
import { roundOff } from 'lib/utils'
|
||||
|
||||
/*
|
||||
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
|
||||
@ -14,6 +15,12 @@ document.addEventListener('mousemove', (e) =>
|
||||
)
|
||||
*/
|
||||
|
||||
const commonPoints = {
|
||||
startAt: '[9.06, -12.22]',
|
||||
num1: 9.14,
|
||||
num2: 18.2,
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
// wait for Vite preview server to be up
|
||||
await waitOn({
|
||||
@ -52,14 +59,15 @@ test('Basic sketch', async ({ page }) => {
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
||||
|
||||
// click on "Start Sketch" button
|
||||
await u.clearCommandLogs()
|
||||
await u.doAndWaitForImageDiff(
|
||||
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
||||
200
|
||||
)
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// select a plane
|
||||
await page.mouse.click(700, 200)
|
||||
@ -72,35 +80,33 @@ test('Basic sketch', async ({ page }) => {
|
||||
|
||||
const startXPx = 600
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
const startAt = '[23.74, -32.03]'
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)`)
|
||||
|> startProfileAt(${commonPoints.startAt}, %)`)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await u.closeDebugPanel()
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
const num = 23.97
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${num}, 0], %)`)
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)`)
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${num}, 0], %)
|
||||
|> line([0, ${num}], %)`)
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)
|
||||
|> line([0, ${commonPoints.num1}], %)`)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${num}, 0], %)
|
||||
|> line([0, ${num}], %)
|
||||
|> line([-47.71, 0], %)`)
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)
|
||||
|> line([0, ${commonPoints.num1}], %)
|
||||
|> line([-${commonPoints.num2}, 0], %)`)
|
||||
|
||||
// deselect line tool
|
||||
await page.getByRole('button', { name: 'Line' }).click()
|
||||
@ -122,9 +128,9 @@ test('Basic sketch', async ({ page }) => {
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line({ to: [${num}, 0], tag: 'seg01' }, %)
|
||||
|> line([0, ${num}], %)
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line({ to: [${commonPoints.num1}, 0], tag: 'seg01' }, %)
|
||||
|> line([0, ${commonPoints.num1}], %)
|
||||
|> angledLine([180, segLen('seg01', %)], %)`)
|
||||
})
|
||||
|
||||
@ -276,10 +282,9 @@ test('Can create sketches on all planes and their back sides', async ({
|
||||
}) => {
|
||||
await u.openDebugPanel()
|
||||
|
||||
await u.updateCamPosition(viewCmd)
|
||||
|
||||
await u.clearCommandLogs()
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
await u.updateCamPosition(viewCmd)
|
||||
|
||||
await u.closeDebugPanel()
|
||||
await page.mouse.click(clickCoords.x, clickCoords.y)
|
||||
@ -305,11 +310,9 @@ test('Can create sketches on all planes and their back sides', async ({
|
||||
}
|
||||
|
||||
const codeTemplate = (
|
||||
plane = 'XY',
|
||||
rounded = false,
|
||||
otherThing = '1'
|
||||
plane = 'XY'
|
||||
) => `const part001 = startSketchOn('${plane}')
|
||||
|> startProfileAt([28.9${otherThing}, -39${rounded ? '' : '.01'}], %)`
|
||||
|> startProfileAt([1.14, -1.54], %)`
|
||||
await TestSinglePlane({
|
||||
viewCmd: camPos,
|
||||
expectedCode: codeTemplate('XY'),
|
||||
@ -318,8 +321,8 @@ test('Can create sketches on all planes and their back sides', async ({
|
||||
})
|
||||
await TestSinglePlane({
|
||||
viewCmd: camPos,
|
||||
expectedCode: codeTemplate('YZ', true),
|
||||
clickCoords: { x: 700, y: 300 }, // green plane
|
||||
expectedCode: codeTemplate('YZ'),
|
||||
clickCoords: { x: 700, y: 250 }, // green plane
|
||||
})
|
||||
await TestSinglePlane({
|
||||
viewCmd: camPos,
|
||||
@ -329,7 +332,7 @@ test('Can create sketches on all planes and their back sides', async ({
|
||||
const camCmdBackSide: [number, number, number] = [-100, -100, -100]
|
||||
await TestSinglePlane({
|
||||
viewCmd: camCmdBackSide,
|
||||
expectedCode: codeTemplate('-XY', false, '3'),
|
||||
expectedCode: codeTemplate('-XY'),
|
||||
clickCoords: { x: 601, y: 118 }, // back of red plane
|
||||
})
|
||||
await TestSinglePlane({
|
||||
@ -339,7 +342,7 @@ test('Can create sketches on all planes and their back sides', async ({
|
||||
})
|
||||
await TestSinglePlane({
|
||||
viewCmd: camCmdBackSide,
|
||||
expectedCode: codeTemplate('-XZ', true),
|
||||
expectedCode: codeTemplate('-XZ'),
|
||||
clickCoords: { x: 680, y: 427 }, // back of blue plane
|
||||
})
|
||||
})
|
||||
@ -380,12 +383,16 @@ test('Auto complete works', async ({ page }) => {
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type('(5, %)')
|
||||
// finish line with comment
|
||||
await page.keyboard.type('(5, %) // lin')
|
||||
await page.waitForTimeout(100)
|
||||
// there shouldn't be any auto complete options for 'lin' in the comment
|
||||
await expect(page.locator('.cm-completionLabel')).not.toBeVisible()
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('XY')
|
||||
|> startProfileAt([0,0], %)
|
||||
|> xLine(5, %)`)
|
||||
|> xLine(5, %) // lin`)
|
||||
})
|
||||
|
||||
// Onboarding tests
|
||||
@ -453,6 +460,9 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
page.mouse.click(767, 396).then(() => page.waitForTimeout(100))
|
||||
|
||||
await u.clearCommandLogs()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
|
||||
// select a plane
|
||||
@ -461,35 +471,32 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
|
||||
const startXPx = 600
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
const startAt = '[23.74, -32.03]'
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)`)
|
||||
|> startProfileAt(${commonPoints.startAt}, %)`)
|
||||
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
|
||||
const num = 23.97
|
||||
const num2 = '47.71'
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${num}, 0], %)`)
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)`)
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${num}, 0], %)
|
||||
|> line([0, ${num}], %)`)
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)
|
||||
|> line([0, ${commonPoints.num1}], %)`)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${num}, 0], %)
|
||||
|> line([0, ${num}], %)
|
||||
|> line([-${num2}, 0], %)`)
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)
|
||||
|> line([0, ${commonPoints.num1}], %)
|
||||
|> line([-${commonPoints.num2}, 0], %)`)
|
||||
|
||||
// deselect line tool
|
||||
await page.getByRole('button', { name: 'Line' }).click()
|
||||
@ -539,7 +546,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
await emptySpaceClick()
|
||||
|
||||
// check the same selection again by putting cursor in code first then selecting axis
|
||||
await page.getByText(` |> line([-${num2}, 0], %)`).click()
|
||||
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click()
|
||||
await page.keyboard.down('Shift')
|
||||
await expect(absYButton).toBeDisabled()
|
||||
await xAxisClick()
|
||||
@ -550,7 +557,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
await emptySpaceClick()
|
||||
|
||||
// select segment in editor than another segment in scene and check there are two cursors
|
||||
await page.getByText(` |> line([-${num2}, 0], %)`).click()
|
||||
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click()
|
||||
await page.waitForTimeout(300)
|
||||
await page.keyboard.down('Shift')
|
||||
await expect(page.locator('.cm-cursor')).toHaveCount(1)
|
||||
@ -575,7 +582,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
|
||||
// select a line
|
||||
// await topHorzSegmentClick()
|
||||
await page.getByText(startAt).click() // TODO remove this and reinstate // await topHorzSegmentClick()
|
||||
await page.getByText(commonPoints.startAt).click() // TODO remove this and reinstate // await topHorzSegmentClick()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// enter sketch again
|
||||
@ -637,12 +644,15 @@ test('Can extrude from the command bar', async ({ page, context }) => {
|
||||
await context.addInitScript(async (token) => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([-6.95, 4.98], %)
|
||||
|> line([25.1, 0.41], %)
|
||||
|> line([0.73, -14.93], %)
|
||||
|> line([-23.44, 0.52], %)
|
||||
|> close(%)`
|
||||
`
|
||||
const distance = sqrt(20)
|
||||
const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([-6.95, 4.98], %)
|
||||
|> line([25.1, 0.41], %)
|
||||
|> line([0.73, -14.93], %)
|
||||
|> line([-23.44, 0.52], %)
|
||||
|> close(%)
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
@ -667,24 +677,44 @@ test('Can extrude from the command bar', async ({ page, context }) => {
|
||||
// Click to select face and set distance
|
||||
await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click()
|
||||
await page.getByRole('button', { name: 'Continue' }).click()
|
||||
|
||||
// Assert that we're on the distance step
|
||||
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled()
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// Assert that the an alternative variable name is chosen,
|
||||
// since the default variable name is already in use (distance)
|
||||
await page.getByRole('button', { name: 'Create new variable' }).click()
|
||||
await expect(page.getByPlaceholder('Variable name')).toHaveValue(
|
||||
'distance001'
|
||||
)
|
||||
await expect(page.getByRole('button', { name: 'Continue' })).toBeEnabled()
|
||||
await page.getByRole('button', { name: 'Continue' }).click()
|
||||
|
||||
// Review step and argument hotkeys
|
||||
await page.keyboard.press('2')
|
||||
await expect(page.getByRole('button', { name: '5' })).toBeDisabled()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Submit command' })
|
||||
).toBeEnabled()
|
||||
await page.keyboard.press('Backspace')
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Distance 12', exact: false })
|
||||
).toBeDisabled()
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
await expect(page.getByText('Confirm Extrude')).toBeVisible()
|
||||
|
||||
// Check that the code was updated
|
||||
await page.keyboard.press('Enter')
|
||||
// Unfortunately this indentation seems to matter for the test
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([-6.95, 4.98], %)
|
||||
|> line([25.1, 0.41], %)
|
||||
|> line([0.73, -14.93], %)
|
||||
|> line([-23.44, 0.52], %)
|
||||
|> close(%)
|
||||
|> extrude(5, %)`
|
||||
`const distance = sqrt(20)
|
||||
const distance001 = 5 + 7
|
||||
const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([-6.95, 4.98], %)
|
||||
|> line([25.1, 0.41], %)
|
||||
|> line([0.73, -14.93], %)
|
||||
|> line([-23.44, 0.52], %)
|
||||
|> close(%)
|
||||
|> extrude(distance001, %)`.replace(/(\r\n|\n|\r)/gm, '') // remove newlines
|
||||
)
|
||||
})
|
||||
|
||||
@ -696,6 +726,9 @@ test('Can add multiple sketches', async ({ page }) => {
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
||||
|
||||
// click on "Start Sketch" button
|
||||
@ -716,34 +749,32 @@ test('Can add multiple sketches', async ({ page }) => {
|
||||
|
||||
const startXPx = 600
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
const startAt = '[23.74, -32.03]'
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)`)
|
||||
|> startProfileAt(${commonPoints.startAt}, %)`)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await u.closeDebugPanel()
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
const num = 23.97
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${num}, 0], %)`)
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)`)
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${num}, 0], %)
|
||||
|> line([0, ${num}], %)`)
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)
|
||||
|> line([0, ${commonPoints.num1}], %)`)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
const finalCodeFirstSketch = `const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${num}, 0], %)
|
||||
|> line([0, ${num}], %)
|
||||
|> line([-47.71, 0], %)`
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)
|
||||
|> line([0, ${commonPoints.num1}], %)
|
||||
|> line([-${commonPoints.num2}, 0], %)`
|
||||
await expect(page.locator('.cm-content')).toHaveText(finalCodeFirstSketch)
|
||||
|
||||
// exit the sketch
|
||||
@ -765,7 +796,7 @@ test('Can add multiple sketches', async ({ page }) => {
|
||||
await u.clearAndCloseDebugPanel()
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
const startAt2 = '[23.61, -31.85]'
|
||||
const startAt2 = '[0.93,-1.25]'
|
||||
await expect(
|
||||
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
||||
).toBe(
|
||||
@ -779,7 +810,7 @@ const part002 = startSketchOn('XY')
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
const num2 = 23.83
|
||||
const num2 = 0.94
|
||||
await expect(
|
||||
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
||||
).toBe(
|
||||
@ -797,7 +828,7 @@ const part002 = startSketchOn('XY')
|
||||
const part002 = startSketchOn('XY')
|
||||
|> startProfileAt(${startAt2}, %)
|
||||
|> line([${num2}, 0], %)
|
||||
|> line([0, ${num2}], %)`.replace(/\s/g, '')
|
||||
|> line([0, ${roundOff(num2 - 0.01)}], %)`.replace(/\s/g, '')
|
||||
)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
await expect(
|
||||
@ -807,8 +838,8 @@ const part002 = startSketchOn('XY')
|
||||
const part002 = startSketchOn('XY')
|
||||
|> startProfileAt(${startAt2}, %)
|
||||
|> line([${num2}, 0], %)
|
||||
|> line([0, ${num2}], %)
|
||||
|> line([-47.44, 0], %)`.replace(/\s/g, '')
|
||||
|> line([0, ${roundOff(num2 - 0.01)}], %)
|
||||
|> line([-1.87, 0], %)`.replace(/\s/g, '')
|
||||
)
|
||||
})
|
||||
|
||||
@ -902,7 +933,7 @@ fn yohey = (pos) => {
|
||||
|> line([-15.79, 17.08], %)
|
||||
return ''
|
||||
}
|
||||
|
||||
|
||||
yohey([15.79, -34.6])
|
||||
`
|
||||
)
|
||||
@ -918,6 +949,11 @@ fn yohey = (pos) => {
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
// wait for start sketch as a proxy for the stream being ready
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
|
||||
await page.getByText(selectionsSnippets.extrudeAndEditBlocked).click()
|
||||
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
|
||||
await expect(
|
||||
@ -964,6 +1000,9 @@ test('Deselecting line tool should mean nothing happens on click', async ({
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
||||
|
||||
// click on "Start Sketch" button
|
||||
|
@ -31,6 +31,12 @@ test.beforeEach(async ({ context, page }) => {
|
||||
|
||||
test.setTimeout(60000)
|
||||
|
||||
const commonPoints = {
|
||||
startAt: '[26.38, -35.59]',
|
||||
num1: 26.63,
|
||||
num2: 53.01,
|
||||
}
|
||||
|
||||
test('change camera, show planes', async ({ page, context }) => {
|
||||
const u = getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
@ -160,7 +166,7 @@ const part001 = startSketchOn('-XZ')
|
||||
}, %)
|
||||
|> angledLineToY([segAng('seg02', %) + 180, -baseHeight], %)
|
||||
|> xLineTo(ZERO, %)
|
||||
|> close(%)
|
||||
|> close(%)
|
||||
|> extrude(4, %)`
|
||||
)
|
||||
})
|
||||
@ -342,24 +348,33 @@ const part001 = startSketchOn('-XZ')
|
||||
// snapshot exports, good compromise to capture that exports are healthy without getting bogged down in "did the formatting change" changes
|
||||
// context: https://github.com/KittyCAD/modeling-app/issues/1222
|
||||
for (const { modelPath, imagePath, outputType } of exportLocations) {
|
||||
const cliCommand = `export KITTYCAD_TOKEN=${secrets.snapshottoken} && kittycad file snapshot --output-format=png --src-format=${outputType} ${modelPath} ${imagePath}`
|
||||
console.log(
|
||||
`taking snapshot of using: "zoo file snapshot --output-format=png --src-format=${outputType} ${modelPath} ${imagePath}"`
|
||||
)
|
||||
const cliCommand = `export ZOO_TOKEN=${secrets.snapshottoken} && zoo file snapshot --output-format=png --src-format=${outputType} ${modelPath} ${imagePath}`
|
||||
const child = spawn(cliCommand, { shell: true })
|
||||
await new Promise((resolve, reject) => {
|
||||
const result = await new Promise<string>((resolve, reject) => {
|
||||
child.on('error', (code: any, msg: any) => {
|
||||
console.log('error', code, msg)
|
||||
reject()
|
||||
reject('error')
|
||||
})
|
||||
child.on('exit', (code, msg) => {
|
||||
console.log('exit', code, msg)
|
||||
if (code !== 0) {
|
||||
reject(`exit code ${code} for model ${modelPath}`)
|
||||
} else {
|
||||
resolve(true)
|
||||
resolve('success')
|
||||
}
|
||||
})
|
||||
child.stderr.on('data', (data) => console.log(`stderr: ${data}`))
|
||||
child.stdout.on('data', (data) => console.log(`stdout: ${data}`))
|
||||
})
|
||||
expect(result).toBe('success')
|
||||
if (result === 'success') {
|
||||
console.log(`snapshot taken for ${modelPath}`)
|
||||
} else {
|
||||
console.log(`snapshot failed for ${modelPath}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -369,13 +384,13 @@ test('extrude on each default plane should be stable', async ({
|
||||
}) => {
|
||||
const u = getUtils(page)
|
||||
const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}')
|
||||
|> startProfileAt([0.70, 0.44], %)
|
||||
|> line([0.66, -0.02], %)
|
||||
|> line([0.28, 0.50], %)
|
||||
|> line([-0.56, 0.44], %)
|
||||
|> line([-0.54, -0.38], %)
|
||||
|> startProfileAt([7.00, 4.40], %)
|
||||
|> line([6.60, -0.20], %)
|
||||
|> line([2.80, 5.00], %)
|
||||
|> line([-5.60, 4.40], %)
|
||||
|> line([-5.40, -3.80], %)
|
||||
|> close(%)
|
||||
|> extrude(1.00, %)
|
||||
|> extrude(10.00, %)
|
||||
`
|
||||
await context.addInitScript(async (code) => {
|
||||
localStorage.setItem('persistCode', code)
|
||||
@ -420,7 +435,23 @@ test('extrude on each default plane should be stable', async ({
|
||||
await runSnapshotsForOtherPlanes('-YZ')
|
||||
})
|
||||
|
||||
test('Draft segments should look right', async ({ page }) => {
|
||||
test('Draft segments should look right', async ({ page, context }) => {
|
||||
await context.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'SETTINGS_PERSIST_KEY',
|
||||
JSON.stringify({
|
||||
baseUnit: 'in',
|
||||
cameraControls: 'KittyCAD',
|
||||
defaultDirectory: '',
|
||||
defaultProjectName: 'project-$nnn',
|
||||
onboardingStatus: 'dismissed',
|
||||
showDebugPanel: true,
|
||||
textWrapping: 'On',
|
||||
theme: 'system',
|
||||
unitSystem: 'imperial',
|
||||
})
|
||||
)
|
||||
})
|
||||
const u = getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
@ -428,6 +459,9 @@ test('Draft segments should look right', async ({ page }) => {
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
||||
|
||||
// click on "Start Sketch" button
|
||||
@ -448,10 +482,9 @@ test('Draft segments should look right', async ({ page }) => {
|
||||
|
||||
const startXPx = 600
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
const startAt = '[23.74, -32.03]'
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)`)
|
||||
|> startProfileAt([9.06, -12.22], %)`)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await u.closeDebugPanel()
|
||||
@ -463,11 +496,10 @@ test('Draft segments should look right', async ({ page }) => {
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
const num = 23.97
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${num}, 0], %)`)
|
||||
|> startProfileAt([9.06, -12.22], %)
|
||||
|> line([9.14, 0], %)`)
|
||||
|
||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||
|
||||
@ -477,3 +509,202 @@ test('Draft segments should look right', async ({ page }) => {
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
})
|
||||
|
||||
test('Client side scene scale should match engine scale inch', async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
await context.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'SETTINGS_PERSIST_KEY',
|
||||
JSON.stringify({
|
||||
baseUnit: 'in',
|
||||
cameraControls: 'KittyCAD',
|
||||
defaultDirectory: '',
|
||||
defaultProjectName: 'project-$nnn',
|
||||
onboardingStatus: 'dismissed',
|
||||
showDebugPanel: true,
|
||||
textWrapping: 'On',
|
||||
theme: 'system',
|
||||
unitSystem: 'imperial',
|
||||
})
|
||||
)
|
||||
})
|
||||
const u = getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
||||
|
||||
// click on "Start Sketch" button
|
||||
await u.clearCommandLogs()
|
||||
await u.doAndWaitForImageDiff(
|
||||
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
||||
200
|
||||
)
|
||||
|
||||
// select a plane
|
||||
await page.mouse.click(700, 200)
|
||||
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`const part001 = startSketchOn('-XZ')`
|
||||
)
|
||||
|
||||
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
|
||||
|
||||
const startXPx = 600
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([9.06, -12.22], %)`)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([9.06, -12.22], %)
|
||||
|> line([9.14, 0], %)`)
|
||||
|
||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([9.06, -12.22], %)
|
||||
|> line([9.14, 0], %)
|
||||
|> tangentialArcTo([27.34, -3.08], %)`)
|
||||
|
||||
// click tangential arc tool again to unequip it
|
||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// screen shot should show the sketch
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
|
||||
// exit sketch
|
||||
await u.openAndClearDebugPanel()
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
|
||||
// wait for execution done
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.clearAndCloseDebugPanel()
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// second screen shot should look almost identical, i.e. scale should be the same.
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
})
|
||||
|
||||
test('Client side scene scale should match engine scale mm', async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
await context.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'SETTINGS_PERSIST_KEY',
|
||||
JSON.stringify({
|
||||
baseUnit: 'mm',
|
||||
cameraControls: 'KittyCAD',
|
||||
defaultDirectory: '',
|
||||
defaultProjectName: 'project-$nnn',
|
||||
onboardingStatus: 'dismissed',
|
||||
showDebugPanel: true,
|
||||
textWrapping: 'On',
|
||||
theme: 'system',
|
||||
unitSystem: 'metric',
|
||||
})
|
||||
)
|
||||
})
|
||||
const u = getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
||||
|
||||
// click on "Start Sketch" button
|
||||
await u.clearCommandLogs()
|
||||
await u.doAndWaitForImageDiff(
|
||||
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
||||
200
|
||||
)
|
||||
|
||||
// select a plane
|
||||
await page.mouse.click(700, 200)
|
||||
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`const part001 = startSketchOn('-XZ')`
|
||||
)
|
||||
|
||||
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
|
||||
|
||||
const startXPx = 600
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([230.03, -310.33], %)`)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([230.03, -310.33], %)
|
||||
|> line([232.2, 0], %)`)
|
||||
|
||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([230.03, -310.33], %)
|
||||
|> line([232.2, 0], %)
|
||||
|> tangentialArcTo([694.43, -78.12], %)`)
|
||||
|
||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// screen shot should show the sketch
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
|
||||
// exit sketch
|
||||
await u.openAndClearDebugPanel()
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
|
||||
// wait for execution done
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.clearAndCloseDebugPanel()
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// second screen shot should look almost identical, i.e. scale should be the same.
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
})
|
||||
|
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 50 KiB |
After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 118 KiB |
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 126 KiB |
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 115 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 53 KiB |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "untitled-app",
|
||||
"version": "0.15.2",
|
||||
"version": "0.15.4",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.10.2",
|
||||
@ -10,12 +10,11 @@
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@kittycad/lib": "^0.0.53",
|
||||
"@kittycad/lib": "^0.0.54",
|
||||
"@lezer/javascript": "^1.4.9",
|
||||
"@open-rpc/client-js": "^1.8.1",
|
||||
"@react-hook/resize-observer": "^1.2.6",
|
||||
"@replit/codemirror-interact": "^6.3.0",
|
||||
"@sentry/react": "^7.77.0",
|
||||
"@tauri-apps/api": "^1.5.1",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
@ -85,7 +84,7 @@
|
||||
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
|
||||
"lint": "eslint --fix src",
|
||||
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json",
|
||||
"postinstall": "patch-package && yarn xstate:typegen",
|
||||
"postinstall": "yarn xstate:typegen",
|
||||
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\""
|
||||
},
|
||||
"prettier": {
|
||||
@ -134,7 +133,6 @@
|
||||
"eslint-plugin-css-modules": "^2.12.0",
|
||||
"happy-dom": "^10.8.0",
|
||||
"husky": "^8.0.3",
|
||||
"patch-package": "^8.0.0",
|
||||
"pixelmatch": "^5.3.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"postcss": "^8.4.31",
|
||||
|
@ -1,138 +0,0 @@
|
||||
diff --git a/node_modules/three/examples/jsm/controls/OrbitControls.js b/node_modules/three/examples/jsm/controls/OrbitControls.js
|
||||
index f29e7fe..0ef636b 100644
|
||||
--- a/node_modules/three/examples/jsm/controls/OrbitControls.js
|
||||
+++ b/node_modules/three/examples/jsm/controls/OrbitControls.js
|
||||
@@ -113,6 +113,25 @@ class OrbitControls extends EventDispatcher {
|
||||
// public methods
|
||||
//
|
||||
|
||||
+ this.interactionGuards = {
|
||||
+ pan: {
|
||||
+ description: 'Right click + Shift + drag or middle click + drag',
|
||||
+ callback: (e) => e.button === 2 && !e.ctrlKey,
|
||||
+ },
|
||||
+ zoom: {
|
||||
+ description: 'Scroll wheel or Right click + Ctrl + drag',
|
||||
+ dragCallback: (e) => e.button === 2 && e.ctrlKey,
|
||||
+ scrollCallback: () => true,
|
||||
+ },
|
||||
+ rotate: {
|
||||
+ description: 'Right click + drag',
|
||||
+ callback: (e) => e.button === 0,
|
||||
+ },
|
||||
+ }
|
||||
+ this.setMouseGuards = (interactionGuards) => {
|
||||
+ this.interactionGuards = interactionGuards
|
||||
+ }
|
||||
+
|
||||
this.getPolarAngle = function () {
|
||||
|
||||
return spherical.phi;
|
||||
@@ -1057,92 +1076,21 @@ class OrbitControls extends EventDispatcher {
|
||||
|
||||
function onMouseDown( event ) {
|
||||
|
||||
- let mouseAction;
|
||||
-
|
||||
- switch ( event.button ) {
|
||||
-
|
||||
- case 0:
|
||||
-
|
||||
- mouseAction = scope.mouseButtons.LEFT;
|
||||
- break;
|
||||
-
|
||||
- case 1:
|
||||
-
|
||||
- mouseAction = scope.mouseButtons.MIDDLE;
|
||||
- break;
|
||||
-
|
||||
- case 2:
|
||||
-
|
||||
- mouseAction = scope.mouseButtons.RIGHT;
|
||||
- break;
|
||||
-
|
||||
- default:
|
||||
-
|
||||
- mouseAction = - 1;
|
||||
-
|
||||
- }
|
||||
-
|
||||
- switch ( mouseAction ) {
|
||||
-
|
||||
- case MOUSE.DOLLY:
|
||||
-
|
||||
- if ( scope.enableZoom === false ) return;
|
||||
-
|
||||
- handleMouseDownDolly( event );
|
||||
-
|
||||
- state = STATE.DOLLY;
|
||||
-
|
||||
- break;
|
||||
-
|
||||
- case MOUSE.ROTATE:
|
||||
-
|
||||
- if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
|
||||
-
|
||||
- if ( scope.enablePan === false ) return;
|
||||
-
|
||||
- handleMouseDownPan( event );
|
||||
-
|
||||
- state = STATE.PAN;
|
||||
-
|
||||
- } else {
|
||||
-
|
||||
- if ( scope.enableRotate === false ) return;
|
||||
-
|
||||
- handleMouseDownRotate( event );
|
||||
-
|
||||
- state = STATE.ROTATE;
|
||||
-
|
||||
- }
|
||||
-
|
||||
- break;
|
||||
-
|
||||
- case MOUSE.PAN:
|
||||
-
|
||||
- if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
|
||||
-
|
||||
- if ( scope.enableRotate === false ) return;
|
||||
-
|
||||
- handleMouseDownRotate( event );
|
||||
-
|
||||
- state = STATE.ROTATE;
|
||||
-
|
||||
- } else {
|
||||
-
|
||||
- if ( scope.enablePan === false ) return;
|
||||
-
|
||||
- handleMouseDownPan( event );
|
||||
-
|
||||
- state = STATE.PAN;
|
||||
-
|
||||
- }
|
||||
-
|
||||
- break;
|
||||
-
|
||||
- default:
|
||||
-
|
||||
- state = STATE.NONE;
|
||||
-
|
||||
- }
|
||||
+ if (scope.interactionGuards.pan.callback(event)) {
|
||||
+ if (scope.enablePan === false) return
|
||||
+ handleMouseDownPan(event)
|
||||
+ state = STATE.PAN
|
||||
+ } else if (scope.interactionGuards.rotate.callback(event)) {
|
||||
+ if (scope.enableRotate === false) return
|
||||
+ handleMouseDownRotate(event)
|
||||
+ state = STATE.ROTATE
|
||||
+ } else if (scope.interactionGuards.zoom.dragCallback(event)) {
|
||||
+ if (scope.enableZoom === false) return
|
||||
+ handleMouseDownDolly(event)
|
||||
+ state = STATE.DOLLY
|
||||
+ } else {
|
||||
+ return
|
||||
+ }
|
||||
|
||||
if ( state !== STATE.NONE ) {
|
||||
|
@ -18,7 +18,7 @@ export default defineConfig({
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 3 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : 1,
|
||||
workers: process.env.CI ? 2 : 1,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
|
22
src-tauri/Cargo.lock
generated
@ -67,9 +67,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.79"
|
||||
version = "1.0.80"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
|
||||
checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
|
||||
|
||||
[[package]]
|
||||
name = "app"
|
||||
@ -1664,9 +1664,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kittycad"
|
||||
version = "0.2.53"
|
||||
version = "0.2.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a086e1a1bbddb3b38959c0f0ce6de6b3a3b7566e38e0b7d5fb101e91911beed4"
|
||||
checksum = "049c3881ffbe77bf1c3a968372a246ce906eceb79f61cd0bc5fa229bec3504cb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -3235,9 +3235,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.196"
|
||||
version = "1.0.197"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
|
||||
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@ -3253,9 +3253,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.196"
|
||||
version = "1.0.197"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
|
||||
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -3275,9 +3275,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.113"
|
||||
version = "1.0.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
|
||||
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
|
||||
dependencies = [
|
||||
"itoa 1.0.6",
|
||||
"ryu",
|
||||
@ -3872,7 +3872,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs-extra"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#01211ff0759d578e0e9ac8c98c31fdf09077eb34"
|
||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#ed682dd96eb765e7cd3cdbc3cc64f794a0d6f9df"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
|
@ -16,7 +16,7 @@ tauri-build = { version = "1.5.1", features = [] }
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
kittycad = "0.2.53"
|
||||
kittycad = "0.2.58"
|
||||
oauth2 = "4.4.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
@ -7,7 +7,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "zoo-modeling-app",
|
||||
"version": "0.15.2"
|
||||
"version": "0.15.4"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
@ -35,9 +35,9 @@ import {
|
||||
settingsMachine,
|
||||
} from './machines/settingsMachine'
|
||||
import { ContextFrom } from 'xstate'
|
||||
import CommandBarProvider from 'components/CommandBar/CommandBar'
|
||||
import { TEST, VITE_KC_SENTRY_DSN } from './env'
|
||||
import * as Sentry from '@sentry/react'
|
||||
import CommandBarProvider, {
|
||||
CommandBar,
|
||||
} from 'components/CommandBar/CommandBar'
|
||||
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
||||
import { KclContextProvider, kclManager } from 'lang/KclSingleton'
|
||||
import FileMachineProvider from 'components/FileMachineProvider'
|
||||
@ -46,38 +46,6 @@ import { paths } from 'lib/paths'
|
||||
import { IndexLoaderData, HomeLoaderData } from 'lib/types'
|
||||
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
||||
|
||||
if (VITE_KC_SENTRY_DSN && !TEST) {
|
||||
Sentry.init({
|
||||
dsn: VITE_KC_SENTRY_DSN,
|
||||
// TODO(paultag): pass in the right env here.
|
||||
// environment: "production",
|
||||
integrations: [
|
||||
new Sentry.BrowserTracing({
|
||||
routingInstrumentation: Sentry.reactRouterV6Instrumentation(
|
||||
useEffect,
|
||||
useLocation,
|
||||
useNavigationType,
|
||||
createRoutesFromChildren,
|
||||
matchRoutes
|
||||
),
|
||||
}),
|
||||
new Sentry.Replay(),
|
||||
],
|
||||
|
||||
// Set tracesSampleRate to 1.0 to capture 100%
|
||||
// of transactions for performance monitoring.
|
||||
tracesSampleRate: 1.0,
|
||||
|
||||
// TODO: Add in kittycad.io endpoints
|
||||
tracePropagationTargets: ['localhost'],
|
||||
|
||||
// Capture Replay for 10% of all sessions,
|
||||
// plus for 100% of sessions with an error
|
||||
replaysSessionSampleRate: 0.1,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
})
|
||||
}
|
||||
|
||||
export const BROWSER_FILE_NAME = 'new'
|
||||
|
||||
type CreateBrowserRouterArg = Parameters<typeof createBrowserRouter>[0]
|
||||
@ -117,6 +85,7 @@ const router = createBrowserRouter(
|
||||
<ModelingMachineProvider>
|
||||
<Outlet />
|
||||
<App />
|
||||
<CommandBar />
|
||||
</ModelingMachineProvider>
|
||||
<WasmErrBanner />
|
||||
</FileMachineProvider>
|
||||
@ -216,6 +185,7 @@ const router = createBrowserRouter(
|
||||
<Auth>
|
||||
<Outlet />
|
||||
<Home />
|
||||
<CommandBar />
|
||||
</Auth>
|
||||
),
|
||||
loader: async (): Promise<HomeLoaderData | Response> => {
|
||||
|
@ -6,7 +6,12 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
import usePlatform from 'hooks/usePlatform'
|
||||
import { isSingleCursorInPipe } from 'lang/queryAst'
|
||||
import { kclManager } from 'lang/KclSingleton'
|
||||
import { kclManager, useKclContext } from 'lang/KclSingleton'
|
||||
import {
|
||||
NetworkHealthState,
|
||||
useNetworkStatus,
|
||||
} from 'components/NetworkHealthIndicator'
|
||||
import { useStore } from 'useStore'
|
||||
|
||||
export const Toolbar = () => {
|
||||
const platform = usePlatform()
|
||||
@ -24,6 +29,13 @@ export const Toolbar = () => {
|
||||
context.selectionRanges
|
||||
)
|
||||
}, [engineCommandManager.artifactMap, context.selectionRanges])
|
||||
const { overallState } = useNetworkStatus()
|
||||
const { isExecuting } = useKclContext()
|
||||
const { isStreamReady } = useStore((s) => ({
|
||||
isStreamReady: s.isStreamReady,
|
||||
}))
|
||||
const disableAllButtons =
|
||||
overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady
|
||||
|
||||
function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) {
|
||||
const span = toolbarButtonsRef.current
|
||||
@ -60,6 +72,7 @@ export const Toolbar = () => {
|
||||
icon: 'sketch',
|
||||
bgClassName,
|
||||
}}
|
||||
disabled={disableAllButtons}
|
||||
>
|
||||
<span data-testid="start-sketch">Start Sketch</span>
|
||||
</ActionButton>
|
||||
@ -74,6 +87,7 @@ export const Toolbar = () => {
|
||||
icon: 'sketch',
|
||||
bgClassName,
|
||||
}}
|
||||
disabled={disableAllButtons}
|
||||
>
|
||||
Edit Sketch
|
||||
</ActionButton>
|
||||
@ -88,6 +102,7 @@ export const Toolbar = () => {
|
||||
icon: 'arrowLeft',
|
||||
bgClassName,
|
||||
}}
|
||||
disabled={disableAllButtons}
|
||||
>
|
||||
Exit Sketch
|
||||
</ActionButton>
|
||||
@ -109,6 +124,7 @@ export const Toolbar = () => {
|
||||
icon: 'line',
|
||||
bgClassName,
|
||||
}}
|
||||
disabled={disableAllButtons}
|
||||
>
|
||||
Line
|
||||
</ActionButton>
|
||||
@ -128,8 +144,9 @@ export const Toolbar = () => {
|
||||
bgClassName,
|
||||
}}
|
||||
disabled={
|
||||
!state.can('Equip tangential arc to') &&
|
||||
!state.matches('Sketch.Tangential arc to')
|
||||
(!state.can('Equip tangential arc to') &&
|
||||
!state.matches('Sketch.Tangential arc to')) ||
|
||||
disableAllButtons
|
||||
}
|
||||
>
|
||||
Tangential Arc
|
||||
@ -169,7 +186,7 @@ export const Toolbar = () => {
|
||||
disabled={
|
||||
!state.nextEvents
|
||||
.filter((event) => state.can(event as any))
|
||||
.includes(eventName)
|
||||
.includes(eventName) || disableAllButtons
|
||||
}
|
||||
title={eventName}
|
||||
icon={{
|
||||
@ -194,7 +211,7 @@ export const Toolbar = () => {
|
||||
data: { name: 'Extrude', ownerMachine: 'modeling' },
|
||||
})
|
||||
}
|
||||
disabled={!state.can('Extrude')}
|
||||
disabled={!state.can('Extrude') || disableAllButtons}
|
||||
title={
|
||||
state.can('Extrude')
|
||||
? 'extrude'
|
||||
|
1027
src/clientSideScene/CameraControls.ts
Normal file
@ -4,11 +4,8 @@ import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { useStore } from 'useStore'
|
||||
import {
|
||||
DEBUG_SHOW_BOTH_SCENES,
|
||||
ReactCameraProperties,
|
||||
sceneInfra,
|
||||
} from './sceneInfra'
|
||||
import { DEBUG_SHOW_BOTH_SCENES, sceneInfra } from './sceneInfra'
|
||||
import { ReactCameraProperties } from './CameraControls'
|
||||
import { throttle } from 'lib/utils'
|
||||
|
||||
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
||||
@ -18,7 +15,7 @@ function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
||||
const { state } = useModelingContext()
|
||||
|
||||
useEffect(() => {
|
||||
sceneInfra.setIsCamMovingCallback((isMoving, isTween) => {
|
||||
sceneInfra.camControls.setIsCamMovingCallback((isMoving, isTween) => {
|
||||
setIsCamMoving(isMoving)
|
||||
setIsTween(isTween)
|
||||
})
|
||||
@ -52,7 +49,8 @@ export const ClientSideScene = ({
|
||||
// Listen for changes to the camera controls setting
|
||||
// and update the client-side scene's controls accordingly.
|
||||
useEffect(() => {
|
||||
sceneInfra.setInteractionGuards(cameraMouseDragGuards[cameraControls])
|
||||
sceneInfra.camControls.interactionGuards =
|
||||
cameraMouseDragGuards[cameraControls]
|
||||
}, [cameraControls])
|
||||
useEffect(() => {
|
||||
sceneInfra.updateOtherSelectionColors(
|
||||
@ -93,7 +91,7 @@ export const ClientSideScene = ({
|
||||
|
||||
const throttled = throttle((a: ReactCameraProperties) => {
|
||||
if (a.type === 'perspective' && a.fov) {
|
||||
sceneInfra.dollyZoom(a.fov)
|
||||
sceneInfra.camControls.dollyZoom(a.fov)
|
||||
}
|
||||
}, 1000 / 15)
|
||||
|
||||
@ -107,7 +105,7 @@ export const CamDebugSettings = () => {
|
||||
const [fov, setFov] = useState(12)
|
||||
|
||||
useEffect(() => {
|
||||
sceneInfra.setReactCameraPropertiesCallback(setCamSettings)
|
||||
sceneInfra.camControls.setReactCameraPropertiesCallback(setCamSettings)
|
||||
}, [sceneInfra])
|
||||
useEffect(() => {
|
||||
if (camSettings.type === 'perspective' && camSettings.fov) {
|
||||
@ -124,9 +122,9 @@ export const CamDebugSettings = () => {
|
||||
checked={camSettings.type === 'perspective'}
|
||||
onChange={(e) => {
|
||||
if (camSettings.type === 'perspective') {
|
||||
sceneInfra.useOrthographicCamera()
|
||||
sceneInfra.camControls.useOrthographicCamera()
|
||||
} else {
|
||||
sceneInfra.usePerspectiveCamera()
|
||||
sceneInfra.camControls.usePerspectiveCamera()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@ -156,7 +154,7 @@ export const CamDebugSettings = () => {
|
||||
value={camSettings.fov}
|
||||
className="text-black w-16"
|
||||
onChange={(e) => {
|
||||
sceneInfra.setCam({
|
||||
sceneInfra.camControls.setCam({
|
||||
...camSettings,
|
||||
fov: parseFloat(e.target.value),
|
||||
})
|
||||
@ -173,7 +171,7 @@ export const CamDebugSettings = () => {
|
||||
value={camSettings.zoom}
|
||||
className="text-black w-16"
|
||||
onChange={(e) => {
|
||||
sceneInfra.setCam({
|
||||
sceneInfra.camControls.setCam({
|
||||
...camSettings,
|
||||
zoom: parseFloat(e.target.value),
|
||||
})
|
||||
@ -194,7 +192,7 @@ export const CamDebugSettings = () => {
|
||||
value={camSettings.position[0]}
|
||||
className="text-black w-16"
|
||||
onChange={(e) => {
|
||||
sceneInfra.setCam({
|
||||
sceneInfra.camControls.setCam({
|
||||
...camSettings,
|
||||
position: [
|
||||
parseFloat(e.target.value),
|
||||
@ -214,7 +212,7 @@ export const CamDebugSettings = () => {
|
||||
value={camSettings.position[1]}
|
||||
className="text-black w-16"
|
||||
onChange={(e) => {
|
||||
sceneInfra.setCam({
|
||||
sceneInfra.camControls.setCam({
|
||||
...camSettings,
|
||||
position: [
|
||||
camSettings.position[0],
|
||||
@ -234,7 +232,7 @@ export const CamDebugSettings = () => {
|
||||
value={camSettings.position[2]}
|
||||
className="text-black w-16"
|
||||
onChange={(e) => {
|
||||
sceneInfra.setCam({
|
||||
sceneInfra.camControls.setCam({
|
||||
...camSettings,
|
||||
position: [
|
||||
camSettings.position[0],
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Quaternion } from 'three'
|
||||
import { isQuaternionVertical } from './sceneInfra'
|
||||
import { isQuaternionVertical } from './helpers'
|
||||
|
||||
describe('isQuaternionVertical', () => {
|
||||
it('should identify vertical quaternions', () => {
|
@ -1,3 +1,4 @@
|
||||
import { compareVec2Epsilon2 } from 'lang/std/sketch'
|
||||
import {
|
||||
GridHelper,
|
||||
LineBasicMaterial,
|
||||
@ -5,6 +6,8 @@ import {
|
||||
PerspectiveCamera,
|
||||
Group,
|
||||
Mesh,
|
||||
Quaternion,
|
||||
Vector3,
|
||||
} from 'three'
|
||||
|
||||
export function createGridHelper({
|
||||
@ -31,3 +34,9 @@ export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) =>
|
||||
|
||||
export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) =>
|
||||
(group.position.distanceTo(cam.position) * cam.fov) / 4000
|
||||
|
||||
export function isQuaternionVertical(q: Quaternion) {
|
||||
const v = new Vector3(0, 0, 1).applyQuaternion(q)
|
||||
// no x or y components means it's vertical
|
||||
return compareVec2Epsilon2([v.x, v.y], [0, 0])
|
||||
}
|
||||
|
@ -24,7 +24,6 @@ import {
|
||||
defaultPlaneColor,
|
||||
getSceneScale,
|
||||
INTERSECTION_PLANE_LAYER,
|
||||
isQuaternionVertical,
|
||||
RAYCASTABLE_PLANE,
|
||||
sceneInfra,
|
||||
SKETCH_GROUP_SEGMENTS,
|
||||
@ -34,6 +33,7 @@ import {
|
||||
Y_AXIS,
|
||||
YZ_PLANE,
|
||||
} from './sceneInfra'
|
||||
import { isQuaternionVertical } from './helpers'
|
||||
import {
|
||||
CallExpression,
|
||||
getTangentialArcToInfo,
|
||||
@ -56,6 +56,7 @@ import { engineCommandManager } from 'lang/std/engineConnection'
|
||||
import {
|
||||
createArcGeometry,
|
||||
dashedStraight,
|
||||
profileStart,
|
||||
straightSegment,
|
||||
tangentialArcToSegment,
|
||||
} from './segments'
|
||||
@ -64,6 +65,7 @@ import {
|
||||
addNewSketchLn,
|
||||
changeSketchArguments,
|
||||
compareVec2Epsilon2,
|
||||
updateStartProfileAtArgs,
|
||||
} from 'lang/std/sketch'
|
||||
import { isReducedMotion, throttle } from 'lib/utils'
|
||||
import {
|
||||
@ -85,6 +87,7 @@ export const TANGENTIAL_ARC_TO_SEGMENT = 'tangential-arc-to-segment'
|
||||
export const TANGENTIAL_ARC_TO_SEGMENT_BODY = 'tangential-arc-to-segment-body'
|
||||
export const TANGENTIAL_ARC_TO__SEGMENT_DASH =
|
||||
'tangential-arc-to-segment-body-dashed'
|
||||
export const PROFILE_START = 'profile-start'
|
||||
|
||||
// This singleton Class is responsible for all of the things the user sees and interacts with.
|
||||
// That mostly mean sketch elements.
|
||||
@ -98,17 +101,18 @@ class SceneEntities {
|
||||
currentSketchQuaternion: Quaternion | null = null
|
||||
constructor() {
|
||||
this.scene = sceneInfra?.scene
|
||||
sceneInfra?.setOnCamChange(this.onCamChange)
|
||||
sceneInfra?.camControls.subscribeToCamChange(this.onCamChange)
|
||||
}
|
||||
|
||||
onCamChange = () => {
|
||||
const orthoFactor = orthoScale(sceneInfra.camera)
|
||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||
|
||||
Object.values(this.activeSegments).forEach((segment) => {
|
||||
const factor =
|
||||
sceneInfra.camera instanceof OrthographicCamera
|
||||
(sceneInfra.camControls.camera instanceof OrthographicCamera
|
||||
? orthoFactor
|
||||
: perspScale(sceneInfra.camera, segment)
|
||||
: perspScale(sceneInfra.camControls.camera, segment)) /
|
||||
sceneInfra._baseUnitMultiplier
|
||||
if (
|
||||
segment.userData.from &&
|
||||
segment.userData.to &&
|
||||
@ -136,21 +140,29 @@ class SceneEntities {
|
||||
scale: factor,
|
||||
})
|
||||
}
|
||||
if (segment.name === PROFILE_START) {
|
||||
segment.scale.set(factor, factor, factor)
|
||||
}
|
||||
})
|
||||
if (this.axisGroup) {
|
||||
const factor =
|
||||
sceneInfra.camera instanceof OrthographicCamera
|
||||
sceneInfra.camControls.camera instanceof OrthographicCamera
|
||||
? orthoFactor
|
||||
: perspScale(sceneInfra.camera, this.axisGroup)
|
||||
: perspScale(sceneInfra.camControls.camera, this.axisGroup)
|
||||
const x = this.axisGroup.getObjectByName(X_AXIS)
|
||||
x?.scale.set(1, factor, 1)
|
||||
x?.scale.set(1, factor / sceneInfra._baseUnitMultiplier, 1)
|
||||
const y = this.axisGroup.getObjectByName(Y_AXIS)
|
||||
y?.scale.set(factor, 1, 1)
|
||||
y?.scale.set(factor / sceneInfra._baseUnitMultiplier, 1, 1)
|
||||
}
|
||||
}
|
||||
|
||||
createIntersectionPlane() {
|
||||
const planeGeometry = new PlaneGeometry(100000, 100000)
|
||||
if (sceneInfra.scene.getObjectByName(RAYCASTABLE_PLANE)) {
|
||||
console.warn('createIntersectionPlane called when it already exists')
|
||||
return
|
||||
}
|
||||
const hundredM = 1000000
|
||||
const planeGeometry = new PlaneGeometry(hundredM, hundredM)
|
||||
const planeMaterial = new MeshBasicMaterial({
|
||||
color: 0xff0000,
|
||||
side: DoubleSide,
|
||||
@ -164,6 +176,7 @@ class SceneEntities {
|
||||
this.scene.add(this.intersectionPlane)
|
||||
}
|
||||
createSketchAxis(sketchPathToNode: PathToNode) {
|
||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||
const baseXColor = 0x000055
|
||||
const baseYColor = 0x550000
|
||||
const xAxisGeometry = new BoxGeometry(100000, 0.3, 0.01)
|
||||
@ -195,13 +208,22 @@ class SceneEntities {
|
||||
|
||||
this.axisGroup = new Group()
|
||||
const gridHelper = createGridHelper({ size: 100, divisions: 10 })
|
||||
gridHelper.position.z = -0.01
|
||||
gridHelper.renderOrder = -3 // is this working?
|
||||
gridHelper.name = 'gridHelper'
|
||||
const sceneScale = getSceneScale(
|
||||
sceneInfra.camera,
|
||||
sceneInfra.controls.target
|
||||
sceneInfra.camControls.camera,
|
||||
sceneInfra.camControls.target
|
||||
)
|
||||
gridHelper.scale.set(sceneScale, sceneScale, sceneScale)
|
||||
|
||||
const factor =
|
||||
sceneInfra.camControls.camera instanceof OrthographicCamera
|
||||
? orthoFactor
|
||||
: perspScale(sceneInfra.camControls.camera, this.axisGroup)
|
||||
xAxisMesh?.scale.set(1, factor / sceneInfra._baseUnitMultiplier, 1)
|
||||
yAxisMesh?.scale.set(factor / sceneInfra._baseUnitMultiplier, 1, 1)
|
||||
|
||||
this.axisGroup.add(xAxisMesh, yAxisMesh, gridHelper)
|
||||
this.currentSketchQuaternion &&
|
||||
this.axisGroup.setRotationFromQuaternion(this.currentSketchQuaternion)
|
||||
@ -240,15 +262,6 @@ class SceneEntities {
|
||||
}) {
|
||||
sceneInfra.resetMouseListeners()
|
||||
this.createIntersectionPlane()
|
||||
const distance = sceneInfra.controls.target.distanceTo(
|
||||
sceneInfra.camera.position
|
||||
)
|
||||
// TODO this should probably be distance to the sketch group, more important after sketch on face
|
||||
// since sketches won't always so close to the origin
|
||||
// is this the best place to adjust camera far?
|
||||
if (sceneInfra.camera.far < distance * 1.5) {
|
||||
sceneInfra.camera.far = distance * 2
|
||||
}
|
||||
|
||||
const { truncatedAst, programMemoryOverride, variableDeclarationName } =
|
||||
this.prepareTruncatedMemoryAndAst(
|
||||
@ -280,11 +293,30 @@ class SceneEntities {
|
||||
sketchGroup.position[1],
|
||||
sketchGroup.position[2]
|
||||
)
|
||||
const orthoFactor = orthoScale(sceneInfra.camera)
|
||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||
const factor =
|
||||
sceneInfra.camera instanceof OrthographicCamera
|
||||
(sceneInfra.camControls.camera instanceof OrthographicCamera
|
||||
? orthoFactor
|
||||
: perspScale(sceneInfra.camera, dummy)
|
||||
: perspScale(sceneInfra.camControls.camera, dummy)) /
|
||||
sceneInfra._baseUnitMultiplier
|
||||
|
||||
let segPathToNode = getNodePathFromSourceRange(
|
||||
draftSegment ? truncatedAst : kclManager.ast,
|
||||
sketchGroup.start.__geoMeta.sourceRange
|
||||
)
|
||||
const _profileStart = profileStart({
|
||||
from: sketchGroup.start.from,
|
||||
id: sketchGroup.start.__geoMeta.id,
|
||||
pathToNode: segPathToNode,
|
||||
scale: factor,
|
||||
})
|
||||
_profileStart.layers.set(SKETCH_LAYER)
|
||||
_profileStart.traverse((child) => {
|
||||
child.layers.set(SKETCH_LAYER)
|
||||
})
|
||||
group.add(_profileStart)
|
||||
this.activeSegments[JSON.stringify(segPathToNode)] = _profileStart
|
||||
|
||||
sketchGroup.value.forEach((segment, index) => {
|
||||
let segPathToNode = getNodePathFromSourceRange(
|
||||
draftSegment ? truncatedAst : kclManager.ast,
|
||||
@ -366,7 +398,11 @@ class SceneEntities {
|
||||
mat.color.set(obj.userData.baseColor)
|
||||
mat.color.offsetHSL(0, 0, 0.5)
|
||||
}
|
||||
const parent = getParentGroup(object)
|
||||
const parent = getParentGroup(object, [
|
||||
STRAIGHT_SEGMENT,
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
PROFILE_START,
|
||||
])
|
||||
if (parent?.userData?.pathToNode) {
|
||||
const updatedAst = parse(recast(kclManager.ast))
|
||||
const node = getNodeFromPath<CallExpression>(
|
||||
@ -407,7 +443,7 @@ class SceneEntities {
|
||||
const isClosingSketch = compareVec2Epsilon2(
|
||||
firstSeg.from,
|
||||
[intersection2d.x, intersection2d.y],
|
||||
1
|
||||
0.5
|
||||
)
|
||||
let modifiedAst
|
||||
if (isClosingSketch) {
|
||||
@ -451,7 +487,7 @@ class SceneEntities {
|
||||
},
|
||||
})
|
||||
}
|
||||
sceneInfra.controls.enableRotate = false
|
||||
sceneInfra.camControls.enableRotate = false
|
||||
}
|
||||
updateAstAndRejigSketch = async (
|
||||
sketchPathToNode: PathToNode,
|
||||
@ -503,7 +539,11 @@ class SceneEntities {
|
||||
variableDeclarationName: string
|
||||
}
|
||||
}) {
|
||||
const group = getParentGroup(object)
|
||||
const group = getParentGroup(object, [
|
||||
STRAIGHT_SEGMENT,
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
PROFILE_START,
|
||||
])
|
||||
if (!group) return
|
||||
const pathToNode: PathToNode = JSON.parse(
|
||||
JSON.stringify(group.userData.pathToNode)
|
||||
@ -527,13 +567,28 @@ class SceneEntities {
|
||||
).node
|
||||
if (node.type !== 'CallExpression') return
|
||||
|
||||
const modded = changeSketchArguments(
|
||||
modifiedAst,
|
||||
kclManager.programMemory,
|
||||
[node.start, node.end],
|
||||
to,
|
||||
from
|
||||
)
|
||||
let modded: {
|
||||
modifiedAst: Program
|
||||
pathToNode: PathToNode
|
||||
}
|
||||
if (group.name === PROFILE_START) {
|
||||
modded = updateStartProfileAtArgs({
|
||||
node: modifiedAst,
|
||||
pathToNode,
|
||||
to,
|
||||
from,
|
||||
previousProgramMemory: kclManager.programMemory,
|
||||
})
|
||||
} else {
|
||||
modded = changeSketchArguments(
|
||||
modifiedAst,
|
||||
kclManager.programMemory,
|
||||
[node.start, node.end],
|
||||
to,
|
||||
from
|
||||
)
|
||||
}
|
||||
|
||||
modifiedAst = modded.modifiedAst
|
||||
const { truncatedAst, programMemoryOverride, variableDeclarationName } =
|
||||
draftInfo
|
||||
@ -552,10 +607,16 @@ class SceneEntities {
|
||||
programMemoryOverride,
|
||||
})
|
||||
this.sceneProgramMemory = programMemory
|
||||
const sketchGroup = programMemory.root[variableDeclarationName]
|
||||
.value as Path[]
|
||||
const orthoFactor = orthoScale(sceneInfra.camera)
|
||||
sketchGroup.forEach((segment, index) => {
|
||||
const sketchGroup = programMemory.root[
|
||||
variableDeclarationName
|
||||
] as SketchGroup
|
||||
const sgPaths = sketchGroup.value
|
||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||
|
||||
const updateSegment = (
|
||||
segment: Path | SketchGroup['start'],
|
||||
index: number
|
||||
) => {
|
||||
const segPathToNode = getNodePathFromSourceRange(
|
||||
modifiedAst,
|
||||
segment.__geoMeta.sourceRange
|
||||
@ -570,12 +631,13 @@ class SceneEntities {
|
||||
// const prevSegment = sketchGroup.slice(index - 1)[0]
|
||||
const type = group?.userData?.type
|
||||
const factor =
|
||||
sceneInfra.camera instanceof OrthographicCamera
|
||||
(sceneInfra.camControls.camera instanceof OrthographicCamera
|
||||
? orthoFactor
|
||||
: perspScale(sceneInfra.camera, group)
|
||||
: perspScale(sceneInfra.camControls.camera, group)) /
|
||||
sceneInfra._baseUnitMultiplier
|
||||
if (type === TANGENTIAL_ARC_TO_SEGMENT) {
|
||||
this.updateTangentialArcToSegment({
|
||||
prevSegment: sketchGroup[index - 1],
|
||||
prevSegment: sgPaths[index - 1],
|
||||
from: segment.from,
|
||||
to: segment.to,
|
||||
group: group,
|
||||
@ -588,8 +650,13 @@ class SceneEntities {
|
||||
group: group,
|
||||
scale: factor,
|
||||
})
|
||||
} else if (type === PROFILE_START) {
|
||||
group.position.set(segment.from[0], segment.from[1], 0)
|
||||
group.scale.set(factor, factor, factor)
|
||||
}
|
||||
})
|
||||
}
|
||||
updateSegment(sketchGroup.start, 0)
|
||||
sgPaths.forEach(updateSegment)
|
||||
})()
|
||||
}
|
||||
|
||||
@ -609,9 +676,7 @@ class SceneEntities {
|
||||
group.userData.from = from
|
||||
group.userData.to = to
|
||||
group.userData.prevSegment = prevSegment
|
||||
const arrowGroup = group.children.find(
|
||||
(child) => child.userData.type === ARROWHEAD
|
||||
) as Group
|
||||
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
||||
|
||||
arrowGroup.position.set(to[0], to[1], 0)
|
||||
|
||||
@ -686,9 +751,7 @@ class SceneEntities {
|
||||
const shape = new Shape()
|
||||
shape.moveTo(0, -0.08 * scale)
|
||||
shape.lineTo(0, 0.08 * scale) // The width of the line
|
||||
const arrowGroup = group.children.find(
|
||||
(child) => child.userData.type === ARROWHEAD
|
||||
) as Group
|
||||
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
||||
|
||||
arrowGroup.position.set(to[0], to[1], 0)
|
||||
|
||||
@ -729,10 +792,10 @@ class SceneEntities {
|
||||
}
|
||||
async animateAfterSketch() {
|
||||
if (isReducedMotion()) {
|
||||
sceneInfra.usePerspectiveCamera()
|
||||
} else {
|
||||
await sceneInfra.animateToPerspective()
|
||||
sceneInfra.camControls.usePerspectiveCamera()
|
||||
return
|
||||
}
|
||||
await sceneInfra.camControls.animateToPerspective()
|
||||
}
|
||||
removeSketchGrid() {
|
||||
if (this.axisGroup) this.scene.remove(this.axisGroup)
|
||||
@ -764,7 +827,7 @@ class SceneEntities {
|
||||
reject()
|
||||
}
|
||||
}
|
||||
sceneInfra.controls.enableRotate = true
|
||||
sceneInfra.camControls.enableRotate = true
|
||||
this.activeSegments = {}
|
||||
// maybe should reset onMove etc handlers
|
||||
if (shouldResolve) resolve(true)
|
||||
@ -797,9 +860,8 @@ class SceneEntities {
|
||||
onClick: (args) => {
|
||||
if (!args || !args.object) return
|
||||
if (args.event.which !== 1) return
|
||||
const { object, intersection } = args
|
||||
const type = object?.userData?.type || ''
|
||||
console.log('intersection.normal?.z', intersection)
|
||||
const { intersection } = args
|
||||
const type = intersection.object.name || ''
|
||||
const posNorm = Number(intersection.normal?.z) > 0
|
||||
let planeString: DefaultPlaneStr = posNorm ? 'XY' : '-XY'
|
||||
let normal: [number, number, number] = posNorm ? [0, 0, 1] : [0, 0, -1]
|
||||
@ -972,9 +1034,9 @@ export function quaternionFromSketchGroup(
|
||||
}
|
||||
|
||||
function colorSegment(object: any, color: number) {
|
||||
const arrowHead = getParentGroup(object, [ARROWHEAD])
|
||||
if (arrowHead) {
|
||||
arrowHead.traverse((child) => {
|
||||
const segmentHead = getParentGroup(object, [ARROWHEAD, PROFILE_START])
|
||||
if (segmentHead) {
|
||||
segmentHead.traverse((child) => {
|
||||
if (child instanceof Mesh) {
|
||||
child.material.color.set(color)
|
||||
}
|
||||
|
@ -1,12 +1,10 @@
|
||||
import {
|
||||
AmbientLight,
|
||||
Color,
|
||||
Euler,
|
||||
GridHelper,
|
||||
LineBasicMaterial,
|
||||
OrthographicCamera,
|
||||
PerspectiveCamera,
|
||||
Quaternion,
|
||||
Scene,
|
||||
Vector3,
|
||||
WebGLRenderer,
|
||||
@ -21,31 +19,24 @@ import {
|
||||
Object3D,
|
||||
Object3DEventMap,
|
||||
} from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
import { EngineCommand, engineCommandManager } from 'lang/std/engineConnection'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
|
||||
import { compareVec2Epsilon2 } from 'lang/std/sketch'
|
||||
import { Coords2d, compareVec2Epsilon2 } from 'lang/std/sketch'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { deg2Rad } from 'lib/utils2d'
|
||||
import * as TWEEN from '@tweenjs/tween.js'
|
||||
import { MouseGuard, cameraMouseDragGuards } from 'lib/cameraControls'
|
||||
import { SourceRange } from 'lang/wasm'
|
||||
import { Axis } from 'lib/selections'
|
||||
import { BaseUnit, SETTINGS_PERSIST_KEY } from 'machines/settingsMachine'
|
||||
import { CameraControls } from './CameraControls'
|
||||
|
||||
type SendType = ReturnType<typeof useModelingContext>['send']
|
||||
|
||||
// 63.5 is definitely a bit of a magic number, play with it until it looked right
|
||||
// if it were 64, that would feel like it's something in the engine where a random
|
||||
// power of 2 is used, but it's the 0.5 seems to make things look much more correct
|
||||
const ZOOM_MAGIC_NUMBER = 63.5
|
||||
const FRAMES_TO_ANIMATE_IN = 30
|
||||
const ORTHOGRAPHIC_CAMERA_SIZE = 20
|
||||
export const ZOOM_MAGIC_NUMBER = 63.5
|
||||
|
||||
export const INTERSECTION_PLANE_LAYER = 1
|
||||
export const SKETCH_LAYER = 2
|
||||
const DEBUG_SHOW_INTERSECTION_PLANE = false
|
||||
export const DEBUG_SHOW_INTERSECTION_PLANE = false
|
||||
export const DEBUG_SHOW_BOTH_SCENES = false
|
||||
|
||||
export const RAYCASTABLE_PLANE = 'raycastable-plane'
|
||||
@ -57,100 +48,6 @@ export const AXIS_GROUP = 'axisGroup'
|
||||
export const SKETCH_GROUP_SEGMENTS = 'sketch-group-segments'
|
||||
export const ARROWHEAD = 'arrowhead'
|
||||
|
||||
const tempQuaternion = new Quaternion() // just used for maths
|
||||
|
||||
interface ThreeCamValues {
|
||||
position: Vector3
|
||||
quaternion: Quaternion
|
||||
zoom: number
|
||||
isPerspective: boolean
|
||||
target: Vector3
|
||||
}
|
||||
|
||||
const lastCmdDelay = 50
|
||||
|
||||
let lastCmd: EngineCommand | null = null
|
||||
let lastCmdTime: number = Date.now()
|
||||
let lastCmdTimeoutId: number | null = null
|
||||
|
||||
const sendLastReliableChannel = () => {
|
||||
if (lastCmd && Date.now() - lastCmdTime >= lastCmdDelay) {
|
||||
engineCommandManager.sendSceneCommand(lastCmd, true)
|
||||
lastCmdTime = Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
const throttledUpdateEngineCamera = throttle((threeValues: ThreeCamValues) => {
|
||||
const cmd: EngineCommand = {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_look_at',
|
||||
...convertThreeCamValuesToEngineCam(threeValues),
|
||||
},
|
||||
}
|
||||
engineCommandManager.sendSceneCommand(cmd)
|
||||
lastCmd = cmd
|
||||
lastCmdTime = Date.now()
|
||||
|
||||
if (lastCmdTimeoutId !== null) {
|
||||
clearTimeout(lastCmdTimeoutId)
|
||||
}
|
||||
lastCmdTimeoutId = setTimeout(
|
||||
sendLastReliableChannel,
|
||||
lastCmdDelay
|
||||
) as any as number
|
||||
}, 1000 / 30)
|
||||
|
||||
let lastPerspectiveCmd: EngineCommand | null = null
|
||||
let lastPerspectiveCmdTime: number = Date.now()
|
||||
let lastPerspectiveCmdTimeoutId: number | null = null
|
||||
|
||||
const sendLastPerspectiveReliableChannel = () => {
|
||||
if (
|
||||
lastPerspectiveCmd &&
|
||||
Date.now() - lastPerspectiveCmdTime >= lastCmdDelay
|
||||
) {
|
||||
engineCommandManager.sendSceneCommand(lastPerspectiveCmd, true)
|
||||
lastPerspectiveCmdTime = Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
const throttledUpdateEngineFov = throttle(
|
||||
(vals: {
|
||||
position: Vector3
|
||||
quaternion: Quaternion
|
||||
zoom: number
|
||||
fov: number
|
||||
target: Vector3
|
||||
}) => {
|
||||
const cmd: EngineCommand = {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_perspective_settings',
|
||||
...convertThreeCamValuesToEngineCam({
|
||||
...vals,
|
||||
isPerspective: true,
|
||||
}),
|
||||
fov_y: vals.fov,
|
||||
...calculateNearFarFromFOV(vals.fov),
|
||||
},
|
||||
}
|
||||
engineCommandManager.sendSceneCommand(cmd)
|
||||
lastPerspectiveCmd = cmd
|
||||
lastPerspectiveCmdTime = Date.now()
|
||||
if (lastPerspectiveCmdTimeoutId !== null) {
|
||||
clearTimeout(lastPerspectiveCmdTimeoutId)
|
||||
}
|
||||
lastPerspectiveCmdTimeoutId = setTimeout(
|
||||
sendLastPerspectiveReliableChannel,
|
||||
lastCmdDelay
|
||||
) as any as number
|
||||
},
|
||||
1000 / 15
|
||||
)
|
||||
|
||||
interface BaseCallbackArgs2 {
|
||||
object: any
|
||||
event: any
|
||||
@ -178,34 +75,20 @@ interface onMoveCallbackArgs {
|
||||
intersection: Intersection<Object3D<Object3DEventMap>>
|
||||
}
|
||||
|
||||
export type ReactCameraProperties =
|
||||
| {
|
||||
type: 'perspective'
|
||||
fov?: number
|
||||
position: [number, number, number]
|
||||
quaternion: [number, number, number, number]
|
||||
}
|
||||
| {
|
||||
type: 'orthographic'
|
||||
zoom?: number
|
||||
position: [number, number, number]
|
||||
quaternion: [number, number, number, number]
|
||||
}
|
||||
|
||||
// This singleton class is responsible for all of the under the hood setup for the client side scene.
|
||||
// That is the cameras and switching between them, raycasters for click mouse events and their abstractions (onClick etc), setting up controls.
|
||||
// Anything that added the the scene for the user to interact with is probably in SceneEntities.ts
|
||||
class SceneInfra {
|
||||
static instance: SceneInfra
|
||||
scene: Scene
|
||||
camera: PerspectiveCamera | OrthographicCamera
|
||||
renderer: WebGLRenderer
|
||||
controls: OrbitControls
|
||||
camControls: CameraControls
|
||||
isPerspective = true
|
||||
fov = 45
|
||||
fovBeforeAnimate = 45
|
||||
isFovAnimationInProgress = false
|
||||
interactionGuards: MouseGuard = cameraMouseDragGuards.KittyCAD
|
||||
_baseUnit: BaseUnit = 'mm'
|
||||
_baseUnitMultiplier = 1
|
||||
onDragCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
||||
onMoveCallback: (arg: onMoveCallbackArgs) => void = () => {}
|
||||
onClickCallback: (arg?: OnClickCallbackArgs) => void = () => {}
|
||||
@ -225,6 +108,15 @@ class SceneInfra {
|
||||
this.onMouseLeave = callbacks.onMouseLeave || this.onMouseLeave
|
||||
this.selected = null // following selections between callbacks being set is too tricky
|
||||
}
|
||||
set baseUnit(unit: BaseUnit) {
|
||||
this._baseUnit = unit
|
||||
this._baseUnitMultiplier = baseUnitTomm(unit)
|
||||
this.scene.scale.set(
|
||||
this._baseUnitMultiplier,
|
||||
this._baseUnitMultiplier,
|
||||
this._baseUnitMultiplier
|
||||
)
|
||||
}
|
||||
resetMouseListeners = () => {
|
||||
sceneInfra.setCallbacks({
|
||||
onDrag: () => {},
|
||||
@ -256,55 +148,18 @@ class SceneInfra {
|
||||
selectedObject: null | any = null
|
||||
mouseDownVector: null | Vector2 = null
|
||||
|
||||
// reacts hooks into some of this singleton's properties
|
||||
reactCameraProperties: ReactCameraProperties = {
|
||||
type: 'perspective',
|
||||
fov: 12,
|
||||
position: [0, 0, 0],
|
||||
quaternion: [0, 0, 0, 1],
|
||||
}
|
||||
reactCameraPropertiesCallback: (a: ReactCameraProperties) => void = () => {}
|
||||
setReactCameraPropertiesCallback = (
|
||||
cb: (a: ReactCameraProperties) => void
|
||||
) => {
|
||||
this.reactCameraPropertiesCallback = cb
|
||||
}
|
||||
setCam = (camProps: ReactCameraProperties) => {
|
||||
if (
|
||||
camProps.type === 'perspective' &&
|
||||
this.camera instanceof OrthographicCamera
|
||||
) {
|
||||
this.usePerspectiveCamera()
|
||||
} else if (
|
||||
camProps.type === 'orthographic' &&
|
||||
this.camera instanceof PerspectiveCamera
|
||||
) {
|
||||
this.useOrthographicCamera()
|
||||
}
|
||||
this.camera.position.set(...camProps.position)
|
||||
this.camera.quaternion.set(...camProps.quaternion)
|
||||
if (
|
||||
camProps.type === 'perspective' &&
|
||||
this.camera instanceof PerspectiveCamera
|
||||
) {
|
||||
// not sure what to do here, calling dollyZoom here is buggy because it updates the position
|
||||
// at the same time
|
||||
} else if (
|
||||
camProps.type === 'orthographic' &&
|
||||
this.camera instanceof OrthographicCamera
|
||||
) {
|
||||
this.camera.zoom = camProps.zoom || 1
|
||||
}
|
||||
this.camera.updateProjectionMatrix()
|
||||
this.controls.update()
|
||||
}
|
||||
|
||||
constructor() {
|
||||
// SCENE
|
||||
this.scene = new Scene()
|
||||
this.scene.background = new Color(0x000000)
|
||||
this.scene.background = null
|
||||
|
||||
// RENDERER
|
||||
this.renderer = new WebGLRenderer({ antialias: true, alpha: true }) // Enable transparency
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
this.renderer.setClearColor(0x000000, 0) // Set clear color to black with 0 alpha (fully transparent)
|
||||
window.addEventListener('resize', this.onWindowResize)
|
||||
|
||||
// CAMERA
|
||||
const camHeightDistanceRatio = 0.5
|
||||
const baseUnit: BaseUnit =
|
||||
@ -315,25 +170,19 @@ class SceneInfra {
|
||||
const ang = Math.atan(camHeightDistanceRatio)
|
||||
const x = Math.cos(ang) * length
|
||||
const y = Math.sin(ang) * length
|
||||
this.camera = this.createPerspectiveCamera()
|
||||
this.camera.position.set(0, -x, y)
|
||||
if (DEBUG_SHOW_INTERSECTION_PLANE)
|
||||
this.camera.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||
|
||||
// RENDERER
|
||||
this.renderer = new WebGLRenderer({ antialias: true, alpha: true }) // Enable transparency
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
this.renderer.setClearColor(0x000000, 0) // Set clear color to black with 0 alpha (fully transparent)
|
||||
window.addEventListener('resize', this.onWindowResize)
|
||||
this.camControls = new CameraControls(false, this.renderer.domElement)
|
||||
this.camControls.subscribeToCamChange(() => this.onCameraChange())
|
||||
this.camControls.camera.layers.enable(SKETCH_LAYER)
|
||||
this.camControls.camera.position.set(0, -x, y)
|
||||
if (DEBUG_SHOW_INTERSECTION_PLANE)
|
||||
this.camControls.camera.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||
|
||||
// RAYCASTERS
|
||||
this.raycaster.layers.enable(SKETCH_LAYER)
|
||||
this.raycaster.layers.disable(0)
|
||||
this.planeRaycaster.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||
|
||||
// CONTROLS
|
||||
this.controls = this.setupOrbitControls()
|
||||
|
||||
// GRID
|
||||
const size = 100
|
||||
const divisions = 10
|
||||
@ -353,415 +202,45 @@ class SceneInfra {
|
||||
|
||||
SceneInfra.instance = this
|
||||
}
|
||||
private _isCamMovingCallback: (isMoving: boolean, isTween: boolean) => void =
|
||||
() => {}
|
||||
setIsCamMovingCallback(cb: (isMoving: boolean, isTween: boolean) => void) {
|
||||
this._isCamMovingCallback = cb
|
||||
}
|
||||
private _onCamChange: () => void = () => {}
|
||||
setOnCamChange(cb: () => void) {
|
||||
this._onCamChange = cb
|
||||
}
|
||||
setInteractionGuards = (guard: MouseGuard) => {
|
||||
this.interactionGuards = guard
|
||||
// setMouseGuards is oun patch-package patch to orbit controls
|
||||
// see patches/three+0.160.0.patch
|
||||
;(this.controls as any).setMouseGuards(guard)
|
||||
}
|
||||
private createPerspectiveCamera = () => {
|
||||
const { z_near, z_far } = calculateNearFarFromFOV(this.fov)
|
||||
this.camera = new PerspectiveCamera(
|
||||
this.fov,
|
||||
window.innerWidth / window.innerHeight,
|
||||
z_near,
|
||||
z_far
|
||||
)
|
||||
this.camera.up.set(0, 0, 1)
|
||||
this.camera.layers.enable(SKETCH_LAYER)
|
||||
if (DEBUG_SHOW_INTERSECTION_PLANE)
|
||||
this.camera.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||
|
||||
return this.camera
|
||||
}
|
||||
setupOrbitControls = (target?: [number, number, number]): OrbitControls => {
|
||||
if (this.controls) this.controls.dispose()
|
||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
|
||||
if (target) {
|
||||
// if we're swapping from perspective to orthographic,
|
||||
// we'll need to recreate the orbit controls
|
||||
// and most likely want the target to be the same
|
||||
this.controls.target.set(...target)
|
||||
}
|
||||
this.controls.update()
|
||||
this.controls.addEventListener('change', this.onCameraChange)
|
||||
// debounce is needed because the start and end events are fired too often for zoom on scroll
|
||||
let debounceTimer = 0
|
||||
const handleStart = () => {
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
this._isCamMovingCallback(true, false)
|
||||
}
|
||||
const handleEnd = () => {
|
||||
debounceTimer = setTimeout(() => {
|
||||
this._isCamMovingCallback(false, false)
|
||||
}, 400) as any as number
|
||||
}
|
||||
this.controls.addEventListener('start', handleStart)
|
||||
this.controls.addEventListener('end', handleEnd)
|
||||
|
||||
// setMouseGuards is oun patch-package patch to orbit controls
|
||||
// see patches/three+0.160.0.patch
|
||||
;(this.controls as any).setMouseGuards(this.interactionGuards)
|
||||
return this.controls
|
||||
}
|
||||
onStreamStart = () => this.onCameraChange()
|
||||
|
||||
deferReactUpdate = throttle((a: ReactCameraProperties) => {
|
||||
this.reactCameraPropertiesCallback(a)
|
||||
}, 200)
|
||||
|
||||
onCameraChange = () => {
|
||||
const scale = getSceneScale(this.camera, this.controls.target)
|
||||
const scale = getSceneScale(
|
||||
this.camControls.camera,
|
||||
this.camControls.target
|
||||
)
|
||||
const planesGroup = this.scene.getObjectByName(DEFAULT_PLANES)
|
||||
const axisGroup = this.scene
|
||||
.getObjectByName(AXIS_GROUP)
|
||||
?.getObjectByName('gridHelper')
|
||||
planesGroup && planesGroup.scale.set(scale, scale, scale)
|
||||
planesGroup &&
|
||||
planesGroup.scale.set(
|
||||
scale / sceneInfra._baseUnitMultiplier,
|
||||
scale / sceneInfra._baseUnitMultiplier,
|
||||
scale / sceneInfra._baseUnitMultiplier
|
||||
)
|
||||
axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale)
|
||||
|
||||
throttledUpdateEngineCamera({
|
||||
quaternion: this.camera.quaternion,
|
||||
position: this.camera.position,
|
||||
zoom: this.camera.zoom,
|
||||
isPerspective: this.isPerspective,
|
||||
target: this.controls.target,
|
||||
})
|
||||
this.deferReactUpdate({
|
||||
type:
|
||||
this.camera instanceof PerspectiveCamera
|
||||
? 'perspective'
|
||||
: 'orthographic',
|
||||
[this.camera instanceof PerspectiveCamera ? 'fov' : 'zoom']:
|
||||
this.camera instanceof PerspectiveCamera
|
||||
? this.camera.fov
|
||||
: this.camera.zoom,
|
||||
position: [
|
||||
roundOff(this.camera.position.x, 2),
|
||||
roundOff(this.camera.position.y, 2),
|
||||
roundOff(this.camera.position.z, 2),
|
||||
],
|
||||
quaternion: [
|
||||
roundOff(this.camera.quaternion.x, 2),
|
||||
roundOff(this.camera.quaternion.y, 2),
|
||||
roundOff(this.camera.quaternion.z, 2),
|
||||
roundOff(this.camera.quaternion.w, 2),
|
||||
],
|
||||
})
|
||||
this._onCamChange()
|
||||
}
|
||||
|
||||
onWindowResize = () => {
|
||||
if (this.camera instanceof PerspectiveCamera) {
|
||||
this.camera.aspect = window.innerWidth / window.innerHeight
|
||||
} else if (this.camera instanceof OrthographicCamera) {
|
||||
const aspect = window.innerWidth / window.innerHeight
|
||||
this.camera.left = -ORTHOGRAPHIC_CAMERA_SIZE * aspect
|
||||
this.camera.right = ORTHOGRAPHIC_CAMERA_SIZE * aspect
|
||||
this.camera.top = ORTHOGRAPHIC_CAMERA_SIZE
|
||||
this.camera.bottom = -ORTHOGRAPHIC_CAMERA_SIZE
|
||||
}
|
||||
this.camera.updateProjectionMatrix()
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
}
|
||||
|
||||
animate = () => {
|
||||
requestAnimationFrame(this.animate)
|
||||
TWEEN.update() // This will update all tweens during the animation loop
|
||||
if (!this.isFovAnimationInProgress)
|
||||
this.renderer.render(this.scene, this.camera)
|
||||
}
|
||||
async tweenCameraToQuaternion(
|
||||
targetQuaternion: Quaternion,
|
||||
duration = 500,
|
||||
toOrthographic = true
|
||||
): Promise<void> {
|
||||
const isVertical = isQuaternionVertical(targetQuaternion)
|
||||
let _duration = duration
|
||||
if (isVertical) {
|
||||
_duration = duration * 0.6
|
||||
await this._tweenCameraToQuaternion(new Quaternion(), _duration, false)
|
||||
if (!this.isFovAnimationInProgress) {
|
||||
// console.log('animation frame', this.cameraControls.camera)
|
||||
this.camControls.update()
|
||||
this.renderer.render(this.scene, this.camControls.camera)
|
||||
}
|
||||
await this._tweenCameraToQuaternion(
|
||||
targetQuaternion,
|
||||
_duration,
|
||||
toOrthographic
|
||||
)
|
||||
}
|
||||
_tweenCameraToQuaternion(
|
||||
targetQuaternion: Quaternion,
|
||||
duration = 500,
|
||||
toOrthographic = false
|
||||
): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const camera = this.camera
|
||||
this._isCamMovingCallback(true, true)
|
||||
const initialQuaternion = camera.quaternion.clone()
|
||||
const isVertical = isQuaternionVertical(targetQuaternion)
|
||||
let tweenEnd = isVertical ? 0.99 : 1
|
||||
const controlsTarget = this.controls.target.clone()
|
||||
const initialDistance = controlsTarget.distanceTo(camera.position.clone())
|
||||
|
||||
const cameraAtTime = (animationProgress: number /* 0 - 1 */) => {
|
||||
const currentQ = tempQuaternion.slerpQuaternions(
|
||||
initialQuaternion,
|
||||
targetQuaternion,
|
||||
animationProgress
|
||||
)
|
||||
if (this.camera instanceof PerspectiveCamera)
|
||||
// changing the camera position back when it's orthographic doesn't do anything
|
||||
// and it messes up animating back to perspective later
|
||||
this.camera.position
|
||||
.set(0, 0, 1)
|
||||
.applyQuaternion(currentQ)
|
||||
.multiplyScalar(initialDistance)
|
||||
.add(controlsTarget)
|
||||
|
||||
this.camera.up.set(0, 1, 0).applyQuaternion(currentQ).normalize()
|
||||
this.camera.quaternion.copy(currentQ)
|
||||
this.controls.target.copy(controlsTarget)
|
||||
this.controls.update()
|
||||
this.camera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
const onComplete = async () => {
|
||||
if (isReducedMotion() && toOrthographic) {
|
||||
cameraAtTime(0.99)
|
||||
this.useOrthographicCamera()
|
||||
} else if (toOrthographic) {
|
||||
await this.animateToOrthographic()
|
||||
}
|
||||
if (isVertical) cameraAtTime(1)
|
||||
this.camera.up.set(0, 0, 1)
|
||||
this.controls.enableRotate = false
|
||||
this._isCamMovingCallback(false, true)
|
||||
resolve()
|
||||
}
|
||||
|
||||
if (isReducedMotion()) {
|
||||
onComplete()
|
||||
return
|
||||
}
|
||||
|
||||
new TWEEN.Tween({ t: 0 })
|
||||
.to({ t: tweenEnd }, duration)
|
||||
.easing(TWEEN.Easing.Quadratic.InOut)
|
||||
.onUpdate(({ t }) => cameraAtTime(t))
|
||||
.onComplete(onComplete)
|
||||
.start()
|
||||
})
|
||||
}
|
||||
|
||||
animateToOrthographic = () =>
|
||||
new Promise((resolve) => {
|
||||
this.isFovAnimationInProgress = true
|
||||
let currentFov = this.fov
|
||||
this.fovBeforeAnimate = this.fov
|
||||
|
||||
const targetFov = 4
|
||||
const fovAnimationStep = (currentFov - targetFov) / FRAMES_TO_ANIMATE_IN
|
||||
let frameWaitOnFinish = 10
|
||||
|
||||
const animateFovChange = () => {
|
||||
if (this.camera instanceof PerspectiveCamera) {
|
||||
if (this.camera.fov > targetFov) {
|
||||
// Decrease the FOV
|
||||
currentFov = Math.max(currentFov - fovAnimationStep, targetFov)
|
||||
this.camera.updateProjectionMatrix()
|
||||
this.dollyZoom(currentFov)
|
||||
requestAnimationFrame(animateFovChange) // Continue the animation
|
||||
} else if (frameWaitOnFinish > 0) {
|
||||
frameWaitOnFinish--
|
||||
requestAnimationFrame(animateFovChange) // Continue the animation
|
||||
} else {
|
||||
// Once the target FOV is reached, switch to the orthographic camera
|
||||
// Needs to wait a couple frames after the FOV animation is complete
|
||||
this.useOrthographicCamera()
|
||||
this.isFovAnimationInProgress = false
|
||||
resolve(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
animateFovChange() // Start the animation
|
||||
})
|
||||
|
||||
animateToPerspective = () =>
|
||||
new Promise((resolve) => {
|
||||
this.isFovAnimationInProgress = true
|
||||
// Immediately set the camera to perspective with a very low FOV
|
||||
this.fov = 4
|
||||
let currentFov = 4
|
||||
this.camera.updateProjectionMatrix()
|
||||
const targetFov = this.fovBeforeAnimate // Target FOV for perspective
|
||||
const fovAnimationStep = (targetFov - currentFov) / FRAMES_TO_ANIMATE_IN
|
||||
this.usePerspectiveCamera()
|
||||
|
||||
const animateFovChange = () => {
|
||||
if (this.camera instanceof OrthographicCamera) return
|
||||
if (this.camera.fov < targetFov) {
|
||||
// Increase the FOV
|
||||
currentFov = Math.min(currentFov + fovAnimationStep, targetFov)
|
||||
// this.camera.fov = currentFov
|
||||
this.camera.updateProjectionMatrix()
|
||||
this.dollyZoom(currentFov)
|
||||
requestAnimationFrame(animateFovChange) // Continue the animation
|
||||
} else {
|
||||
// Set the flag to false as the FOV animation is complete
|
||||
this.isFovAnimationInProgress = false
|
||||
resolve(true)
|
||||
}
|
||||
}
|
||||
animateFovChange() // Start the animation
|
||||
})
|
||||
dispose = () => {
|
||||
// Dispose of scene resources, renderer, and controls
|
||||
this.renderer.dispose()
|
||||
window.removeEventListener('resize', this.onWindowResize)
|
||||
// Dispose of any other resources like geometries, materials, textures
|
||||
}
|
||||
|
||||
useOrthographicCamera = () => {
|
||||
this.isPerspective = false
|
||||
const { x: px, y: py, z: pz } = this.camera.position
|
||||
const { x: qx, y: qy, z: qz, w: qw } = this.camera.quaternion
|
||||
const { x: tx, y: ty, z: tz } = this.controls.target
|
||||
const aspect = window.innerWidth / window.innerHeight
|
||||
const { z_near, z_far } = calculateNearFarFromFOV(this.fov)
|
||||
this.camera = new OrthographicCamera(
|
||||
-ORTHOGRAPHIC_CAMERA_SIZE * aspect,
|
||||
ORTHOGRAPHIC_CAMERA_SIZE * aspect,
|
||||
ORTHOGRAPHIC_CAMERA_SIZE,
|
||||
-ORTHOGRAPHIC_CAMERA_SIZE,
|
||||
z_near,
|
||||
z_far
|
||||
)
|
||||
this.camera.up.set(0, 0, 1)
|
||||
this.camera.layers.enable(SKETCH_LAYER)
|
||||
if (DEBUG_SHOW_INTERSECTION_PLANE)
|
||||
this.camera.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||
this.camera.position.set(px, py, pz)
|
||||
const distance = this.camera.position.distanceTo(new Vector3(tx, ty, tz))
|
||||
const fovFactor = 45 / this.fov
|
||||
this.camera.zoom = (ZOOM_MAGIC_NUMBER * fovFactor * 0.8) / distance
|
||||
|
||||
this.setupOrbitControls([tx, ty, tz])
|
||||
this.camera.quaternion.set(qx, qy, qz, qw)
|
||||
this.camera.updateProjectionMatrix()
|
||||
this.controls.update()
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_set_orthographic',
|
||||
},
|
||||
})
|
||||
}
|
||||
usePerspectiveCamera = () => {
|
||||
this.isPerspective = true
|
||||
const { x: px, y: py, z: pz } = this.camera.position
|
||||
const { x: qx, y: qy, z: qz, w: qw } = this.camera.quaternion
|
||||
const { x: tx, y: ty, z: tz } = this.controls.target
|
||||
const zoom = this.camera.zoom
|
||||
this.camera = this.createPerspectiveCamera()
|
||||
|
||||
this.camera.position.set(px, py, pz)
|
||||
this.camera.quaternion.set(qx, qy, qz, qw)
|
||||
const zoomFudgeFactor = 2280
|
||||
const distance = zoomFudgeFactor / (zoom * this.fov)
|
||||
const direction = new Vector3().subVectors(
|
||||
this.camera.position,
|
||||
this.controls.target
|
||||
)
|
||||
direction.normalize()
|
||||
this.camera.position
|
||||
.copy(this.controls.target)
|
||||
.addScaledVector(direction, distance)
|
||||
|
||||
this.setupOrbitControls([tx, ty, tz])
|
||||
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_set_perspective',
|
||||
parameters: {
|
||||
fov_y: this.camera.fov,
|
||||
...calculateNearFarFromFOV(this.fov),
|
||||
},
|
||||
},
|
||||
})
|
||||
this.onCameraChange()
|
||||
return this.camera
|
||||
}
|
||||
|
||||
dollyZoom = (newFov: number) => {
|
||||
if (!(this.camera instanceof PerspectiveCamera)) {
|
||||
console.warn('Dolly zoom is only applicable to perspective cameras.')
|
||||
return
|
||||
}
|
||||
this.fov = newFov
|
||||
|
||||
// Calculate the direction vector from the camera towards the controls target
|
||||
const direction = new Vector3()
|
||||
.subVectors(this.controls.target, this.camera.position)
|
||||
.normalize()
|
||||
|
||||
// Calculate the distance to the controls target before changing the FOV
|
||||
const distanceBefore = this.camera.position.distanceTo(this.controls.target)
|
||||
|
||||
// Calculate the scale factor for the new FOV compared to the old one
|
||||
// This needs to be calculated before updating the camera's FOV
|
||||
const oldFov = this.camera.fov
|
||||
|
||||
const viewHeightFactor = (fov: number) => {
|
||||
/* *
|
||||
/|
|
||||
/ |
|
||||
/ |
|
||||
/ |
|
||||
/ | viewHeight/2
|
||||
/ |
|
||||
/ |
|
||||
/↙️fov/2 |
|
||||
/________|
|
||||
\ |
|
||||
\._._._.|
|
||||
*/
|
||||
return Math.tan(deg2Rad(fov / 2))
|
||||
}
|
||||
const scaleFactor = viewHeightFactor(oldFov) / viewHeightFactor(newFov)
|
||||
|
||||
this.camera.fov = newFov
|
||||
this.camera.updateProjectionMatrix()
|
||||
|
||||
const distanceAfter = distanceBefore * scaleFactor
|
||||
|
||||
const newPosition = this.controls.target
|
||||
.clone()
|
||||
.add(direction.multiplyScalar(-distanceAfter))
|
||||
this.camera.position.copy(newPosition)
|
||||
|
||||
const { z_near, z_far } = calculateNearFarFromFOV(this.fov)
|
||||
this.camera.near = z_near
|
||||
this.camera.far = z_far
|
||||
|
||||
throttledUpdateEngineFov({
|
||||
fov: newFov,
|
||||
position: newPosition,
|
||||
quaternion: this.camera.quaternion,
|
||||
zoom: this.camera.zoom,
|
||||
target: this.controls.target,
|
||||
})
|
||||
}
|
||||
getPlaneIntersectPoint = (): {
|
||||
intersection2d?: Vector2
|
||||
intersectPoint: Vector3
|
||||
@ -769,7 +248,7 @@ class SceneInfra {
|
||||
} | null => {
|
||||
this.planeRaycaster.setFromCamera(
|
||||
this.currentMouseVector,
|
||||
sceneInfra.camera
|
||||
sceneInfra.camControls.camera
|
||||
)
|
||||
const planeIntersects = this.planeRaycaster.intersectObjects(
|
||||
this.scene.children,
|
||||
@ -806,8 +285,11 @@ class SceneInfra {
|
||||
}
|
||||
|
||||
return {
|
||||
intersection2d: new Vector2(transformedPoint.x, transformedPoint.y), // z should be 0
|
||||
intersectPoint,
|
||||
intersection2d: new Vector2(
|
||||
transformedPoint.x / this._baseUnitMultiplier,
|
||||
transformedPoint.y / this._baseUnitMultiplier
|
||||
), // z should be 0
|
||||
intersectPoint: intersectPoint.divideScalar(this._baseUnitMultiplier),
|
||||
intersection: planeIntersects[0],
|
||||
}
|
||||
}
|
||||
@ -907,7 +389,7 @@ class SceneInfra {
|
||||
}
|
||||
|
||||
// Check the center point
|
||||
this.raycaster.setFromCamera(mouseDownVector, this.camera)
|
||||
this.raycaster.setFromCamera(mouseDownVector, this.camControls.camera)
|
||||
updateClosestIntersection(
|
||||
this.raycaster.intersectObjects(this.scene.children, true)
|
||||
)
|
||||
@ -922,7 +404,7 @@ class SceneInfra {
|
||||
mouseDownVector.x + offsetX,
|
||||
mouseDownVector.y - offsetY
|
||||
)
|
||||
this.raycaster.setFromCamera(ringVector, this.camera)
|
||||
this.raycaster.setFromCamera(ringVector, this.camControls.camera)
|
||||
updateClosestIntersection(
|
||||
this.raycaster.intersectObjects(this.scene.children, true)
|
||||
)
|
||||
@ -1015,8 +497,15 @@ class SceneInfra {
|
||||
}
|
||||
})
|
||||
planesGroup.layers.enable(SKETCH_LAYER)
|
||||
const sceneScale = getSceneScale(this.camera, this.controls.target)
|
||||
planesGroup.scale.set(sceneScale, sceneScale, sceneScale)
|
||||
const sceneScale = getSceneScale(
|
||||
this.camControls.camera,
|
||||
this.camControls.target
|
||||
)
|
||||
planesGroup.scale.set(
|
||||
sceneScale / sceneInfra._baseUnitMultiplier,
|
||||
sceneScale / sceneInfra._baseUnitMultiplier,
|
||||
sceneScale / sceneInfra._baseUnitMultiplier
|
||||
)
|
||||
this.scene.add(planesGroup)
|
||||
}
|
||||
removeDefaultPlanes() {
|
||||
@ -1050,52 +539,6 @@ class SceneInfra {
|
||||
|
||||
export const sceneInfra = new SceneInfra()
|
||||
|
||||
function convertThreeCamValuesToEngineCam({
|
||||
target,
|
||||
position,
|
||||
quaternion,
|
||||
zoom,
|
||||
isPerspective,
|
||||
}: ThreeCamValues): {
|
||||
center: Vector3
|
||||
up: Vector3
|
||||
vantage: Vector3
|
||||
} {
|
||||
// Something to consider is that the orbit controls have a target,
|
||||
// we're kind of deriving the target/lookAtVector here when it might not be needed
|
||||
// leaving for now since it's working but maybe revisit later
|
||||
const euler = new Euler().setFromQuaternion(quaternion, 'XYZ')
|
||||
|
||||
const lookAtVector = new Vector3(0, 0, -1)
|
||||
.applyEuler(euler)
|
||||
.normalize()
|
||||
.add(position)
|
||||
|
||||
const upVector = new Vector3(0, 1, 0).applyEuler(euler).normalize()
|
||||
if (isPerspective) {
|
||||
return {
|
||||
center: target,
|
||||
up: upVector,
|
||||
vantage: position,
|
||||
}
|
||||
}
|
||||
const zoomFactor = -ZOOM_MAGIC_NUMBER / zoom
|
||||
const direction = lookAtVector.clone().sub(position).normalize()
|
||||
const newVantage = position.clone().add(direction.multiplyScalar(zoomFactor))
|
||||
return {
|
||||
center: lookAtVector,
|
||||
up: upVector,
|
||||
vantage: newVantage,
|
||||
}
|
||||
}
|
||||
|
||||
function calculateNearFarFromFOV(fov: number) {
|
||||
const nearFarRatio = (fov - 3) / (45 - 3)
|
||||
// const z_near = 0.1 + nearFarRatio * (5 - 0.1)
|
||||
const z_far = 1000 + nearFarRatio * (100000 - 1000)
|
||||
return { z_near: 0.1, z_far }
|
||||
}
|
||||
|
||||
export function getSceneScale(
|
||||
camera: PerspectiveCamera | OrthographicCamera,
|
||||
target: Vector3
|
||||
@ -1131,12 +574,6 @@ function baseUnitTomm(baseUnit: BaseUnit) {
|
||||
}
|
||||
}
|
||||
|
||||
export function isQuaternionVertical(q: Quaternion) {
|
||||
const v = new Vector3(0, 0, 1).applyQuaternion(q)
|
||||
// no x or y components means it's vertical
|
||||
return compareVec2Epsilon2([v.x, v.y], [0, 0])
|
||||
}
|
||||
|
||||
export type DefaultPlane =
|
||||
| 'xy-default-plane'
|
||||
| 'xz-default-plane'
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Coords2d } from 'lang/std/sketch'
|
||||
import {
|
||||
BoxGeometry,
|
||||
BufferGeometry,
|
||||
CatmullRomCurve3,
|
||||
ConeGeometry,
|
||||
@ -19,6 +20,7 @@ import {
|
||||
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
||||
import { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm'
|
||||
import {
|
||||
PROFILE_START,
|
||||
STRAIGHT_SEGMENT,
|
||||
STRAIGHT_SEGMENT_BODY,
|
||||
STRAIGHT_SEGMENT_DASH,
|
||||
@ -29,6 +31,38 @@ import {
|
||||
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
|
||||
import { ARROWHEAD } from './sceneInfra'
|
||||
|
||||
export function profileStart({
|
||||
from,
|
||||
id,
|
||||
pathToNode,
|
||||
scale = 1,
|
||||
}: {
|
||||
from: Coords2d
|
||||
id: string
|
||||
pathToNode: PathToNode
|
||||
scale?: number
|
||||
}) {
|
||||
const group = new Group()
|
||||
|
||||
const geometry = new BoxGeometry(0.8, 0.8, 0.8)
|
||||
const body = new MeshBasicMaterial({ color: 0xffffff })
|
||||
const mesh = new Mesh(geometry, body)
|
||||
|
||||
group.add(mesh)
|
||||
|
||||
group.userData = {
|
||||
type: PROFILE_START,
|
||||
id,
|
||||
from,
|
||||
pathToNode,
|
||||
isSelected: false,
|
||||
}
|
||||
group.name = PROFILE_START
|
||||
group.position.set(from[0], from[1], 0)
|
||||
group.scale.set(scale, scale, scale)
|
||||
return group
|
||||
}
|
||||
|
||||
export function straightSegment({
|
||||
from,
|
||||
to,
|
||||
|
@ -87,7 +87,7 @@ export function useCalc({
|
||||
inputRef: React.RefObject<HTMLInputElement>
|
||||
valueNode: Value | null
|
||||
calcResult: string
|
||||
prevVariables: PrevVariable<any>[]
|
||||
prevVariables: PrevVariable<unknown>[]
|
||||
newVariableName: string
|
||||
isNewVariableNameUnique: boolean
|
||||
newVariableInsertIndex: number
|
||||
|
@ -4,7 +4,7 @@ import { engineCommandManager } from 'lang/std/engineConnection'
|
||||
import { throttle, isReducedMotion } from 'lib/utils'
|
||||
|
||||
const updateDollyZoom = throttle(
|
||||
(newFov: number) => sceneInfra.dollyZoom(newFov),
|
||||
(newFov: number) => sceneInfra.camControls.dollyZoom(newFov),
|
||||
1000 / 15
|
||||
)
|
||||
|
||||
@ -15,19 +15,19 @@ export const CamToggle = () => {
|
||||
|
||||
useEffect(() => {
|
||||
engineCommandManager.waitForReady.then(async () => {
|
||||
sceneInfra.dollyZoom(fov)
|
||||
sceneInfra.camControls.dollyZoom(fov)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const toggleCamera = () => {
|
||||
if (isPerspective) {
|
||||
isReducedMotion()
|
||||
? sceneInfra.useOrthographicCamera()
|
||||
: sceneInfra.animateToOrthographic()
|
||||
? sceneInfra.camControls.useOrthographicCamera()
|
||||
: sceneInfra.camControls.animateToOrthographic()
|
||||
} else {
|
||||
isReducedMotion()
|
||||
? sceneInfra.usePerspectiveCamera()
|
||||
: sceneInfra.animateToPerspective()
|
||||
? sceneInfra.camControls.usePerspectiveCamera()
|
||||
: sceneInfra.camControls.animateToPerspective()
|
||||
}
|
||||
setIsPerspective(!isPerspective)
|
||||
}
|
||||
@ -60,9 +60,9 @@ export const CamToggle = () => {
|
||||
<button
|
||||
onClick={() => {
|
||||
if (enableRotate) {
|
||||
sceneInfra.controls.enableRotate = false
|
||||
sceneInfra.camControls.enableRotate = false
|
||||
} else {
|
||||
sceneInfra.controls.enableRotate = true
|
||||
sceneInfra.camControls.enableRotate = true
|
||||
}
|
||||
setEnableRotate(!enableRotate)
|
||||
}}
|
||||
|
@ -57,12 +57,11 @@ export const CommandBarProvider = ({
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<CommandBar />
|
||||
</CommandsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const CommandBar = () => {
|
||||
export const CommandBar = () => {
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
const {
|
||||
context: { selectedCommand, currentArgument, commands },
|
||||
@ -84,17 +83,25 @@ const CommandBar = () => {
|
||||
if (commandBarState.matches('Review')) {
|
||||
const entries = Object.entries(selectedCommand?.args || {})
|
||||
|
||||
commandBarSend({
|
||||
type: commandBarState.matches('Review')
|
||||
? 'Edit argument'
|
||||
: 'Change current argument',
|
||||
data: {
|
||||
arg: {
|
||||
name: entries[entries.length - 1][0],
|
||||
...entries[entries.length - 1][1],
|
||||
const currentArgName = entries[entries.length - 1][0]
|
||||
const currentArg = {
|
||||
name: currentArgName,
|
||||
...entries[entries.length - 1][1],
|
||||
}
|
||||
|
||||
if (commandBarState.matches('Review')) {
|
||||
commandBarSend({
|
||||
type: 'Edit argument',
|
||||
data: {
|
||||
arg: currentArg,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
} else {
|
||||
commandBarSend({
|
||||
type: 'Remove argument',
|
||||
data: { [currentArgName]: currentArg },
|
||||
})
|
||||
}
|
||||
} else {
|
||||
commandBarSend({ type: 'Deselect command' })
|
||||
}
|
||||
@ -117,6 +124,11 @@ const CommandBar = () => {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() => console.log(commandBarState.context.argumentsToSubmit),
|
||||
[commandBarState.context.argumentsToSubmit]
|
||||
)
|
||||
|
||||
return (
|
||||
<Transition.Root
|
||||
show={!commandBarState.matches('Closed') || false}
|
||||
|
@ -4,6 +4,7 @@ import CommandBarSelectionInput from './CommandBarSelectionInput'
|
||||
import { CommandArgument } from 'lib/commandTypes'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import CommandBarHeader from './CommandBarHeader'
|
||||
import CommandBarKclInput from './CommandBarKclInput'
|
||||
|
||||
function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
@ -17,10 +18,7 @@ function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
|
||||
commandBarSend({
|
||||
type: 'Submit argument',
|
||||
data: {
|
||||
[currentArgument.name]:
|
||||
currentArgument.inputType === 'number'
|
||||
? parseFloat((data as string) || '0')
|
||||
: data,
|
||||
[currentArgument.name]: data,
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -68,6 +66,10 @@ function ArgumentInput({
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
)
|
||||
case 'kcl':
|
||||
return (
|
||||
<CommandBarKclInput arg={arg} stepBack={stepBack} onSubmit={onSubmit} />
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<CommandBarBasicInput
|
||||
|
@ -9,7 +9,7 @@ function CommandBarBasicInput({
|
||||
onSubmit,
|
||||
}: {
|
||||
arg: CommandArgument<unknown> & {
|
||||
inputType: 'number' | 'string'
|
||||
inputType: 'string'
|
||||
name: string
|
||||
}
|
||||
stepBack: () => void
|
||||
@ -18,7 +18,6 @@ function CommandBarBasicInput({
|
||||
const { commandBarSend, commandBarState } = useCommandsContext()
|
||||
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const inputType = arg.inputType === 'number' ? 'number' : 'text'
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
@ -40,9 +39,9 @@ function CommandBarBasicInput({
|
||||
</span>
|
||||
<input
|
||||
id="arg-form"
|
||||
name={inputType}
|
||||
name={arg.inputType}
|
||||
ref={inputRef}
|
||||
type={inputType}
|
||||
type={arg.inputType}
|
||||
required
|
||||
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
|
||||
placeholder="Enter a value"
|
||||
|
@ -4,6 +4,9 @@ import React, { ReactNode, useState } from 'react'
|
||||
import { ActionButton } from '../ActionButton'
|
||||
import { Selections, getSelectionTypeDisplayText } from 'lib/selections'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { KclCommandValue, KclExpressionWithVariable } from 'lib/commandTypes'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { roundOff } from 'lib/utils'
|
||||
|
||||
function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
@ -45,6 +48,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
||||
parseInt(b.keys[0], 10) - 1
|
||||
]
|
||||
const arg = selectedCommand?.args[argName]
|
||||
if (!argName || !arg) return
|
||||
commandBarSend({
|
||||
type: 'Change current argument',
|
||||
data: { arg: { ...arg, name: argName } },
|
||||
@ -59,7 +63,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
||||
selectedCommand &&
|
||||
argumentsToSubmit && (
|
||||
<>
|
||||
<div className="px-4 text-sm flex gap-4 items-start">
|
||||
<div className="group px-4 text-sm flex gap-4 items-start">
|
||||
<div className="flex flex-1 flex-wrap gap-2">
|
||||
<p
|
||||
data-command-name={selectedCommand?.name}
|
||||
@ -91,25 +95,50 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
||||
: 'bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80'
|
||||
}`}
|
||||
>
|
||||
<span className="capitalize">{argName}</span>
|
||||
{argumentsToSubmit[argName] ? (
|
||||
arg.inputType === 'selection' ? (
|
||||
getSelectionTypeDisplayText(
|
||||
argumentsToSubmit[argName] as Selections
|
||||
)
|
||||
) : arg.inputType === 'kcl' ? (
|
||||
roundOff(
|
||||
Number(
|
||||
(argumentsToSubmit[argName] as KclCommandValue)
|
||||
.valueCalculated
|
||||
),
|
||||
4
|
||||
)
|
||||
) : typeof argumentsToSubmit[argName] === 'object' ? (
|
||||
JSON.stringify(argumentsToSubmit[argName])
|
||||
) : (
|
||||
<em>{argumentsToSubmit[argName] as ReactNode}</em>
|
||||
)
|
||||
) : (
|
||||
<em>{argName}</em>
|
||||
)}
|
||||
) : null}
|
||||
{showShortcuts && (
|
||||
<small className="absolute -top-[1px] right-full translate-x-1/2 px-0.5 rounded-sm bg-chalkboard-80 text-chalkboard-10 dark:bg-energy-10 dark:text-chalkboard-100">
|
||||
<span className="sr-only">Hotkey: </span>
|
||||
{i + 1}
|
||||
</small>
|
||||
)}
|
||||
{arg.inputType === 'kcl' &&
|
||||
!!argumentsToSubmit[argName] &&
|
||||
'variableName' in
|
||||
(argumentsToSubmit[argName] as KclCommandValue) && (
|
||||
<>
|
||||
<CustomIcon name="make-variable" className="w-4 h-4" />
|
||||
<Tooltip position="blockEnd">
|
||||
New variable:{' '}
|
||||
{
|
||||
(
|
||||
argumentsToSubmit[
|
||||
argName
|
||||
] as KclExpressionWithVariable
|
||||
).variableName
|
||||
}
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
|
17
src/components/CommandBar/CommandBarKclInput.module.css
Normal file
@ -0,0 +1,17 @@
|
||||
.editor {
|
||||
@apply text-base flex-1;
|
||||
}
|
||||
|
||||
.editor :global(.cm-editor) {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
|
||||
.editor :global(.cm-line)::selection {
|
||||
@apply px-1;
|
||||
@apply text-chalkboard-100;
|
||||
@apply bg-energy-10/50;
|
||||
}
|
||||
:global(.dark) .editor :global(.cm-line)::selection {
|
||||
@apply text-energy-10;
|
||||
@apply bg-energy-10/20;
|
||||
}
|
221
src/components/CommandBar/CommandBarKclInput.tsx
Normal file
@ -0,0 +1,221 @@
|
||||
import { Completion } from '@codemirror/autocomplete'
|
||||
import { EditorState, EditorView, useCodeMirror } from '@uiw/react-codemirror'
|
||||
import { CustomIcon } from 'components/CustomIcon'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { CommandArgument, KclCommandValue } from 'lib/commandTypes'
|
||||
import { getSystemTheme } from 'lib/theme'
|
||||
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
|
||||
import { roundOff } from 'lib/utils'
|
||||
import { varMentions } from 'lib/varCompletionExtension'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import styles from './CommandBarKclInput.module.css'
|
||||
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
|
||||
|
||||
function CommandBarKclInput({
|
||||
arg,
|
||||
stepBack,
|
||||
onSubmit,
|
||||
}: {
|
||||
arg: CommandArgument<unknown> & {
|
||||
inputType: 'kcl'
|
||||
name: string
|
||||
}
|
||||
stepBack: () => void
|
||||
onSubmit: (event: unknown) => void
|
||||
}) {
|
||||
const { commandBarSend, commandBarState } = useCommandsContext()
|
||||
const previouslySetValue = commandBarState.context.argumentsToSubmit[
|
||||
arg.name
|
||||
] as KclCommandValue | undefined
|
||||
const { settings } = useGlobalStateContext()
|
||||
const defaultValue = (arg.defaultValue as string) || ''
|
||||
const [value, setValue] = useState(
|
||||
previouslySetValue?.valueText || defaultValue || ''
|
||||
)
|
||||
const [createNewVariable, setCreateNewVariable] = useState(
|
||||
previouslySetValue && 'variableName' in previouslySetValue
|
||||
)
|
||||
const [canSubmit, setCanSubmit] = useState(true)
|
||||
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
||||
const editorRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const {
|
||||
prevVariables,
|
||||
calcResult,
|
||||
newVariableInsertIndex,
|
||||
valueNode,
|
||||
newVariableName,
|
||||
setNewVariableName,
|
||||
isNewVariableNameUnique,
|
||||
} = useCalculateKclExpression({
|
||||
value,
|
||||
initialVariableName:
|
||||
previouslySetValue && 'variableName' in previouslySetValue
|
||||
? previouslySetValue.variableName
|
||||
: arg.name,
|
||||
})
|
||||
const varMentionData: Completion[] = prevVariables.map((v) => ({
|
||||
label: v.key,
|
||||
detail: String(roundOff(v.value as number)),
|
||||
}))
|
||||
|
||||
const { setContainer } = useCodeMirror({
|
||||
container: editorRef.current,
|
||||
value,
|
||||
indentWithTab: false,
|
||||
basicSetup: false,
|
||||
autoFocus: true,
|
||||
selection: {
|
||||
anchor: 0,
|
||||
head:
|
||||
previouslySetValue && 'valueText' in previouslySetValue
|
||||
? previouslySetValue.valueText.length
|
||||
: defaultValue.length,
|
||||
},
|
||||
accessKey: 'command-bar',
|
||||
theme:
|
||||
settings.context.theme === 'system'
|
||||
? getSystemTheme()
|
||||
: settings.context.theme,
|
||||
extensions: [
|
||||
EditorView.domEventHandlers({
|
||||
keydown: (event) => {
|
||||
if (event.key === 'Backspace' && value === '') {
|
||||
event.preventDefault()
|
||||
stepBack()
|
||||
}
|
||||
},
|
||||
}),
|
||||
varMentions(varMentionData),
|
||||
EditorState.transactionFilter.of((tr) => {
|
||||
if (tr.newDoc.lines > 1) {
|
||||
handleSubmit()
|
||||
return []
|
||||
}
|
||||
return tr
|
||||
}),
|
||||
],
|
||||
onChange: (newValue) => setValue(newValue),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (editorRef.current) {
|
||||
setContainer(editorRef.current)
|
||||
}
|
||||
}, [arg, editorRef])
|
||||
|
||||
useEffect(() => {
|
||||
setCanSubmit(
|
||||
calcResult !== 'NAN' && (!createNewVariable || isNewVariableNameUnique)
|
||||
)
|
||||
}, [calcResult, createNewVariable, isNewVariableNameUnique])
|
||||
|
||||
function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
|
||||
e?.preventDefault()
|
||||
if (!canSubmit || valueNode === null) return
|
||||
|
||||
onSubmit(
|
||||
createNewVariable
|
||||
? ({
|
||||
valueAst: valueNode,
|
||||
valueText: value,
|
||||
valueCalculated: calcResult,
|
||||
variableName: newVariableName,
|
||||
insertIndex: newVariableInsertIndex,
|
||||
variableIdentifierAst: createIdentifier(newVariableName),
|
||||
variableDeclarationAst: createVariableDeclaration(
|
||||
newVariableName,
|
||||
valueNode
|
||||
),
|
||||
} satisfies KclCommandValue)
|
||||
: ({
|
||||
valueAst: valueNode,
|
||||
valueText: value,
|
||||
valueCalculated: calcResult,
|
||||
} satisfies KclCommandValue)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<form id="arg-form" onSubmit={handleSubmit} data-can-submit={canSubmit}>
|
||||
<label className="flex gap-4 items-center mx-4 my-4 border-solid border-b border-chalkboard-50">
|
||||
<span className="capitalize text-chalkboard-80 dark:text-chalkboard-20">
|
||||
{arg.name}
|
||||
</span>
|
||||
<div ref={editorRef} className={styles.editor} />
|
||||
<CustomIcon
|
||||
name="equal"
|
||||
className="w-5 h-5 text-chalkboard-70 dark:text-chalkboard-40"
|
||||
/>
|
||||
<span
|
||||
className={
|
||||
calcResult === 'NAN'
|
||||
? 'text-destroy-80 dark:text-destroy-40'
|
||||
: 'text-energy-60 dark:text-energy-20'
|
||||
}
|
||||
>
|
||||
{calcResult === 'NAN'
|
||||
? "Can't calculate"
|
||||
: roundOff(Number(calcResult), 4)}
|
||||
</span>
|
||||
</label>
|
||||
{createNewVariable ? (
|
||||
<div className="flex items-baseline gap-4 mx-4 border-solid border-0 border-b border-chalkboard-50">
|
||||
<label
|
||||
htmlFor="variable-name"
|
||||
className="text-base text-chalkboard-80 dark:text-chalkboard-20"
|
||||
>
|
||||
Variable name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="variable-name"
|
||||
name="variable-name"
|
||||
className="flex-1 border-none bg-transparent"
|
||||
placeholder="Variable name"
|
||||
value={newVariableName}
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
autoFocus
|
||||
onChange={(e) => setNewVariableName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.currentTarget.value === '' && e.key === 'Backspace') {
|
||||
setCreateNewVariable(false)
|
||||
}
|
||||
}}
|
||||
onKeyUp={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSubmit()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
className={
|
||||
isNewVariableNameUnique
|
||||
? 'text-energy-60 dark:text-energy-20'
|
||||
: 'text-destroy-60 dark:text-destroy-40'
|
||||
}
|
||||
>
|
||||
{isNewVariableNameUnique ? 'Available' : 'Unavailable'}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex justify-between gap-2 px-4">
|
||||
<button
|
||||
onClick={() => setCreateNewVariable(true)}
|
||||
className="text-blue border-none bg-transparent font-sm flex gap-1 items-center pl-0 pr-1"
|
||||
>
|
||||
<CustomIcon name="plus" className="w-5 h-5" />
|
||||
Create new variable
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default CommandBarKclInput
|
@ -14,7 +14,18 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
||||
})
|
||||
|
||||
useHotkeys(
|
||||
'1, 2, 3, 4, 5, 6, 7, 8, 9, 0',
|
||||
[
|
||||
'alt+1',
|
||||
'alt+2',
|
||||
'alt+3',
|
||||
'alt+4',
|
||||
'alt+5',
|
||||
'alt+6',
|
||||
'alt+7',
|
||||
'alt+8',
|
||||
'alt+9',
|
||||
'alt+0',
|
||||
],
|
||||
(_, b) => {
|
||||
if (b.keys && !Number.isNaN(parseInt(b.keys[0], 10))) {
|
||||
if (!selectedCommand?.args) return
|
||||
|
@ -18,10 +18,12 @@ export type CustomIconName =
|
||||
| 'horizontal'
|
||||
| 'horizontalDash'
|
||||
| 'line'
|
||||
| 'make-variable'
|
||||
| 'move'
|
||||
| 'network'
|
||||
| 'networkCrossedOut'
|
||||
| 'parallel'
|
||||
| 'plus'
|
||||
| 'search'
|
||||
| 'settings'
|
||||
| 'sketch'
|
||||
@ -336,6 +338,22 @@ export const CustomIcon = ({
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'make-variable':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4.07178 6.57735L9.99998 3.1547L15.9282 6.57735V13.4227L9.99998 16.8453L4.07178 13.4227V6.57735ZM9.99998 2L16.9282 6V14L9.99998 18L3.07178 14V6L9.99998 2ZM9.45068 6.854C9.20802 6.798 8.97468 6.78867 8.75068 6.826C8.39602 6.90067 8.06468 7.04533 7.75668 7.26C7.73802 7.26933 7.72402 7.27867 7.71468 7.288C7.45335 7.484 7.24802 7.694 7.09868 7.918C6.96802 8.09533 6.86068 8.282 6.77668 8.478C6.69268 8.65533 6.63668 8.814 6.60868 8.954C6.60868 9.00067 6.62268 9.038 6.65068 9.066L6.69268 9.108H6.95868C7.13602 9.108 7.23402 9.09867 7.25268 9.08C7.28068 9.052 7.30868 8.982 7.33668 8.87C7.45802 8.52467 7.65402 8.212 7.92468 7.932C8.13002 7.72667 8.36802 7.58667 8.63868 7.512C8.83468 7.456 9.02602 7.456 9.21268 7.512C9.40868 7.57733 9.53002 7.68467 9.57668 7.834C9.62335 7.96467 9.61402 8.198 9.54868 8.534L8.77868 11.614C8.65735 11.9593 8.47535 12.216 8.23268 12.384C8.10202 12.4587 7.97602 12.4913 7.85468 12.482C7.68668 12.482 7.53735 12.4307 7.40668 12.328L7.36468 12.286L7.42068 12.272C7.50468 12.244 7.57002 12.216 7.61668 12.188C7.93402 12.02 8.10668 11.7493 8.13468 11.376C8.15335 11.1053 8.05535 10.9187 7.84068 10.816C7.60735 10.6853 7.34135 10.69 7.04268 10.83C6.73468 10.9793 6.54802 11.2547 6.48268 11.656C6.45468 11.8893 6.47335 12.1087 6.53868 12.314C6.56668 12.4073 6.60868 12.4913 6.66468 12.566C6.92602 12.986 7.32268 13.182 7.85468 13.154C8.31202 13.126 8.72268 12.8787 9.08668 12.412L9.12868 12.37L9.21268 12.496C9.44602 12.8133 9.80068 13.0233 10.2767 13.126C10.5474 13.1633 10.79 13.1633 11.0047 13.126C11.6954 12.9767 12.2507 12.58 12.6707 11.936C12.6894 11.9173 12.7034 11.894 12.7127 11.866C12.9553 11.474 13.0767 11.18 13.0767 10.984C13.0767 10.9373 13.0674 10.9047 13.0487 10.886C13.0207 10.8673 12.918 10.858 12.7407 10.858C12.61 10.858 12.526 10.8627 12.4887 10.872C12.442 10.8813 12.4047 10.9327 12.3767 11.026C12.2834 11.3807 12.092 11.7073 11.8027 12.006C11.56 12.23 11.3174 12.3793 11.0747 12.454C11.0094 12.4727 10.9067 12.482 10.7667 12.482C10.6174 12.482 10.5194 12.4727 10.4727 12.454C10.314 12.398 10.1974 12.3 10.1227 12.16C10.0667 12.0573 10.062 11.8613 10.1087 11.572C10.1087 11.5347 10.132 11.4367 10.1787 11.278C10.58 9.542 10.8274 8.55733 10.9207 8.324C11.0887 7.88533 11.3127 7.61467 11.5927 7.512C11.6114 7.50267 11.63 7.498 11.6487 7.498C11.8914 7.43267 12.0967 7.47467 12.2647 7.624L12.3207 7.68L12.2087 7.722C11.8354 7.85267 11.6207 8.128 11.5647 8.548C11.5367 8.76267 11.5927 8.94 11.7327 9.08C11.77 9.11733 11.8167 9.15 11.8727 9.178C12.1714 9.32733 12.4887 9.28067 12.8247 9.038C12.9367 8.954 13.03 8.83267 13.1047 8.674C13.282 8.26333 13.2774 7.87133 13.0907 7.498C12.9787 7.26467 12.7874 7.078 12.5167 6.938C12.162 6.77933 11.8074 6.76533 11.4527 6.896C11.1447 7.01733 10.8787 7.20867 10.6547 7.47L10.5707 7.582C10.552 7.582 10.524 7.554 10.4867 7.498C10.2627 7.17133 9.91735 6.95667 9.45068 6.854Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'move':
|
||||
return (
|
||||
<svg
|
||||
@ -400,6 +418,22 @@ export const CustomIcon = ({
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'plus':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M9.5 9.5V5.5H10.5V9.5H14.5V10.5H10.5V14.5H9.5V10.5H5.5V9.5H9.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'search':
|
||||
return (
|
||||
<svg
|
||||
|
@ -13,7 +13,7 @@ type OutputTypeKey = OutputFormat['type']
|
||||
type ExtractStorageTypes<T> = T extends { storage: infer U } ? U : never
|
||||
type StorageUnion = ExtractStorageTypes<OutputFormat>
|
||||
|
||||
interface ExportButtonProps extends React.PropsWithChildren {
|
||||
export interface ExportButtonProps extends React.PropsWithChildren {
|
||||
className?: {
|
||||
button?: string
|
||||
icon?: string
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { settingsCommandBarConfig } from 'lib/commandBarConfigs/settingsCommandConfig'
|
||||
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
|
||||
import { sceneInfra } from 'clientSideScene/sceneInfra'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -97,6 +98,7 @@ export const GlobalStateProvider = ({
|
||||
if (settingsState.context.theme !== 'system') return
|
||||
setThemeClass(e.matches ? Themes.Dark : Themes.Light)
|
||||
}
|
||||
sceneInfra.baseUnit = settingsState?.context?.baseUnit || 'mm'
|
||||
|
||||
matcher.addEventListener('change', listener)
|
||||
return () => matcher.removeEventListener('change', listener)
|
||||
|
@ -25,8 +25,7 @@ describe('processMemory', () => {
|
||||
|> lineTo([-3.35, 0.17], %)
|
||||
|> lineTo([0.98, 5.16], %)
|
||||
|> lineTo([2.15, 4.32], %)
|
||||
// |> rx(90, %)
|
||||
show(theExtrude, theSketch)`
|
||||
// |> rx(90, %)`
|
||||
const ast = parse(code)
|
||||
const programMemory = await enginelessExecutor(ast, {
|
||||
root: {},
|
||||
|
@ -213,7 +213,7 @@ export const ModelingMachineProvider = ({
|
||||
)
|
||||
await kclManager.updateAst(modifiedAst, false)
|
||||
const quaternion = getSketchQuaternion(pathToNode, normal)
|
||||
await sceneInfra.tweenCameraToQuaternion(quaternion)
|
||||
await sceneInfra.camControls.tweenCameraToQuaternion(quaternion)
|
||||
return {
|
||||
sketchPathToNode: pathToNode,
|
||||
sketchNormalBackUp: normal,
|
||||
@ -227,7 +227,7 @@ export const ModelingMachineProvider = ({
|
||||
sketchPathToNode || [],
|
||||
sketchNormalBackUp
|
||||
)
|
||||
await sceneInfra.tweenCameraToQuaternion(quaternion)
|
||||
await sceneInfra.camControls.tweenCameraToQuaternion(quaternion)
|
||||
},
|
||||
'Get horizontal info': async ({
|
||||
selectionRanges,
|
||||
@ -374,6 +374,7 @@ export const ModelingMachineProvider = ({
|
||||
send: modelingSend,
|
||||
actor: modelingActor,
|
||||
commandBarConfig: modelingMachineConfig,
|
||||
allCommandsRequireNetwork: true,
|
||||
onCancel: () => modelingSend({ type: 'Cancel' }),
|
||||
})
|
||||
|
||||
|
@ -80,7 +80,7 @@ const overallConnectionStateIcon: Record<
|
||||
[NetworkHealthState.Disconnected]: 'networkCrossedOut',
|
||||
}
|
||||
|
||||
export const NetworkHealthIndicator = () => {
|
||||
export function useNetworkStatus() {
|
||||
const [steps, setSteps] = useState(initialConnectingTypeGroupState)
|
||||
const [internetConnected, setInternetConnected] = useState<boolean>(true)
|
||||
const [overallState, setOverallState] = useState<NetworkHealthState>(
|
||||
@ -118,18 +118,18 @@ export const NetworkHealthIndicator = () => {
|
||||
}, [hasIssues, internetConnected])
|
||||
|
||||
useEffect(() => {
|
||||
const cb1 = () => {
|
||||
const onlineCallback = () => {
|
||||
setSteps(initialConnectingTypeGroupState)
|
||||
setInternetConnected(true)
|
||||
}
|
||||
const cb2 = () => {
|
||||
const offlineCallback = () => {
|
||||
setInternetConnected(false)
|
||||
}
|
||||
window.addEventListener('online', cb1)
|
||||
window.addEventListener('offline', cb2)
|
||||
window.addEventListener('online', onlineCallback)
|
||||
window.addEventListener('offline', offlineCallback)
|
||||
return () => {
|
||||
window.removeEventListener('online', cb1)
|
||||
window.removeEventListener('offline', cb2)
|
||||
window.removeEventListener('online', onlineCallback)
|
||||
window.removeEventListener('offline', offlineCallback)
|
||||
}
|
||||
}, [])
|
||||
|
||||
@ -183,6 +183,30 @@ export const NetworkHealthIndicator = () => {
|
||||
)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
hasIssues,
|
||||
overallState,
|
||||
internetConnected,
|
||||
steps,
|
||||
issues,
|
||||
error,
|
||||
setHasCopied,
|
||||
hasCopied,
|
||||
}
|
||||
}
|
||||
|
||||
export const NetworkHealthIndicator = () => {
|
||||
const {
|
||||
hasIssues,
|
||||
overallState,
|
||||
internetConnected,
|
||||
steps,
|
||||
issues,
|
||||
error,
|
||||
setHasCopied,
|
||||
hasCopied,
|
||||
} = useNetworkStatus()
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
<Popover.Button
|
||||
|
@ -3,8 +3,9 @@ import { BrowserRouter } from 'react-router-dom'
|
||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||
import { type ProjectWithEntryPointMetadata } from 'lib/types'
|
||||
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||
import CommandBarProvider from './CommandBar/CommandBar'
|
||||
import { APP_NAME } from 'lib/constants'
|
||||
import { vi } from 'vitest'
|
||||
import { ExportButtonProps } from './ExportButton'
|
||||
|
||||
const now = new Date()
|
||||
const projectWellFormed = {
|
||||
@ -37,15 +38,22 @@ const projectWellFormed = {
|
||||
},
|
||||
} satisfies ProjectWithEntryPointMetadata
|
||||
|
||||
const mockExportButton = vi.fn()
|
||||
vi.mock('/src/components/ExportButton', () => ({
|
||||
// engineCommandManager method call in ExportButton causes vitest to hang
|
||||
ExportButton: (props: ExportButtonProps) => {
|
||||
mockExportButton(props)
|
||||
return <button>Fake export button</button>
|
||||
},
|
||||
}))
|
||||
|
||||
describe('ProjectSidebarMenu tests', () => {
|
||||
test('Renders the project name', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<CommandBarProvider>
|
||||
<GlobalStateProvider>
|
||||
<ProjectSidebarMenu project={projectWellFormed} />
|
||||
</GlobalStateProvider>
|
||||
</CommandBarProvider>
|
||||
<GlobalStateProvider>
|
||||
<ProjectSidebarMenu project={projectWellFormed} />
|
||||
</GlobalStateProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
|
||||
@ -62,11 +70,9 @@ describe('ProjectSidebarMenu tests', () => {
|
||||
test('Renders app name if given no project', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<CommandBarProvider>
|
||||
<GlobalStateProvider>
|
||||
<ProjectSidebarMenu />
|
||||
</GlobalStateProvider>
|
||||
</CommandBarProvider>
|
||||
<GlobalStateProvider>
|
||||
<ProjectSidebarMenu />
|
||||
</GlobalStateProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
|
||||
@ -78,14 +84,9 @@ describe('ProjectSidebarMenu tests', () => {
|
||||
test('Renders as a link if set to do so', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<CommandBarProvider>
|
||||
<GlobalStateProvider>
|
||||
<ProjectSidebarMenu
|
||||
project={projectWellFormed}
|
||||
renderAsLink={true}
|
||||
/>
|
||||
</GlobalStateProvider>
|
||||
</CommandBarProvider>
|
||||
<GlobalStateProvider>
|
||||
<ProjectSidebarMenu project={projectWellFormed} renderAsLink={true} />
|
||||
</GlobalStateProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
|
||||
|
@ -5,10 +5,10 @@ import { Value } from '../lang/wasm'
|
||||
import {
|
||||
AvailableVars,
|
||||
addToInputHelper,
|
||||
useCalc,
|
||||
CalcResult,
|
||||
CreateNewVariable,
|
||||
} from './AvailableVarsHelpers'
|
||||
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
|
||||
|
||||
type ModalResolve = {
|
||||
value: string
|
||||
@ -55,7 +55,7 @@ export const SetAngleLengthModal = ({
|
||||
setNewVariableName,
|
||||
inputRef,
|
||||
newVariableInsertIndex,
|
||||
} = useCalc({
|
||||
} = useCalculateKclExpression({
|
||||
value,
|
||||
initialVariableName: valueName,
|
||||
})
|
||||
|
@ -5,10 +5,10 @@ import { Value } from '../lang/wasm'
|
||||
import {
|
||||
AvailableVars,
|
||||
addToInputHelper,
|
||||
useCalc,
|
||||
CalcResult,
|
||||
CreateNewVariable,
|
||||
} from './AvailableVarsHelpers'
|
||||
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
|
||||
|
||||
type ModalResolve = {
|
||||
value: string
|
||||
@ -59,7 +59,7 @@ export const GetInfoModal = ({
|
||||
newVariableName,
|
||||
isNewVariableNameUnique,
|
||||
newVariableInsertIndex,
|
||||
} = useCalc({ value: value, initialVariableName })
|
||||
} = useCalculateKclExpression({ value: value, initialVariableName })
|
||||
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Dialog, Transition } from '@headlessui/react'
|
||||
import { Fragment } from 'react'
|
||||
import { useCalc, CreateNewVariable } from './AvailableVarsHelpers'
|
||||
import { CreateNewVariable } from './AvailableVarsHelpers'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { type InstanceProps, create } from 'react-modal-promise'
|
||||
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
|
||||
|
||||
type ModalResolve = { variableName: string }
|
||||
type ModalReject = boolean
|
||||
@ -25,7 +26,7 @@ export const SetVarNameModal = ({
|
||||
valueName,
|
||||
}: SetVarNameModalProps) => {
|
||||
const { isNewVariableNameUnique, newVariableName, setNewVariableName } =
|
||||
useCalc({ value: '', initialVariableName: valueName })
|
||||
useCalculateKclExpression({ value: '', initialVariableName: valueName })
|
||||
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
|
@ -1,21 +1,15 @@
|
||||
import {
|
||||
MouseEventHandler,
|
||||
WheelEventHandler,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { MouseEventHandler, useEffect, useRef, useState } from 'react'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useStore } from '../useStore'
|
||||
import { getNormalisedCoordinates, throttle } from '../lib/utils'
|
||||
import { getNormalisedCoordinates } from '../lib/utils'
|
||||
import Loading from './Loading'
|
||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { useKclContext } from 'lang/KclSingleton'
|
||||
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
|
||||
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
|
||||
|
||||
export const Stream = ({ className = '' }: { className?: string }) => {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
@ -35,9 +29,10 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
||||
streamDimensions: s.streamDimensions,
|
||||
}))
|
||||
const { settings } = useGlobalStateContext()
|
||||
const cameraControls = settings?.context?.cameraControls
|
||||
const { state } = useModelingContext()
|
||||
const { isExecuting } = useKclContext()
|
||||
const { overallState } = useNetworkStatus()
|
||||
const isNetworkOkay = overallState === NetworkHealthState.Ok
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@ -65,19 +60,6 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
||||
setClickCoords({ x, y })
|
||||
}
|
||||
|
||||
const fps = 60
|
||||
const handleScroll: WheelEventHandler<HTMLVideoElement> = throttle((e) => {
|
||||
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'default_camera_zoom',
|
||||
magnitude: e.deltaY * 0.4,
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
})
|
||||
}, Math.round(1000 / fps))
|
||||
|
||||
const handleMouseUp: MouseEventHandler<HTMLDivElement> = ({
|
||||
clientX,
|
||||
clientY,
|
||||
@ -156,7 +138,6 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
||||
muted
|
||||
autoPlay
|
||||
controls={false}
|
||||
onWheel={handleScroll}
|
||||
onPlay={() => setIsLoading(false)}
|
||||
onMouseMoveCapture={handleMouseMove}
|
||||
className={`w-full cursor-pointer h-full ${isExecuting && 'blur-md'}`}
|
||||
@ -164,6 +145,13 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
||||
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
|
||||
/>
|
||||
<ClientSideScene cameraControls={settings.context.cameraControls} />
|
||||
{!isNetworkOkay && !isLoading && (
|
||||
<div className="text-center absolute inset-0">
|
||||
<Loading>
|
||||
<span data-testid="loading-stream">Stream disconnected</span>
|
||||
</Loading>
|
||||
</div>
|
||||
)}
|
||||
{isLoading && (
|
||||
<div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<Loading>
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { undo, redo } from '@codemirror/commands'
|
||||
import ReactCodeMirror, {
|
||||
Extension,
|
||||
ViewUpdate,
|
||||
@ -11,7 +12,7 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
||||
import { Themes } from 'lib/theme'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { linter, lintGutter } from '@codemirror/lint'
|
||||
import { useStore } from 'useStore'
|
||||
import { processCodeMirrorRanges } from 'lib/selections'
|
||||
@ -25,11 +26,14 @@ import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import interact from '@replit/codemirror-interact'
|
||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||
import { kclManager, useKclContext } from 'lang/KclSingleton'
|
||||
import { useFileContext } from 'hooks/useFileContext'
|
||||
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
||||
import { sceneInfra } from 'clientSideScene/sceneInfra'
|
||||
import { copilotPlugin } from 'editor/plugins/lsp/copilot'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import type * as LSP from 'vscode-languageserver-protocol'
|
||||
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
|
||||
export const editorShortcutMeta = {
|
||||
formatCode: {
|
||||
@ -75,6 +79,28 @@ export const TextEditor = ({
|
||||
}))
|
||||
const { code, errors } = useKclContext()
|
||||
const lastEvent = useRef({ event: '', time: Date.now() })
|
||||
const { overallState } = useNetworkStatus()
|
||||
const isNetworkOkay = overallState === NetworkHealthState.Ok
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return
|
||||
const onlineCallback = () => kclManager.setCodeAndExecute(kclManager.code)
|
||||
window.addEventListener('online', onlineCallback)
|
||||
return () => window.removeEventListener('online', onlineCallback)
|
||||
}, [])
|
||||
|
||||
useHotkeys('mod+z', (e) => {
|
||||
e.preventDefault()
|
||||
if (editorView) {
|
||||
undo(editorView)
|
||||
}
|
||||
})
|
||||
useHotkeys('mod+shift+z', (e) => {
|
||||
e.preventDefault()
|
||||
if (editorView) {
|
||||
redo(editorView)
|
||||
}
|
||||
})
|
||||
|
||||
const {
|
||||
context: { selectionRanges, selectionRangeTypeMap },
|
||||
@ -85,6 +111,9 @@ export const TextEditor = ({
|
||||
const { settings: { context: { textWrapping } = {} } = {}, auth } =
|
||||
useGlobalStateContext()
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const {
|
||||
context: { project },
|
||||
} = useFileContext()
|
||||
const { enable: convertEnabled, handleClick: convertCallback } =
|
||||
useConvertToVariable()
|
||||
|
||||
@ -107,7 +136,7 @@ export const TextEditor = ({
|
||||
}, [setIsKclLspServerReady])
|
||||
|
||||
// Here we initialize the plugin which will start the client.
|
||||
// When we have multi-file support the name of the file will be a dep of
|
||||
// Now that we have multi-file support the name of the file is a dep of
|
||||
// this use memo, as well as the directory structure, which I think is
|
||||
// a good setup because it will restart the client but not the server :)
|
||||
// We do not want to restart the server, its just wasteful.
|
||||
@ -163,11 +192,12 @@ export const TextEditor = ({
|
||||
plugin = lsp
|
||||
}
|
||||
return plugin
|
||||
}, [copilotLspClient, isCopilotLspServerReady])
|
||||
}, [copilotLspClient, isCopilotLspServerReady, project])
|
||||
|
||||
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
||||
const onChange = (newCode: string) => {
|
||||
kclManager.setCodeAndExecute(newCode)
|
||||
const onChange = async (newCode: string) => {
|
||||
if (isNetworkOkay) kclManager.setCodeAndExecute(newCode)
|
||||
else kclManager.setCode(newCode)
|
||||
} //, []);
|
||||
const onUpdate = (viewUpdate: ViewUpdate) => {
|
||||
if (!editorView) {
|
||||
|
@ -27,6 +27,8 @@ describe('UserSidebarMenu tests', () => {
|
||||
phone: '555-555-5555',
|
||||
first_name: 'Test',
|
||||
last_name: 'User',
|
||||
can_train_on_data: false,
|
||||
is_service_account: false,
|
||||
}
|
||||
|
||||
render(
|
||||
@ -57,6 +59,8 @@ describe('UserSidebarMenu tests', () => {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
name: '',
|
||||
can_train_on_data: false,
|
||||
is_service_account: false,
|
||||
}
|
||||
|
||||
render(
|
||||
@ -84,6 +88,8 @@ describe('UserSidebarMenu tests', () => {
|
||||
first_name: 'Test',
|
||||
last_name: 'User',
|
||||
image: '',
|
||||
can_train_on_data: false,
|
||||
is_service_account: false,
|
||||
}
|
||||
|
||||
render(
|
||||
|
@ -142,7 +142,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="externalLink"
|
||||
to="https://github.com/KittyCAD/modeling-app/issues/new"
|
||||
to="https://github.com/KittyCAD/modeling-app/issues/new/choose"
|
||||
icon={{ icon: faBug, className: 'p-1', size: 'sm' }}
|
||||
className="border-transparent dark:border-transparent"
|
||||
>
|
||||
|
@ -4,6 +4,7 @@ import { ViewPlugin, hoverTooltip, tooltips } from '@codemirror/view'
|
||||
import { CompletionTriggerKind } from 'vscode-languageserver-protocol'
|
||||
import { offsetToPos } from 'editor/plugins/lsp/util'
|
||||
import { LanguageServerOptions } from 'editor/plugins/lsp'
|
||||
import { syntaxTree } from '@codemirror/language'
|
||||
import {
|
||||
LanguageServerPlugin,
|
||||
documentUri,
|
||||
@ -40,6 +41,14 @@ export function kclPlugin(options: LanguageServerOptions): Extension {
|
||||
if (plugin == null) return null
|
||||
|
||||
const { state, pos, explicit } = context
|
||||
|
||||
let nodeBefore = syntaxTree(state).resolveInner(pos, -1)
|
||||
if (
|
||||
nodeBefore.name === 'BlockComment' ||
|
||||
nodeBefore.name === 'LineComment'
|
||||
)
|
||||
return null
|
||||
|
||||
const line = state.doc.lineAt(pos)
|
||||
let trigKind: CompletionTriggerKind = CompletionTriggerKind.Invoked
|
||||
let trigChar: string | undefined
|
||||
@ -60,6 +69,7 @@ export function kclPlugin(options: LanguageServerOptions): Extension {
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return await plugin.requestCompletion(
|
||||
context,
|
||||
offsetToPos(state.doc, pos),
|
||||
|
@ -7,6 +7,5 @@ export const VITE_KC_API_BASE_URL = import.meta.env.VITE_KC_API_BASE_URL
|
||||
export const VITE_KC_SITE_BASE_URL = import.meta.env.VITE_KC_SITE_BASE_URL
|
||||
export const VITE_KC_CONNECTION_TIMEOUT_MS = import.meta.env
|
||||
.VITE_KC_CONNECTION_TIMEOUT_MS
|
||||
export const VITE_KC_SENTRY_DSN = import.meta.env.VITE_KC_SENTRY_DSN
|
||||
export const TEST = import.meta.env.TEST
|
||||
export const DEV = import.meta.env.DEV
|
||||
|
@ -7,6 +7,12 @@ import { authMachine } from 'machines/authMachine'
|
||||
import { settingsMachine } from 'machines/settingsMachine'
|
||||
import { homeMachine } from 'machines/homeMachine'
|
||||
import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes'
|
||||
import {
|
||||
NetworkHealthState,
|
||||
useNetworkStatus,
|
||||
} from 'components/NetworkHealthIndicator'
|
||||
import { useKclContext } from 'lang/KclSingleton'
|
||||
import { useStore } from 'useStore'
|
||||
|
||||
// This might not be necessary, AnyStateMachine from xstate is working
|
||||
export type AllMachines =
|
||||
@ -24,6 +30,7 @@ interface UseStateMachineCommandsArgs<
|
||||
send: Function
|
||||
actor?: InterpreterFrom<T>
|
||||
commandBarConfig?: CommandSetConfig<T, S>
|
||||
allCommandsRequireNetwork?: boolean
|
||||
onCancel?: () => void
|
||||
}
|
||||
|
||||
@ -36,12 +43,21 @@ export default function useStateMachineCommands<
|
||||
send,
|
||||
actor,
|
||||
commandBarConfig,
|
||||
allCommandsRequireNetwork = false,
|
||||
onCancel,
|
||||
}: UseStateMachineCommandsArgs<T, S>) {
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const { overallState } = useNetworkStatus()
|
||||
const { isExecuting } = useKclContext()
|
||||
const { isStreamReady } = useStore((s) => ({
|
||||
isStreamReady: s.isStreamReady,
|
||||
}))
|
||||
|
||||
useEffect(() => {
|
||||
const disableAllButtons =
|
||||
overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady
|
||||
const newCommands = state.nextEvents
|
||||
.filter((_) => !allCommandsRequireNetwork || !disableAllButtons)
|
||||
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
|
||||
.map((type) =>
|
||||
createMachineCommand<T, S>({
|
||||
@ -64,5 +80,5 @@ export default function useStateMachineCommands<
|
||||
data: { commands: newCommands },
|
||||
})
|
||||
}
|
||||
}, [state])
|
||||
}, [state, overallState, isExecuting, isStreamReady])
|
||||
}
|
||||
|
@ -93,7 +93,7 @@ class KclManager {
|
||||
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
|
||||
this._params.id &&
|
||||
writeTextFile(this._params.id, code).catch((err) => {
|
||||
// TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
|
||||
// TODO: add tracing per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
|
||||
console.error('error saving file', err)
|
||||
toast.error('Error saving file, please check file permissions')
|
||||
})
|
||||
@ -239,8 +239,8 @@ class KclManager {
|
||||
const currentExecutionId = executionId || Date.now()
|
||||
this._cancelTokens.set(currentExecutionId, false)
|
||||
|
||||
await this.ensureWasmInit()
|
||||
this.isExecuting = true
|
||||
await this.ensureWasmInit()
|
||||
const { logs, errors, programMemory } = await executeAst({
|
||||
ast,
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
|
@ -11,59 +11,53 @@ const mySketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> lineTo([-1.59, -1.54], %)
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
// |> rx(45, %)
|
||||
show(mySketch001)`
|
||||
// |> rx(45, %)`
|
||||
const programMemory = await enginelessExecutor(parse(code))
|
||||
// @ts-ignore
|
||||
const shown = programMemory?.return?.map(
|
||||
// @ts-ignore
|
||||
(a) => programMemory?.root?.[a.name]
|
||||
)
|
||||
expect(shown).toEqual([
|
||||
{
|
||||
type: 'SketchGroup',
|
||||
on: expect.any(Object),
|
||||
start: {
|
||||
to: [0, 0],
|
||||
from: [0, 0],
|
||||
const sketch001 = programMemory?.root?.mySketch001
|
||||
expect(sketch001).toEqual({
|
||||
type: 'SketchGroup',
|
||||
on: expect.any(Object),
|
||||
start: {
|
||||
to: [0, 0],
|
||||
from: [0, 0],
|
||||
name: '',
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [46, 71],
|
||||
},
|
||||
},
|
||||
value: [
|
||||
{
|
||||
type: 'ToPoint',
|
||||
name: '',
|
||||
to: [-1.59, -1.54],
|
||||
from: [0, 0],
|
||||
__geoMeta: {
|
||||
sourceRange: [77, 102],
|
||||
id: expect.any(String),
|
||||
sourceRange: [46, 71],
|
||||
},
|
||||
},
|
||||
value: [
|
||||
{
|
||||
type: 'ToPoint',
|
||||
name: '',
|
||||
to: [-1.59, -1.54],
|
||||
from: [0, 0],
|
||||
__geoMeta: {
|
||||
sourceRange: [77, 102],
|
||||
id: expect.any(String),
|
||||
},
|
||||
{
|
||||
type: 'ToPoint',
|
||||
to: [0.46, -5.82],
|
||||
from: [-1.59, -1.54],
|
||||
name: '',
|
||||
__geoMeta: {
|
||||
sourceRange: [108, 132],
|
||||
id: expect.any(String),
|
||||
},
|
||||
{
|
||||
type: 'ToPoint',
|
||||
to: [0.46, -5.82],
|
||||
from: [-1.59, -1.54],
|
||||
name: '',
|
||||
__geoMeta: {
|
||||
sourceRange: [108, 132],
|
||||
id: expect.any(String),
|
||||
},
|
||||
},
|
||||
],
|
||||
position: [0, 0, 0],
|
||||
rotation: [0, 0, 0, 1],
|
||||
xAxis: { x: 1, y: 0, z: 0 },
|
||||
yAxis: { x: 0, y: 1, z: 0 },
|
||||
zAxis: { x: 0, y: 0, z: 1 },
|
||||
id: expect.any(String),
|
||||
entityId: expect.any(String),
|
||||
__meta: [{ sourceRange: [46, 71] }],
|
||||
},
|
||||
])
|
||||
},
|
||||
],
|
||||
position: [0, 0, 0],
|
||||
rotation: [0, 0, 0, 1],
|
||||
xAxis: { x: 1, y: 0, z: 0 },
|
||||
yAxis: { x: 0, y: 1, z: 0 },
|
||||
zAxis: { x: 0, y: 0, z: 1 },
|
||||
id: expect.any(String),
|
||||
entityId: expect.any(String),
|
||||
__meta: [{ sourceRange: [46, 71] }],
|
||||
})
|
||||
})
|
||||
test('extrude artifacts', async () => {
|
||||
// Enable rotations #152
|
||||
@ -73,30 +67,24 @@ const mySketch001 = startSketchOn('XY')
|
||||
|> lineTo([-1.59, -1.54], %)
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
// |> rx(45, %)
|
||||
|> extrude(2, %)
|
||||
show(mySketch001)`
|
||||
|> extrude(2, %)`
|
||||
const programMemory = await enginelessExecutor(parse(code))
|
||||
// @ts-ignore
|
||||
const shown = programMemory?.return?.map(
|
||||
// @ts-ignore
|
||||
(a) => programMemory?.root?.[a.name]
|
||||
)
|
||||
expect(shown).toEqual([
|
||||
{
|
||||
type: 'ExtrudeGroup',
|
||||
id: expect.any(String),
|
||||
value: [],
|
||||
height: 2,
|
||||
position: [0, 0, 0],
|
||||
rotation: [0, 0, 0, 1],
|
||||
endCapId: null,
|
||||
startCapId: null,
|
||||
xAxis: { x: 1, y: 0, z: 0 },
|
||||
yAxis: { x: 0, y: 1, z: 0 },
|
||||
zAxis: { x: 0, y: 0, z: 1 },
|
||||
__meta: [{ sourceRange: [46, 71] }],
|
||||
},
|
||||
])
|
||||
const sketch001 = programMemory?.root?.mySketch001
|
||||
expect(sketch001).toEqual({
|
||||
type: 'ExtrudeGroup',
|
||||
id: expect.any(String),
|
||||
value: [],
|
||||
height: 2,
|
||||
position: [0, 0, 0],
|
||||
rotation: [0, 0, 0, 1],
|
||||
endCapId: null,
|
||||
startCapId: null,
|
||||
xAxis: { x: 1, y: 0, z: 0 },
|
||||
yAxis: { x: 0, y: 1, z: 0 },
|
||||
zAxis: { x: 0, y: 0, z: 1 },
|
||||
__meta: [{ sourceRange: [46, 71] }],
|
||||
})
|
||||
})
|
||||
test('sketch extrude and sketch on one of the faces', async () => {
|
||||
// Enable rotations #152
|
||||
@ -120,14 +108,10 @@ const sk2 = startSketchOn('XY')
|
||||
// |> transform(theTransf, %)
|
||||
|> extrude(2, %)
|
||||
|
||||
|
||||
show(theExtrude, sk2)`
|
||||
`
|
||||
const programMemory = await enginelessExecutor(parse(code))
|
||||
// @ts-ignore
|
||||
const geos = programMemory?.return?.map(
|
||||
// @ts-ignore
|
||||
({ name }) => programMemory?.root?.[name]
|
||||
)
|
||||
const geos = [programMemory?.root?.theExtrude, programMemory?.root?.sk2]
|
||||
expect(geos).toEqual([
|
||||
{
|
||||
type: 'ExtrudeGroup',
|
||||
|
@ -47,9 +47,8 @@ const newVar = myVar + 1`
|
||||
|> lineTo([2,3], %)
|
||||
|> lineTo({ to: [5,-1], tag: "rightPath" }, %)
|
||||
// |> close(%)
|
||||
show(mySketch)
|
||||
`
|
||||
const { root, return: _return } = await exe(code)
|
||||
const { root } = await exe(code)
|
||||
// geo is three js buffer geometry and is very bloated to have in tests
|
||||
const minusGeo = root.mySketch.value
|
||||
expect(minusGeo).toEqual([
|
||||
@ -84,15 +83,6 @@ show(mySketch)
|
||||
name: 'rightPath',
|
||||
},
|
||||
])
|
||||
// expect(root.mySketch.sketch[0]).toEqual(root.mySketch.sketch[4].firstPath)
|
||||
expect(_return).toEqual([
|
||||
{
|
||||
type: 'Identifier',
|
||||
start: 203,
|
||||
end: 211,
|
||||
name: 'mySketch',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('pipe binary expression into call expression', async () => {
|
||||
@ -357,7 +347,6 @@ describe('testing math operators', () => {
|
||||
` -legLen(segLen('seg01', %), myVar)`,
|
||||
`], %)`,
|
||||
``,
|
||||
`show(part001)`,
|
||||
].join('\n')
|
||||
const { root } = await exe(code)
|
||||
const sketch = root.part001
|
||||
@ -392,8 +381,7 @@ const theExtrude = startSketchOn('XY')
|
||||
|> line([-0.76], myVarZ, %)
|
||||
|> line([5,5], %)
|
||||
|> close(%)
|
||||
|> extrude(4, %)
|
||||
show(theExtrude)`
|
||||
|> extrude(4, %)`
|
||||
await expect(exe(code)).rejects.toEqual(
|
||||
new KCLError(
|
||||
'undefined_value',
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { parse, recast, initPromise } from './wasm'
|
||||
import { parse, recast, initPromise, Identifier } from './wasm'
|
||||
import {
|
||||
createLiteral,
|
||||
createIdentifier,
|
||||
@ -90,7 +90,17 @@ describe('Testing createPipeExpression', () => {
|
||||
describe('Testing findUniqueName', () => {
|
||||
it('should find a unique name', () => {
|
||||
const result = findUniqueName(
|
||||
'yo01 yo02 yo03 yo04 yo05 yo06 yo07 yo08 yo09',
|
||||
JSON.stringify([
|
||||
{ type: 'Identifier', name: 'yo01', start: 0, end: 0 },
|
||||
{ type: 'Identifier', name: 'yo02', start: 0, end: 0 },
|
||||
{ type: 'Identifier', name: 'yo03', start: 0, end: 0 },
|
||||
{ type: 'Identifier', name: 'yo04', start: 0, end: 0 },
|
||||
{ type: 'Identifier', name: 'yo05', start: 0, end: 0 },
|
||||
{ type: 'Identifier', name: 'yo06', start: 0, end: 0 },
|
||||
{ type: 'Identifier', name: 'yo07', start: 0, end: 0 },
|
||||
{ type: 'Identifier', name: 'yo08', start: 0, end: 0 },
|
||||
{ type: 'Identifier', name: 'yo09', start: 0, end: 0 },
|
||||
] satisfies Identifier[]),
|
||||
'yo',
|
||||
2
|
||||
)
|
||||
@ -112,7 +122,6 @@ describe('Testing addSketchTo', () => {
|
||||
expect(str).toBe(`const part001 = startSketchOn('YZ')
|
||||
|> startProfileAt('default', %)
|
||||
|> line('default', %)
|
||||
show(part001)
|
||||
`)
|
||||
})
|
||||
})
|
||||
@ -137,8 +146,7 @@ describe('Testing giveSketchFnCallTag', () => {
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([-2.57, -0.13], %)
|
||||
|> line([0, 0.83], %)
|
||||
|> line([0.82, 0.34], %)
|
||||
show(part001)`
|
||||
|> line([0.82, 0.34], %)`
|
||||
it('Should add tag to a sketch function call', () => {
|
||||
const { newCode, tag, isTagExisting } = giveSketchFnCallTagTestHelper(
|
||||
code,
|
||||
@ -194,8 +202,7 @@ const part001 = startSketchOn('XY')
|
||||
|> angledLine([def(yo), 3.09], %)
|
||||
|> angledLine([ghi(%), 3.09], %)
|
||||
|> angledLine([jkl(yo) + 2, 3.09], %)
|
||||
const yo2 = hmm([identifierGuy + 5])
|
||||
show(part001)`
|
||||
const yo2 = hmm([identifierGuy + 5])`
|
||||
it('should move a binary expression into a new variable', async () => {
|
||||
const ast = parse(code)
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
|
@ -128,16 +128,8 @@ export function addSketchTo(
|
||||
createPipeExpression(pipeBody)
|
||||
)
|
||||
|
||||
const showCallIndex = getShowIndex(_node)
|
||||
let sketchIndex = showCallIndex
|
||||
if (showCallIndex === -1) {
|
||||
_node.body = [...node.body, variableDeclaration]
|
||||
sketchIndex = _node.body.length - 1
|
||||
} else {
|
||||
const newBody = [...node.body]
|
||||
newBody.splice(showCallIndex, 0, variableDeclaration)
|
||||
_node.body = newBody
|
||||
}
|
||||
_node.body = [...node.body, variableDeclaration]
|
||||
let sketchIndex = _node.body.length - 1
|
||||
let pathToNode: PathToNode = [
|
||||
['body', ''],
|
||||
[sketchIndex, 'index'],
|
||||
@ -150,7 +142,7 @@ export function addSketchTo(
|
||||
}
|
||||
|
||||
return {
|
||||
modifiedAst: addToShow(_node, _name),
|
||||
modifiedAst: _node,
|
||||
id: _name,
|
||||
pathToNode,
|
||||
}
|
||||
@ -162,59 +154,35 @@ export function findUniqueName(
|
||||
pad = 3,
|
||||
index = 1
|
||||
): string {
|
||||
let searchStr = ''
|
||||
if (typeof ast === 'string') {
|
||||
searchStr = ast
|
||||
} else {
|
||||
searchStr = JSON.stringify(ast)
|
||||
let searchStr: string = typeof ast === 'string' ? ast : JSON.stringify(ast)
|
||||
const indexStr = String(index).padStart(pad, '0')
|
||||
|
||||
const endingDigitsMatcher = /\d+$/
|
||||
const nameEndsInDigits = name.match(endingDigitsMatcher)
|
||||
let nameIsInString = searchStr.includes(`:"${name}"`)
|
||||
|
||||
if (nameEndsInDigits !== null) {
|
||||
// base case: name is unique and ends in digits
|
||||
if (!nameIsInString) return name
|
||||
|
||||
// recursive case: name is not unique and ends in digits
|
||||
const newPad = nameEndsInDigits[1].length
|
||||
const newIndex = parseInt(nameEndsInDigits[1]) + 1
|
||||
const nameWithoutDigits = name.replace(endingDigitsMatcher, '')
|
||||
|
||||
return findUniqueName(searchStr, nameWithoutDigits, newPad, newIndex)
|
||||
}
|
||||
const indexStr = `${index}`.padStart(pad, '0')
|
||||
|
||||
const newName = `${name}${indexStr}`
|
||||
const isInString = searchStr.includes(newName)
|
||||
if (!isInString) {
|
||||
return newName
|
||||
}
|
||||
nameIsInString = searchStr.includes(`:"${newName}"`)
|
||||
|
||||
// base case: name is unique and does not end in digits
|
||||
if (!nameIsInString) return newName
|
||||
|
||||
// recursive case: name is not unique and does not end in digits
|
||||
return findUniqueName(searchStr, name, pad, index + 1)
|
||||
}
|
||||
|
||||
function addToShow(node: Program, name: string): Program {
|
||||
const _node = { ...node }
|
||||
const dumbyStartend = { start: 0, end: 0 }
|
||||
const showCallIndex = getShowIndex(_node)
|
||||
if (showCallIndex === -1) {
|
||||
const showCall = createCallExpressionStdLib('show', [
|
||||
createIdentifier(name),
|
||||
])
|
||||
const showExpressionStatement: ExpressionStatement = {
|
||||
type: 'ExpressionStatement',
|
||||
...dumbyStartend,
|
||||
expression: showCall,
|
||||
}
|
||||
_node.body = [..._node.body, showExpressionStatement]
|
||||
return _node
|
||||
}
|
||||
const showCall = { ..._node.body[showCallIndex] } as ExpressionStatement
|
||||
const showCallArgs = (showCall.expression as CallExpression).arguments
|
||||
const newShowCallArgs: Value[] = [...showCallArgs, createIdentifier(name)]
|
||||
const newShowExpression = createCallExpressionStdLib('show', newShowCallArgs)
|
||||
|
||||
_node.body[showCallIndex] = {
|
||||
...showCall,
|
||||
expression: newShowExpression,
|
||||
}
|
||||
return _node
|
||||
}
|
||||
|
||||
function getShowIndex(node: Program): number {
|
||||
return node.body.findIndex(
|
||||
(statement) =>
|
||||
statement.type === 'ExpressionStatement' &&
|
||||
statement.expression.type === 'CallExpression' &&
|
||||
statement.expression.callee.type === 'Identifier' &&
|
||||
statement.expression.callee.name === 'show'
|
||||
)
|
||||
}
|
||||
|
||||
export function mutateArrExp(
|
||||
node: Value,
|
||||
updateWith: ArrayExpression
|
||||
@ -273,7 +241,7 @@ export function extrudeSketch(
|
||||
node: Program,
|
||||
pathToNode: PathToNode,
|
||||
shouldPipe = true,
|
||||
distance = 4
|
||||
distance = createLiteral(4) as Value
|
||||
): {
|
||||
modifiedAst: Program
|
||||
pathToNode: PathToNode
|
||||
@ -299,7 +267,7 @@ export function extrudeSketch(
|
||||
getNodeFromPath<VariableDeclarator>(_node, pathToNode, 'VariableDeclarator')
|
||||
|
||||
const extrudeCall = createCallExpressionStdLib('extrude', [
|
||||
createLiteral(distance),
|
||||
distance,
|
||||
shouldPipe
|
||||
? createPipeSubstitution()
|
||||
: {
|
||||
@ -334,15 +302,10 @@ export function extrudeSketch(
|
||||
}
|
||||
const name = findUniqueName(node, 'part')
|
||||
const VariableDeclaration = createVariableDeclaration(name, extrudeCall)
|
||||
let showCallIndex = getShowIndex(_node)
|
||||
if (showCallIndex === -1) {
|
||||
// We didn't find a show, so let's just append everything
|
||||
showCallIndex = _node.body.length
|
||||
}
|
||||
_node.body.splice(showCallIndex, 0, VariableDeclaration)
|
||||
_node.body.splice(_node.body.length, 0, VariableDeclaration)
|
||||
const pathToExtrudeArg: PathToNode = [
|
||||
['body', ''],
|
||||
[showCallIndex, 'index'],
|
||||
[_node.body.length, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclarator'],
|
||||
@ -351,7 +314,7 @@ export function extrudeSketch(
|
||||
]
|
||||
return {
|
||||
modifiedAst: node,
|
||||
pathToNode: [...pathToNode.slice(0, -1), [showCallIndex, 'index']],
|
||||
pathToNode: [...pathToNode.slice(0, -1), [-1, 'index']],
|
||||
pathToExtrudeArg,
|
||||
}
|
||||
}
|
||||
@ -411,7 +374,7 @@ export function sketchOnExtrudedFace(
|
||||
_node.body.splice(expressionIndex + 1, 0, newSketch)
|
||||
|
||||
return {
|
||||
modifiedAst: addToShow(_node, newSketchName),
|
||||
modifiedAst: _node,
|
||||
pathToNode: [...pathToNode.slice(0, -1), [expressionIndex, 'index']],
|
||||
}
|
||||
}
|
||||
|
@ -34,8 +34,7 @@ const part001 = startSketchOn('XY')
|
||||
|> xLine(3.84, %) // selection-range-7ish-before-this
|
||||
|
||||
const variableBelowShouldNotBeIncluded = 3
|
||||
|
||||
show(part001)`
|
||||
`
|
||||
const rangeStart = code.indexOf('// selection-range-7ish-before-this') - 7
|
||||
const ast = parse(code)
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
@ -69,8 +68,7 @@ describe('testing argIsNotIdentifier', () => {
|
||||
|> angledLine([ghi(%), 3.09], %)
|
||||
|> angledLine([jkl('yo') + 2, 3.09], %)
|
||||
const yo = 5 + 6
|
||||
const yo2 = hmm([identifierGuy + 5])
|
||||
show(part001)`
|
||||
const yo2 = hmm([identifierGuy + 5])`
|
||||
it('find a safe binaryExpression', () => {
|
||||
const ast = parse(code)
|
||||
const rangeStart = code.indexOf('100 + 100') + 2
|
||||
@ -201,8 +199,7 @@ describe('testing getNodePathFromSourceRange', () => {
|
||||
const code = `const part001 = startSketchOn('XY')
|
||||
|> startProfileAt([0.39, -0.05], %)
|
||||
|> line([0.94, 2.61], %)
|
||||
|> line([-0.21, -1.4], %)
|
||||
show(part001)`
|
||||
|> line([-0.21, -1.4], %)`
|
||||
it('finds the second line when cursor is put at the end', () => {
|
||||
const searchLn = `line([0.94, 2.61], %)`
|
||||
const sourceIndex = code.indexOf(searchLn) + searchLn.length
|
||||
|
@ -68,8 +68,6 @@ log(5, myVar)
|
||||
|> lineTo([1, 1], %)
|
||||
|> lineTo({ to: [1, 0], tag: "rightPath" }, %)
|
||||
|> close(%)
|
||||
|
||||
show(mySketch)
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
@ -331,7 +329,6 @@ describe('it recasts wrapped object expressions in pipe bodies with correct inde
|
||||
intersectTag: 'seg01'
|
||||
}, %)
|
||||
|> line([-0.42, -1.72], %)
|
||||
show(part001)
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
|
@ -3,7 +3,6 @@ import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { exportSave } from 'lib/exportSave'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import * as Sentry from '@sentry/react'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { sceneInfra } from 'clientSideScene/sceneInfra'
|
||||
|
||||
@ -290,12 +289,6 @@ class EngineConnection {
|
||||
}
|
||||
}
|
||||
|
||||
// shouldTrace will return true when Sentry should be used to instrument
|
||||
// the Engine.
|
||||
shouldTrace() {
|
||||
return Sentry.getCurrentHub()?.getClient()?.getOptions()?.sendClientReports
|
||||
}
|
||||
|
||||
// connect will attempt to connect to the Engine over a WebSocket, and
|
||||
// establish the WebRTC connections.
|
||||
//
|
||||
@ -308,41 +301,6 @@ class EngineConnection {
|
||||
|
||||
// Information on the connect transaction
|
||||
|
||||
class SpanPromise {
|
||||
span: Sentry.Span
|
||||
promise: Promise<void>
|
||||
resolve?: (v: void) => void
|
||||
|
||||
constructor(span: Sentry.Span) {
|
||||
this.span = span
|
||||
this.promise = new Promise((resolve) => {
|
||||
this.resolve = (v: void) => {
|
||||
// here we're going to invoke finish before resolving the
|
||||
// promise so that a `.then()` will order strictly after
|
||||
// all spans have -- for sure -- been resolved, rather than
|
||||
// doing a `then` on this promise.
|
||||
this.span.finish()
|
||||
resolve(v)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let webrtcMediaTransaction: Sentry.Transaction
|
||||
let websocketSpan: SpanPromise
|
||||
let mediaTrackSpan: SpanPromise
|
||||
let dataChannelSpan: SpanPromise
|
||||
let handshakeSpan: SpanPromise
|
||||
let iceSpan: SpanPromise
|
||||
|
||||
const spanStart = (op: string) =>
|
||||
new SpanPromise(webrtcMediaTransaction.startChild({ op }))
|
||||
|
||||
if (this.shouldTrace()) {
|
||||
webrtcMediaTransaction = Sentry.startTransaction({ name: 'webrtc-media' })
|
||||
websocketSpan = spanStart('websocket')
|
||||
}
|
||||
|
||||
const createPeerConnection = () => {
|
||||
this.pc = new RTCPeerConnection()
|
||||
|
||||
@ -393,10 +351,6 @@ class EngineConnection {
|
||||
// From what I understand, only after have we done the ICE song and
|
||||
// dance is it safest to connect the video tracks / stream
|
||||
case 'connected':
|
||||
if (this.shouldTrace()) {
|
||||
iceSpan.resolve?.()
|
||||
}
|
||||
|
||||
// Let the browser attach to the video stream now
|
||||
this.onNewTrack({ conn: this, mediaStream: this.mediaStream! })
|
||||
break
|
||||
@ -429,17 +383,6 @@ class EngineConnection {
|
||||
},
|
||||
}
|
||||
|
||||
if (this.shouldTrace()) {
|
||||
let mediaStreamTrack = mediaStream.getVideoTracks()[0]
|
||||
mediaStreamTrack.addEventListener('unmute', () => {
|
||||
// let settings = mediaStreamTrack.getSettings()
|
||||
// mediaTrackSpan.span.setTag("fps", settings.frameRate)
|
||||
// mediaTrackSpan.span.setTag("width", settings.width)
|
||||
// mediaTrackSpan.span.setTag("height", settings.height)
|
||||
mediaTrackSpan.resolve?.()
|
||||
})
|
||||
}
|
||||
|
||||
this.webrtcStatsCollector = (): Promise<ClientMetrics> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (mediaStream.getVideoTracks().length !== 1) {
|
||||
@ -522,10 +465,6 @@ class EngineConnection {
|
||||
},
|
||||
}
|
||||
|
||||
if (this.shouldTrace()) {
|
||||
dataChannelSpan.resolve?.()
|
||||
}
|
||||
|
||||
// Everything is now connected.
|
||||
this.state = { type: EngineConnectionStateType.ConnectionEstablished }
|
||||
|
||||
@ -577,27 +516,6 @@ class EngineConnection {
|
||||
if (this.token) {
|
||||
this.send({ headers: { Authorization: `Bearer ${this.token}` } })
|
||||
}
|
||||
|
||||
if (this.shouldTrace()) {
|
||||
websocketSpan.resolve?.()
|
||||
|
||||
handshakeSpan = spanStart('handshake')
|
||||
iceSpan = spanStart('ice')
|
||||
dataChannelSpan = spanStart('data-channel')
|
||||
mediaTrackSpan = spanStart('media-track')
|
||||
}
|
||||
|
||||
if (this.shouldTrace()) {
|
||||
void Promise.all([
|
||||
handshakeSpan.promise,
|
||||
iceSpan.promise,
|
||||
dataChannelSpan.promise,
|
||||
mediaTrackSpan.promise,
|
||||
]).then(() => {
|
||||
console.log('All spans finished, reporting')
|
||||
webrtcMediaTransaction?.finish()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
this.websocket.addEventListener('close', (event) => {
|
||||
@ -786,13 +704,6 @@ failed cmd type was ${artifactThatFailed?.commandType}`
|
||||
type: ConnectingType.WebRTCConnecting,
|
||||
},
|
||||
}
|
||||
|
||||
if (this.shouldTrace()) {
|
||||
// When both ends have a local and remote SDP, we've been able to
|
||||
// set up successfully. We'll still need to find the right ICE
|
||||
// servers, but this is hand-shook.
|
||||
handshakeSpan.resolve?.()
|
||||
}
|
||||
break
|
||||
|
||||
case 'trickle_ice':
|
||||
@ -885,7 +796,7 @@ interface UnreliableSubscription<T extends UnreliableResponses['type']> {
|
||||
callback: (data: Extract<UnreliableResponses, { type: T }>) => void
|
||||
}
|
||||
|
||||
interface Subscription<T extends ModelTypes> {
|
||||
export interface Subscription<T extends ModelTypes> {
|
||||
event: T
|
||||
callback: (
|
||||
data: Extract<Models['OkModelingCmdResponse_type'], { type: T }>
|
||||
@ -996,9 +907,6 @@ export class EngineCommandManager {
|
||||
}
|
||||
},
|
||||
onEngineConnectionOpen: () => {
|
||||
this.resolveReady()
|
||||
setIsStreamReady(true)
|
||||
|
||||
// Make the axis gizmo.
|
||||
// We do this after the connection opened to avoid a race condition.
|
||||
// Connected opened is the last thing that happens when the stream
|
||||
@ -1017,9 +925,20 @@ export class EngineCommandManager {
|
||||
gizmo_mode: true,
|
||||
},
|
||||
})
|
||||
sceneInfra.onStreamStart()
|
||||
sceneInfra.camControls.onCameraChange()
|
||||
this.sendSceneCommand({
|
||||
// CameraControls subscribes to default_camera_get_settings response events
|
||||
// firing this at connection ensure the camera's are synced initially
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_get_settings',
|
||||
},
|
||||
})
|
||||
|
||||
this.initPlanes().then(() => {
|
||||
this.resolveReady()
|
||||
setIsStreamReady(true)
|
||||
executeCode(undefined, true)
|
||||
})
|
||||
},
|
||||
|
@ -101,7 +101,6 @@ describe('testing changeSketchArguments', () => {
|
||||
|> ${line}
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
// |> rx(45, %)
|
||||
show(mySketch001)
|
||||
`
|
||||
const code = genCode(lineToChange)
|
||||
const expectedCode = genCode(lineAfterChange)
|
||||
@ -128,8 +127,7 @@ const mySketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([0, 0], %)
|
||||
// |> rx(45, %)
|
||||
|> lineTo([-1.59, -1.54], %)
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
show(mySketch001)`
|
||||
|> lineTo([0.46, -5.82], %)`
|
||||
const ast = parse(code)
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const sourceStart = code.indexOf(lineToChange)
|
||||
@ -155,7 +153,6 @@ show(mySketch001)`
|
||||
|> lineTo([-1.59, -1.54], %)
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
|> lineTo([2, 3], %)
|
||||
show(mySketch001)
|
||||
`
|
||||
expect(recast(modifiedAst)).toBe(expectedCode)
|
||||
|
||||
@ -177,7 +174,6 @@ show(mySketch001)
|
||||
|> lineTo([-1.59, -1.54], %)
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
|> close(%)
|
||||
show(mySketch001)
|
||||
`
|
||||
expect(recast(modifiedAst)).toBe(expectedCode)
|
||||
})
|
||||
@ -192,7 +188,6 @@ describe('testing addTagForSketchOnFace', () => {
|
||||
// |> rx(45, %)
|
||||
|> ${line}
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
show(mySketch001)
|
||||
`
|
||||
const code = genCode(originalLine)
|
||||
const ast = parse(code)
|
||||
|
@ -91,12 +91,6 @@ export function createFirstArg(
|
||||
throw new Error('all sketch line types should have been covered')
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
type LineData = {
|
||||
from: [number, number, number]
|
||||
to: [number, number, number]
|
||||
}
|
||||
|
||||
export const lineTo: SketchLineHelper = {
|
||||
add: ({
|
||||
node,
|
||||
@ -966,6 +960,30 @@ export const angledLineThatIntersects: SketchLineHelper = {
|
||||
addTag: addTagWithTo('angleTo'), // TODO might be wrong
|
||||
}
|
||||
|
||||
export const updateStartProfileAtArgs: SketchLineHelper['updateArgs'] = ({
|
||||
node,
|
||||
pathToNode,
|
||||
to,
|
||||
}) => {
|
||||
const _node = { ...node }
|
||||
const { node: callExpression } = getNodeFromPath<CallExpression>(
|
||||
_node,
|
||||
pathToNode
|
||||
)
|
||||
|
||||
const toArrExp = createArrayExpression([
|
||||
createLiteral(roundOff(to[0])),
|
||||
createLiteral(roundOff(to[1])),
|
||||
])
|
||||
|
||||
mutateArrExp(callExpression.arguments?.[0], toArrExp) ||
|
||||
mutateObjExpProp(callExpression.arguments?.[0], toArrExp, 'to')
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode,
|
||||
}
|
||||
}
|
||||
|
||||
export const sketchLineHelperMap: { [key: string]: SketchLineHelper } = {
|
||||
line,
|
||||
lineTo,
|
||||
|
@ -88,7 +88,6 @@ describe('testing swapping out sketch calls with xLine/xLineTo', () => {
|
||||
` |> yLine(-1.07, %)`,
|
||||
` |> xLineTo(3.27, %)`,
|
||||
` |> yLineTo(2.14, %)`,
|
||||
`show(part001)`,
|
||||
]
|
||||
const bigExample = bigExampleArr.join('\n')
|
||||
it('line with tag converts to xLine', async () => {
|
||||
@ -290,7 +289,6 @@ describe('testing swapping out sketch calls with xLine/xLineTo while keeping var
|
||||
` |> angledLineToX([330, angledLineToXx], %)`,
|
||||
` |> angledLineToY([217, angledLineToYy], %)`,
|
||||
` |> line([0.89, -0.1], %)`,
|
||||
`show(part001)`,
|
||||
]
|
||||
const varExample = variablesExampleArr.join('\n')
|
||||
it('line keeps variable when converted to xLine', async () => {
|
||||
@ -378,8 +376,7 @@ const part001 = startSketchOn('XY')
|
||||
|> line([0, 0.4], %)
|
||||
|> xLine(3.48, %)
|
||||
|> line([2.14, 1.35], %) // normal-segment
|
||||
|> xLine(3.54, %)
|
||||
show(part001)`
|
||||
|> xLine(3.54, %)`
|
||||
it('normal case works', async () => {
|
||||
const programMemory = await enginelessExecutor(parse(code))
|
||||
const index = code.indexOf('// normal-segment') - 7
|
||||
|
@ -123,7 +123,6 @@ const part001 = startSketchOn('XY')
|
||||
|> yLine(1.04, %) // ln-yLine-free should sub in segLen
|
||||
|> xLineTo(30, %) // ln-xLineTo-free should convert to xLine
|
||||
|> yLineTo(20, %) // ln-yLineTo-free should convert to yLine
|
||||
show(part001)
|
||||
`
|
||||
const expectModifiedScript = `const myVar = 3
|
||||
const myVar2 = 5
|
||||
@ -196,7 +195,6 @@ const part001 = startSketchOn('XY')
|
||||
|> yLine(segLen('seg01', %), %) // ln-yLine-free should sub in segLen
|
||||
|> xLine(segLen('seg01', %), %) // ln-xLineTo-free should convert to xLine
|
||||
|> yLine(segLen('seg01', %), %) // ln-yLineTo-free should convert to yLine
|
||||
show(part001)
|
||||
`
|
||||
it('should transform the ast', async () => {
|
||||
const ast = parse(inputScript)
|
||||
@ -257,7 +255,6 @@ const part001 = startSketchOn('XY')
|
||||
|> angledLineToY([223, 7.68], %) // select for vertical constraint 9
|
||||
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|
||||
|> angledLineToY([301, myVar], %) // select for vertical constraint 10
|
||||
show(part001)
|
||||
`
|
||||
it('should transform horizontal lines the ast', async () => {
|
||||
const expectModifiedScript = `const myVar = 2
|
||||
@ -286,7 +283,6 @@ const part001 = startSketchOn('XY')
|
||||
|> angledLineToY([223, 7.68], %) // select for vertical constraint 9
|
||||
|> xLineTo(myVar3, %) // select for horizontal constraint 10
|
||||
|> angledLineToY([301, myVar], %) // select for vertical constraint 10
|
||||
show(part001)
|
||||
`
|
||||
const ast = parse(inputScript)
|
||||
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
||||
@ -345,7 +341,6 @@ const part001 = startSketchOn('XY')
|
||||
|> yLineTo(7.68, %) // select for vertical constraint 9
|
||||
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|
||||
|> yLineTo(myVar, %) // select for vertical constraint 10
|
||||
show(part001)
|
||||
`
|
||||
const ast = parse(inputScript)
|
||||
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
||||
@ -389,7 +384,6 @@ const part001 = startSketchOn('XY')
|
||||
|> line([0.45, 1.46], %) // free
|
||||
|> line([myVar, 0.01], %) // xRelative
|
||||
|> line([0.7, myVar], %) // yRelative
|
||||
show(part001)
|
||||
`
|
||||
it('testing for free to horizontal and vertical distance', async () => {
|
||||
const expectedHorizontalCode = await helperThing(
|
||||
@ -501,8 +495,7 @@ const part001 = startSketchOn('XY')
|
||||
|> xLine(3.36, %) // partial
|
||||
|> line([-1.49, 1.06], %) // free
|
||||
|> xLine(-3.43 + 0, %) // full
|
||||
|> angledLineOfXLength([243 + 0, 1.2 + 0], %) // full
|
||||
show(part001)`
|
||||
|> angledLineOfXLength([243 + 0, 1.2 + 0], %) // full`
|
||||
const ast = parse(code)
|
||||
const constraintLevels: ReturnType<
|
||||
typeof getConstraintLevelFromSourceRange
|
||||
|
@ -15,8 +15,7 @@ describe('testing angledLineThatIntersects', () => {
|
||||
offset: ${offset},
|
||||
tag: "yo2"
|
||||
}, %)
|
||||
const intersect = segEndX('yo2', part001)
|
||||
show(part001)`
|
||||
const intersect = segEndX('yo2', part001)`
|
||||
const { root } = await enginelessExecutor(parse(code('-1')))
|
||||
expect(root.intersect.value).toBe(1 + Math.sqrt(2))
|
||||
const { root: noOffset } = await enginelessExecutor(parse(code('0')))
|
||||
|
@ -39,30 +39,36 @@ export interface MouseGuard {
|
||||
rotate: MouseGuardHandler
|
||||
}
|
||||
|
||||
const butName = (e: React.MouseEvent) => ({
|
||||
middle: !!(e.buttons & 4) || e.button === 1,
|
||||
right: !!(e.buttons & 2) || e.button === 2,
|
||||
left: !!(e.buttons & 1) || e.button === 0,
|
||||
})
|
||||
|
||||
export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
|
||||
KittyCAD: {
|
||||
pan: {
|
||||
description: 'Right click + Shift + drag or middle click + drag',
|
||||
callback: (e) =>
|
||||
(e.button === 1 && noModifiersPressed(e)) ||
|
||||
(e.button === 2 && e.shiftKey),
|
||||
(butName(e).middle && noModifiersPressed(e)) ||
|
||||
(butName(e).right && e.shiftKey),
|
||||
},
|
||||
zoom: {
|
||||
description: 'Scroll wheel or Right click + Ctrl + drag',
|
||||
dragCallback: (e) => e.button === 2 && e.ctrlKey,
|
||||
dragCallback: (e) => !!(e.buttons & 2) && e.ctrlKey,
|
||||
scrollCallback: () => true,
|
||||
},
|
||||
rotate: {
|
||||
description: 'Right click + drag',
|
||||
callback: (e) => e.button === 2 && noModifiersPressed(e),
|
||||
callback: (e) => butName(e).right && noModifiersPressed(e),
|
||||
},
|
||||
},
|
||||
OnShape: {
|
||||
pan: {
|
||||
description: 'Right click + Ctrl + drag or middle click + drag',
|
||||
callback: (e) =>
|
||||
(e.button === 2 && e.ctrlKey) ||
|
||||
(e.button === 1 && noModifiersPressed(e)),
|
||||
(butName(e).right && e.ctrlKey) ||
|
||||
(butName(e).middle && noModifiersPressed(e)),
|
||||
},
|
||||
zoom: {
|
||||
description: 'Scroll wheel',
|
||||
@ -71,77 +77,77 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
|
||||
},
|
||||
rotate: {
|
||||
description: 'Right click + drag',
|
||||
callback: (e) => e.button === 2 && noModifiersPressed(e),
|
||||
callback: (e) => butName(e).right && noModifiersPressed(e),
|
||||
},
|
||||
},
|
||||
'Trackpad Friendly': {
|
||||
pan: {
|
||||
description: 'Left click + Alt + Shift + drag or middle click + drag',
|
||||
callback: (e) =>
|
||||
(e.button === 0 && e.altKey && e.shiftKey && !e.metaKey) ||
|
||||
(e.button === 1 && noModifiersPressed(e)),
|
||||
(butName(e).left && e.altKey && e.shiftKey && !e.metaKey) ||
|
||||
(butName(e).middle && noModifiersPressed(e)),
|
||||
},
|
||||
zoom: {
|
||||
description: 'Scroll wheel or Left click + Alt + OS + drag',
|
||||
dragCallback: (e) => e.button === 0 && e.altKey && e.metaKey,
|
||||
dragCallback: (e) => butName(e).left && e.altKey && e.metaKey,
|
||||
scrollCallback: () => true,
|
||||
},
|
||||
rotate: {
|
||||
description: 'Left click + Alt + drag',
|
||||
callback: (e) => e.button === 0 && e.altKey && !e.shiftKey && !e.metaKey,
|
||||
callback: (e) => butName(e).left && e.altKey && !e.shiftKey && !e.metaKey,
|
||||
lenientDragStartButton: 0,
|
||||
},
|
||||
},
|
||||
Solidworks: {
|
||||
pan: {
|
||||
description: 'Right click + Ctrl + drag',
|
||||
callback: (e) => e.button === 2 && e.ctrlKey,
|
||||
callback: (e) => butName(e).right && e.ctrlKey,
|
||||
lenientDragStartButton: 2,
|
||||
},
|
||||
zoom: {
|
||||
description: 'Scroll wheel or Middle click + Shift + drag',
|
||||
dragCallback: (e) => e.button === 1 && e.shiftKey,
|
||||
dragCallback: (e) => butName(e).middle && e.shiftKey,
|
||||
scrollCallback: () => true,
|
||||
},
|
||||
rotate: {
|
||||
description: 'Middle click + drag',
|
||||
callback: (e) => e.button === 1 && noModifiersPressed(e),
|
||||
callback: (e) => butName(e).middle && noModifiersPressed(e),
|
||||
},
|
||||
},
|
||||
NX: {
|
||||
pan: {
|
||||
description: 'Middle click + Shift + drag',
|
||||
callback: (e) => e.button === 1 && e.shiftKey,
|
||||
callback: (e) => butName(e).middle && e.shiftKey,
|
||||
},
|
||||
zoom: {
|
||||
description: 'Scroll wheel or Middle click + Ctrl + drag',
|
||||
dragCallback: (e) => e.button === 1 && e.ctrlKey,
|
||||
dragCallback: (e) => butName(e).middle && e.ctrlKey,
|
||||
scrollCallback: () => true,
|
||||
},
|
||||
rotate: {
|
||||
description: 'Middle click + drag',
|
||||
callback: (e) => e.button === 1 && noModifiersPressed(e),
|
||||
callback: (e) => butName(e).middle && noModifiersPressed(e),
|
||||
},
|
||||
},
|
||||
Creo: {
|
||||
pan: {
|
||||
description: 'Middle click + Shift + drag',
|
||||
callback: (e) => e.button === 1 && e.shiftKey,
|
||||
callback: (e) => butName(e).middle && e.shiftKey,
|
||||
},
|
||||
zoom: {
|
||||
description: 'Scroll wheel or Middle click + Ctrl + drag',
|
||||
dragCallback: (e) => e.button === 1 && e.ctrlKey,
|
||||
dragCallback: (e) => butName(e).middle && e.ctrlKey,
|
||||
scrollCallback: () => true,
|
||||
},
|
||||
rotate: {
|
||||
description: 'Middle click + drag',
|
||||
callback: (e) => e.button === 1 && noModifiersPressed(e),
|
||||
callback: (e) => butName(e).middle && noModifiersPressed(e),
|
||||
},
|
||||
},
|
||||
AutoCAD: {
|
||||
pan: {
|
||||
description: 'Middle click + drag',
|
||||
callback: (e) => e.button === 1 && noModifiersPressed(e),
|
||||
callback: (e) => butName(e).middle && noModifiersPressed(e),
|
||||
},
|
||||
zoom: {
|
||||
description: 'Scroll wheel',
|
||||
@ -150,7 +156,7 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
|
||||
},
|
||||
rotate: {
|
||||
description: 'Middle click + Shift + drag',
|
||||
callback: (e) => e.button === 1 && e.shiftKey,
|
||||
callback: (e) => butName(e).middle && e.shiftKey,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CommandSetConfig } from 'lib/commandTypes'
|
||||
import { CommandSetConfig, KclCommandValue } from 'lib/commandTypes'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { modelingMachine } from 'machines/modelingMachine'
|
||||
|
||||
@ -14,7 +14,7 @@ export type ModelingCommandSchema = {
|
||||
Extrude: {
|
||||
selection: Selections // & { type: 'face' } would be cool to lock that down
|
||||
// result: (typeof EXTRUSION_RESULTS)[number]
|
||||
distance: number
|
||||
distance: KclCommandValue
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,8 +50,8 @@ export const modelingMachineConfig: CommandSetConfig<
|
||||
// })),
|
||||
// },
|
||||
distance: {
|
||||
inputType: 'number',
|
||||
defaultValue: 5,
|
||||
inputType: 'kcl',
|
||||
defaultValue: '5 + 7',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
@ -7,10 +7,23 @@ import {
|
||||
InterpreterFrom,
|
||||
} from 'xstate'
|
||||
import { Selection } from './selections'
|
||||
import { Identifier, Value, VariableDeclaration } from 'lang/wasm'
|
||||
|
||||
type Icon = CustomIconName
|
||||
const PLATFORMS = ['both', 'web', 'desktop'] as const
|
||||
const INPUT_TYPES = ['options', 'string', 'number', 'selection'] as const
|
||||
const INPUT_TYPES = ['options', 'string', 'kcl', 'selection'] as const
|
||||
export interface KclExpression {
|
||||
valueAst: Value
|
||||
valueText: string
|
||||
valueCalculated: string
|
||||
}
|
||||
export interface KclExpressionWithVariable extends KclExpression {
|
||||
variableName: string
|
||||
variableDeclarationAst: VariableDeclaration
|
||||
variableIdentifierAst: Identifier
|
||||
insertIndex: number
|
||||
}
|
||||
export type KclCommandValue = KclExpression | KclExpressionWithVariable
|
||||
export type CommandInputType = (typeof INPUT_TYPES)[number]
|
||||
|
||||
export type CommandSetSchema<T extends AnyStateMachine> = Partial<{
|
||||
@ -82,20 +95,24 @@ export type CommandArgumentConfig<
|
||||
description?: string
|
||||
required: boolean
|
||||
skip?: true
|
||||
defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
|
||||
} & (
|
||||
| {
|
||||
inputType: Extract<CommandInputType, 'options'>
|
||||
options:
|
||||
| CommandArgumentOption<OutputType>[]
|
||||
| ((context: ContextFrom<T>) => CommandArgumentOption<OutputType>[])
|
||||
defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
|
||||
}
|
||||
| {
|
||||
inputType: Extract<CommandInputType, 'selection'>
|
||||
selectionTypes: Selection['type'][]
|
||||
multiple: boolean
|
||||
}
|
||||
| { inputType: Exclude<CommandInputType, 'options' | 'selection'> }
|
||||
| { inputType: Extract<CommandInputType, 'kcl'>; defaultValue?: string } // KCL expression inputs have simple strings as default values
|
||||
| {
|
||||
inputType: Extract<CommandInputType, 'string'>
|
||||
defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
|
||||
}
|
||||
)
|
||||
|
||||
export type CommandArgument<
|
||||
@ -106,11 +123,11 @@ export type CommandArgument<
|
||||
description?: string
|
||||
required: boolean
|
||||
skip?: true
|
||||
defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
|
||||
} & (
|
||||
| {
|
||||
inputType: Extract<CommandInputType, 'options'>
|
||||
options: CommandArgumentOption<OutputType>[]
|
||||
defaultValue?: OutputType
|
||||
}
|
||||
| {
|
||||
inputType: Extract<CommandInputType, 'selection'>
|
||||
@ -118,7 +135,11 @@ export type CommandArgument<
|
||||
actor: InterpreterFrom<T>
|
||||
multiple: boolean
|
||||
}
|
||||
| { inputType: Exclude<CommandInputType, 'options' | 'selection'> }
|
||||
| { inputType: Extract<CommandInputType, 'kcl'>; defaultValue?: string } // KCL expression inputs have simple strings as default values
|
||||
| {
|
||||
inputType: Extract<CommandInputType, 'string'>
|
||||
defaultValue?: OutputType
|
||||
}
|
||||
)
|
||||
|
||||
export type CommandArgumentWithName<
|
||||
|
15
src/lib/commandUtils.ts
Normal file
@ -0,0 +1,15 @@
|
||||
// Some command argument payloads are objects with a value field that is a KCL expression.
|
||||
// That object also contains some metadata about what to do with the KCL expression,
|
||||
// such as whether we need to create a new variable for it.
|
||||
// This function extracts the value field from those arg payloads and returns
|
||||
// The arg object with all its field as natural values that the command to be executed will expect.
|
||||
export function getCommandArgumentKclValuesOnly(args: Record<string, unknown>) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(args).map(([key, value]) => {
|
||||
if (value !== null && typeof value === 'object' && 'value' in value) {
|
||||
return [key, value.value]
|
||||
}
|
||||
return [key, value]
|
||||
})
|
||||
)
|
||||
}
|
@ -97,7 +97,7 @@ function buildCommandArguments<
|
||||
|
||||
for (const arg in args) {
|
||||
const argConfig = args[arg] as CommandArgumentConfig<S[typeof arg], T>
|
||||
const newArg = buildCommandArgument(argConfig, state, actor)
|
||||
const newArg = buildCommandArgument(argConfig, arg, state, actor)
|
||||
newArgs[arg] = newArg
|
||||
}
|
||||
|
||||
@ -109,6 +109,7 @@ function buildCommandArgument<
|
||||
T extends AnyStateMachine
|
||||
>(
|
||||
arg: CommandArgumentConfig<O, T>,
|
||||
argName: string,
|
||||
state: StateFrom<T>,
|
||||
actor?: InterpreterFrom<T>
|
||||
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
|
||||
@ -116,10 +117,6 @@ function buildCommandArgument<
|
||||
description: arg.description,
|
||||
required: arg.required,
|
||||
skip: arg.skip,
|
||||
defaultValue:
|
||||
arg.defaultValue instanceof Function
|
||||
? arg.defaultValue(state.context)
|
||||
: arg.defaultValue,
|
||||
} satisfies Omit<CommandArgument<O, T>, 'inputType'>
|
||||
|
||||
if (arg.inputType === 'options') {
|
||||
@ -136,6 +133,10 @@ function buildCommandArgument<
|
||||
return {
|
||||
inputType: arg.inputType,
|
||||
...baseCommandArgument,
|
||||
defaultValue:
|
||||
arg.defaultValue instanceof Function
|
||||
? arg.defaultValue(state.context)
|
||||
: arg.defaultValue,
|
||||
options,
|
||||
} satisfies CommandArgument<O, T> & { inputType: 'options' }
|
||||
} else if (arg.inputType === 'selection') {
|
||||
@ -149,9 +150,19 @@ function buildCommandArgument<
|
||||
selectionTypes: arg.selectionTypes,
|
||||
actor,
|
||||
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
|
||||
} else if (arg.inputType === 'kcl') {
|
||||
return {
|
||||
inputType: arg.inputType,
|
||||
defaultValue: arg.defaultValue,
|
||||
...baseCommandArgument,
|
||||
} satisfies CommandArgument<O, T> & { inputType: 'kcl' }
|
||||
} else {
|
||||
return {
|
||||
inputType: arg.inputType,
|
||||
defaultValue:
|
||||
arg.defaultValue instanceof Function
|
||||
? arg.defaultValue(state.context)
|
||||
: arg.defaultValue,
|
||||
...baseCommandArgument,
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
export function isTauri(): boolean {
|
||||
if (typeof window !== 'undefined') {
|
||||
return '__TAURI__' in window
|
||||
if (globalThis.window && typeof globalThis.window !== 'undefined') {
|
||||
return '__TAURI__' in globalThis.window
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
126
src/lib/useCalculateKclExpression.ts
Normal file
@ -0,0 +1,126 @@
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { kclManager, useKclContext } from 'lang/KclSingleton'
|
||||
import { findUniqueName } from 'lang/modifyAst'
|
||||
import { PrevVariable, findAllPreviousVariables } from 'lang/queryAst'
|
||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||
import { Value, parse } from 'lang/wasm'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { executeAst } from 'useStore'
|
||||
|
||||
const isValidVariableName = (name: string) =>
|
||||
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)
|
||||
|
||||
/**
|
||||
* Given a value and a possible variablename,
|
||||
* return helpers for calculating the value and inserting it into the code
|
||||
* as well as information about the variables that are available
|
||||
*/
|
||||
export function useCalculateKclExpression({
|
||||
value,
|
||||
initialVariableName: valueName = '',
|
||||
}: {
|
||||
value: string
|
||||
initialVariableName?: string
|
||||
}): {
|
||||
inputRef: React.RefObject<HTMLInputElement>
|
||||
valueNode: Value | null
|
||||
calcResult: string
|
||||
prevVariables: PrevVariable<unknown>[]
|
||||
newVariableName: string
|
||||
isNewVariableNameUnique: boolean
|
||||
newVariableInsertIndex: number
|
||||
setNewVariableName: (a: string) => void
|
||||
} {
|
||||
const { programMemory } = useKclContext()
|
||||
const { context } = useModelingContext()
|
||||
const selectionRange = context.selectionRanges.codeBasedSelections[0].range
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const [availableVarInfo, setAvailableVarInfo] = useState<
|
||||
ReturnType<typeof findAllPreviousVariables>
|
||||
>({
|
||||
variables: [],
|
||||
insertIndex: 0,
|
||||
bodyPath: [],
|
||||
})
|
||||
const [valueNode, setValueNode] = useState<Value | null>(null)
|
||||
const [calcResult, setCalcResult] = useState('NAN')
|
||||
const [newVariableName, setNewVariableName] = useState('')
|
||||
const [isNewVariableNameUnique, setIsNewVariableNameUnique] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
inputRef.current && inputRef.current.focus()
|
||||
inputRef.current &&
|
||||
inputRef.current.setSelectionRange(0, String(value).length)
|
||||
}, 100)
|
||||
setNewVariableName(findUniqueName(kclManager.ast, valueName))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const allVarNames = Object.keys(programMemory.root)
|
||||
if (
|
||||
allVarNames.includes(newVariableName) ||
|
||||
newVariableName === '' ||
|
||||
!isValidVariableName(newVariableName)
|
||||
) {
|
||||
setIsNewVariableNameUnique(false)
|
||||
} else {
|
||||
setIsNewVariableNameUnique(true)
|
||||
}
|
||||
}, [newVariableName])
|
||||
|
||||
useEffect(() => {
|
||||
if (!programMemory || !selectionRange) return
|
||||
const varInfo = findAllPreviousVariables(
|
||||
kclManager.ast,
|
||||
kclManager.programMemory,
|
||||
selectionRange
|
||||
)
|
||||
setAvailableVarInfo(varInfo)
|
||||
}, [kclManager.ast, kclManager.programMemory, selectionRange])
|
||||
|
||||
useEffect(() => {
|
||||
const execAstAndSetResult = async () => {
|
||||
const code = `const __result__ = ${value}`
|
||||
const ast = parse(code)
|
||||
const _programMem: any = { root: {}, return: null }
|
||||
availableVarInfo.variables.forEach(({ key, value }) => {
|
||||
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
|
||||
})
|
||||
const { programMemory } = await executeAst({
|
||||
ast,
|
||||
engineCommandManager,
|
||||
useFakeExecutor: true,
|
||||
programMemoryOverride: JSON.parse(
|
||||
JSON.stringify(kclManager.programMemory)
|
||||
),
|
||||
})
|
||||
const resultDeclaration = ast.body.find(
|
||||
(a) =>
|
||||
a.type === 'VariableDeclaration' &&
|
||||
a.declarations?.[0]?.id?.name === '__result__'
|
||||
)
|
||||
const init =
|
||||
resultDeclaration?.type === 'VariableDeclaration' &&
|
||||
resultDeclaration?.declarations?.[0]?.init
|
||||
const result = programMemory?.root?.__result__?.value
|
||||
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
|
||||
init && setValueNode(init)
|
||||
}
|
||||
execAstAndSetResult().catch(() => {
|
||||
setCalcResult('NAN')
|
||||
setValueNode(null)
|
||||
})
|
||||
}, [value, availableVarInfo])
|
||||
|
||||
return {
|
||||
valueNode,
|
||||
calcResult,
|
||||
prevVariables: availableVarInfo.variables,
|
||||
newVariableInsertIndex: availableVarInfo.insertIndex,
|
||||
newVariableName,
|
||||
isNewVariableNameUnique,
|
||||
setNewVariableName,
|
||||
inputRef,
|
||||
}
|
||||
}
|
23
src/lib/usePreviousVarMentions.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { CompletionContext } from '@codemirror/autocomplete'
|
||||
import { usePreviousVariables } from './usePreviousVariables'
|
||||
|
||||
/// Basically a fork of the `mentions` extension https://github.com/uiwjs/react-codemirror/blob/master/extensions/mentions/src/index.ts
|
||||
/// But it matches on any word, not just the `@` symbol
|
||||
export function usePreviousVarMentions(context: CompletionContext) {
|
||||
const previousVariables = usePreviousVariables()
|
||||
const data = previousVariables.variables.map((variable) => {
|
||||
return {
|
||||
label: variable.key,
|
||||
detail: variable.value,
|
||||
}
|
||||
})
|
||||
let word = context.matchBefore(/^\w*$/)
|
||||
if (!word) return null
|
||||
if (word && word.from === word.to && !context.explicit) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
from: word?.from!,
|
||||
options: [...data],
|
||||
}
|
||||
}
|
30
src/lib/usePreviousVariables.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { kclManager, useKclContext } from 'lang/KclSingleton'
|
||||
import { findAllPreviousVariables } from 'lang/queryAst'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export function usePreviousVariables() {
|
||||
const { programMemory, code } = useKclContext()
|
||||
const { context } = useModelingContext()
|
||||
const selectionRange = context.selectionRanges.codeBasedSelections[0]
|
||||
?.range || [code.length, code.length]
|
||||
const [previousVariablesInfo, setPreviousVariablesInfo] = useState<
|
||||
ReturnType<typeof findAllPreviousVariables>
|
||||
>({
|
||||
variables: [],
|
||||
insertIndex: 0,
|
||||
bodyPath: [],
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!programMemory || !selectionRange) return
|
||||
const varInfo = findAllPreviousVariables(
|
||||
kclManager.ast,
|
||||
kclManager.programMemory,
|
||||
selectionRange
|
||||
)
|
||||
setPreviousVariablesInfo(varInfo)
|
||||
}, [kclManager.ast, kclManager.programMemory, selectionRange])
|
||||
|
||||
return previousVariablesInfo
|
||||
}
|
28
src/lib/varCompletionExtension.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { Extension } from '@codemirror/state'
|
||||
import {
|
||||
CompletionContext,
|
||||
autocompletion,
|
||||
Completion,
|
||||
} from '@codemirror/autocomplete'
|
||||
|
||||
/// Basically a fork of the `mentions` extension https://github.com/uiwjs/react-codemirror/blob/master/extensions/mentions/src/index.ts
|
||||
/// But it matches on any word, not just the `@` symbol
|
||||
export function varMentions(data: Completion[] = []): Extension {
|
||||
return autocompletion({
|
||||
override: [
|
||||
(context: CompletionContext) => {
|
||||
let word = context.matchBefore(/(\w+)?/)
|
||||
if (!word) return null
|
||||
if (word && word.from === word.to && !context.explicit) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
from: word?.from!,
|
||||
options: [...data],
|
||||
}
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
export const varMentionsView: Extension = [varMentions()]
|
@ -20,6 +20,8 @@ const LOCAL_USER: Models['User_type'] = {
|
||||
phone: '555-555-5555',
|
||||
first_name: 'Test',
|
||||
last_name: 'User',
|
||||
can_train_on_data: false,
|
||||
is_service_account: false,
|
||||
}
|
||||
|
||||
export interface UserContext {
|
||||
|
@ -3,12 +3,14 @@ import {
|
||||
Command,
|
||||
CommandArgument,
|
||||
CommandArgumentWithName,
|
||||
KclCommandValue,
|
||||
} from 'lib/commandTypes'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
|
||||
|
||||
export const commandBarMachine = createMachine(
|
||||
{
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswdJNWcNbPFLNMr5AsRFWUtJcVSdRR2VVMUmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUG1klTsSi0awQDgYWhmqkWjnUslBWhOZyegU61GuZAAghACN8-HhYH92FxAXFQAlRA5FpJ5FjFPYtNDpIpLOkEW4GNs4eJxJoGvJxOVcT82gSrj0AEpgdCoABuYC+8ogNOYI3psQmYlkGUkU2slhc6mkmUUCIyKLc4i0lkU8iUyjUHLlVIuJEkAGUwK8Pg8oDr-WQQ2HPpTzrTIkagUzEDkLHZkQK1CUtKCHAiNlsdjkVAdFEc-ed2oG8W0Xu9uD0kwDjcCEJDplUPfn+Ut7CUhXJJFo6uYbBOMtJq-jLrrnqGmy2HOE6dEGSbESoRSUHOVqpV99Zh7JR+PXPu1G7zI1vKcFwSAOJYbgACzAJAj+FIUAAruggzcLAFBvrgMBfH+JAkEBP4kP+gE4NwrYpoywiIA4qjyDIyR2LmlTSHY8LGBhyjsrm-L2No0hpHys4Kh0L7vp+36-gBQEgQAInAS4fFGiYGv8qFbjkpSWLmswMNK-LOI6UmSKouZOBo8jOPu9EBpITEfl+OCRmxiHAZIJIAO5YDEen4A8bB-twMZ-gARugPBwQhQEoRu7ZpoiyRbLm1HiJC16bLIQo7FYUrVNkKhZJ4971pp2ksZZBkcZIABqWCUJwECvhGZAQPwYCSA8GqoAA1sVGpZTlr5gCS8HsUhHljGhCTODYVissoxaWPutQInyFhctKSL8jFmwabWWmvjprGNYZsAZTVuW8HpZCfiQqCBmwlCvgAZtt6CSNV2WrfVC3uYJ66tVuqQlNsaTpKkUlCrMClqNeuibHUolTQSqoapwYAmfZTkuQmvzXcmnmpuhCB9eyOieuk1o6C4DokQjZHqKjO66C6-0dIDwOg2SBCpc10NtnDCTiqU0ICjyciyG4+RYwKpQaBmSKpE4dpE4GJMg2QqrqlqrlNch1PCR2vNSLa4rZq4eaDVyo4+jymRqJWDQ4vFj7E2AQMiwAohALmU9La4w7dHaaNhNjaBKnqsqCatqBrdTirMNiQuIgudB+bzld+DVuUhIGFTgxWlRVVUrXV4dS-qNs021iDyO9TslMoTj1LJWN6BYOsyphOhmDkcXNP603IMHoeWcni0FUVJU4GVlUnYnzbNxxdCroasMZwgWKPay0qWNYLhZ+UCIuGeyR6O47o0ZsAcG7XioN2Hl2Rxt0HbZIu0HUd3dnUne-AS1m5y+UWz7DkY7pKpsKDRoMz1MK4olO4G-3jgVAEA4CCASrWIedtvKiAWBaBgmQ6g2g0PaR00xWYSkWB6coHI16ByVBACBt8oHFAtHma0qR6iemlKsLGHppg61cJWDY+wzRqEDrGJs35IZ4AIV5eGswGabG9FJPqdRQo0NUqiBhmESjpFZrghcjYPiQB4bTEEU8NbImyFiaQ1hCw0M2Eja8mEVBmn5KCQOSVdL6SvvAISw87qzDPDKBQ-IdB8npkWH0FoJR8lcOKKeWgHAWNmslaxEcjKmXMmtSM1lbIqJHvuQi2x-KuPMPsQKb1HaBT6roWwqlNDBOYlYyWi1loX2ifEu6lQpDWlUNaHRsxUjSCFBsGY1TdB2i5BkBQgdhYmUqfbTI4JPr9lUHIdIxFCi6M-h6bBWcMz6xrjWbe7xG6Rj7pHAZ3lUgSLcD6NIApFj7jEVMrICkbA0VLjyHWd4vBAA */
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswdJNWcNbPFLNMr5AsRFWUtJcVSdRR2VVMUmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUG1klTsSi0awQDgYWhmqkWjnUslBWhOZyegU61GuZAAghACN8-HhYH92FxAXFQAlRA5FpJ5FjFPYtNDpIpLOkEW4GNs4eJxJoGvJxOVcT82gSrj0AEpgdCoABuYC+8ogNOYI3psQmYlkGUkU2slhc6mkmUUCIyKLc4i0lkU8iUyjUHLlVIuJEkAGUwK8Pg8oDr-WQQ2HPpTzrTIkagUzEDkLHZkQK1CUtKCHAiNlsdjkVAdFEc-ed2oG8W0Xu9uD0kwDjcCEJDplUPfn+Ut7CUhXJJFo6uYbBOMtJq-jLrrnqGmy2HOE6dEGSbESoRSUHOVqpV99Zh7JR+PXPu1G7zI1vKcFwSAOJYbgACzAJAj+FIUAAruggzcLAFBvrgMBfH+JAkEBP4kP+gE4NwrYpoywiIA4qjyDIyR2LmlTSHY8LGBhyjsrm-L2No0hpHys4Kh0L7vp+36-gBQEgQAInAS4fFGiYGv8qFbjkpSWLmswMNK-LOI6UmSKouZOBo8jOPu9EBpITEfl+OCRmxiHAZIJIAO5YDEen4A8bB-twMZ-gARugPBwQhQEoRu7ZpoiyRbLm1HiJC16bLIQo7FYUrVNkKhZJ4971pp2ksZZBkcZIABqWCUJwECvhGZAQPwYCSA8GqoAA1sVGpZTlr5gCS8HsUhHljGhCTODYVissoxaWPutQInyFhctKSL8jFmwabWWmvjprGNYZsAZTVuW8HpZCfiQqCBmwlCvgAZtt6CSNV2WrfVC3uYJ66tVuqQlNsaTpKkUlCrMClqNeuibHUolTQSqoapwYAmfZTkuQmvzXcmnmpuhCB9eyOieuk1o6C4DokQjZHqKjO66C6-0dIDwOg2SBCpc10NtnDCTiqU0ICjyciyG4+RYwKpQaBmSKpE4dpE4GJMg2QqrqlqrlNch1PCR2vNSLa4rZq4eaDVyo4+jymRqJWDQ4vFj7E2AQMiwAohALmU9La4w7dHaaNhNjaBKnqsqCatqBrdTirMNiQuIgudB+bzld+DVuUhIGFTgxWlRVVUrXV4dS-qNs021iDyO9TslMoTj1LJWN6BYOsyphOhmDkcXNP603IMHoeWcni0FUVJU4GVlUnYnzbNxxdCroasMZwgWKPay0qWNYLhZ+UCIuGeyR6O47o0ZsAcG7XioN2Hl2Rxt0HbZIu0HUd3dnUne-AS1m5y+UWz7DkY7pKpsKDRoMz1MK4olO4G-3jgVAEA4CCASrWIedtvKiAWBaBgmQ6g2g0PaR00xWYSjHFPHQNFCIzk3jWRURJIAQNvlA4oFo8zWlSPUT00pVhYw9NMHWrhKwbH2GaNQgdYxNm-JDPAxCvLw1mAzTY3opJ9TqKFehqlUTMMwiUdIrNA5gMbB8IhQlh5bkWFsVwyJshYmkNYQs9DNhI2vJhFQZp+SgkDklXS+kr7wHUZA+G-UzwygUPyLB+xApFh9BaCU6hpSLGqNKDheC5yBlsfNCORlTLmTWpGaytl+G0wwhoEU7ilDWnMN4zGhRPqO0Cn1XQthVKaBsbNZK9iYlLUyhfBJKSR7OH2FYAi1oDGzFSNIIUGwZiVD0BKTCSkFCB2FiZRpIlMjgk+v2VQch0jEUKIYz+HoOSaA0IeJRO8m4OImXLTQjDYRpAFIsfckillZAUjYGipceQ6zvF4IAA */
|
||||
context: {
|
||||
commands: [] as Command[],
|
||||
selectedCommand: undefined as Command | undefined,
|
||||
@ -143,7 +145,7 @@ export const commandBarMachine = createMachine(
|
||||
'Change current argument': {
|
||||
target: 'Gathering arguments',
|
||||
internal: true,
|
||||
actions: ['Set current argument'],
|
||||
actions: ['Remove current argument and set a new one'],
|
||||
},
|
||||
|
||||
'Deselect command': {
|
||||
@ -172,17 +174,7 @@ export const commandBarMachine = createMachine(
|
||||
|
||||
'Remove argument': {
|
||||
target: 'Review',
|
||||
actions: [
|
||||
assign({
|
||||
argumentsToSubmit: (context, event) => {
|
||||
const argName = Object.keys(event.data)[0]
|
||||
const { argumentsToSubmit } = context
|
||||
const newArgumentsToSubmit = { ...argumentsToSubmit }
|
||||
newArgumentsToSubmit[argName] = undefined
|
||||
return newArgumentsToSubmit
|
||||
},
|
||||
}),
|
||||
],
|
||||
actions: ['Remove argument'],
|
||||
},
|
||||
|
||||
'Edit argument': {
|
||||
@ -272,7 +264,7 @@ export const commandBarMachine = createMachine(
|
||||
}
|
||||
| {
|
||||
type: 'Change current argument'
|
||||
data: { arg: CommandArgumentWithName<unknown> }
|
||||
data: { [x: string]: CommandArgumentWithName<unknown> }
|
||||
},
|
||||
},
|
||||
predictableActionArguments: true,
|
||||
@ -283,19 +275,17 @@ export const commandBarMachine = createMachine(
|
||||
'Execute command': (context, event) => {
|
||||
const { selectedCommand } = context
|
||||
if (!selectedCommand) return
|
||||
if (selectedCommand?.args) {
|
||||
selectedCommand?.onSubmit(
|
||||
event.type === 'Submit command' ||
|
||||
event.type === 'done.invoke.validateArguments'
|
||||
? event.data
|
||||
: undefined
|
||||
)
|
||||
if (
|
||||
(selectedCommand?.args && event.type === 'Submit command') ||
|
||||
event.type === 'done.invoke.validateArguments'
|
||||
) {
|
||||
selectedCommand?.onSubmit(getCommandArgumentKclValuesOnly(event.data))
|
||||
} else {
|
||||
selectedCommand?.onSubmit()
|
||||
}
|
||||
},
|
||||
'Set current argument to first non-skippable': assign({
|
||||
currentArgument: (context, event) => {
|
||||
currentArgument: (context) => {
|
||||
const { selectedCommand } = context
|
||||
if (!(selectedCommand && selectedCommand.args)) return undefined
|
||||
|
||||
@ -331,6 +321,15 @@ export const commandBarMachine = createMachine(
|
||||
'Clear current argument': assign({
|
||||
currentArgument: undefined,
|
||||
}),
|
||||
'Remove argument': assign({
|
||||
argumentsToSubmit: (context, event) => {
|
||||
if (event.type !== 'Remove argument') return context.argumentsToSubmit
|
||||
const argToRemove = Object.values(event.data)[0]
|
||||
// Extract all but the argument to remove and return it
|
||||
const { [argToRemove.name]: _, ...rest } = context.argumentsToSubmit
|
||||
return rest
|
||||
},
|
||||
}),
|
||||
'Set current argument': assign({
|
||||
currentArgument: (context, event) => {
|
||||
switch (event.type) {
|
||||
@ -338,13 +337,34 @@ export const commandBarMachine = createMachine(
|
||||
return event.data.arg
|
||||
case 'Edit argument':
|
||||
return event.data.arg
|
||||
case 'Change current argument':
|
||||
return event.data.arg
|
||||
default:
|
||||
return context.currentArgument
|
||||
}
|
||||
},
|
||||
}),
|
||||
'Remove current argument and set a new one': assign({
|
||||
currentArgument: (context, event) => {
|
||||
if (event.type !== 'Change current argument')
|
||||
return context.currentArgument
|
||||
return Object.values(event.data)[0]
|
||||
},
|
||||
argumentsToSubmit: (context, event) => {
|
||||
if (
|
||||
event.type !== 'Change current argument' ||
|
||||
!context.currentArgument
|
||||
)
|
||||
return context.argumentsToSubmit
|
||||
const { name, required } = context.currentArgument
|
||||
if (required)
|
||||
return {
|
||||
[name]: undefined,
|
||||
...context.argumentsToSubmit,
|
||||
}
|
||||
|
||||
const { [name]: _, ...rest } = context.argumentsToSubmit
|
||||
return rest
|
||||
},
|
||||
}),
|
||||
'Clear argument data': assign({
|
||||
selectedCommand: undefined,
|
||||
currentArgument: undefined,
|
||||
@ -378,7 +398,8 @@ export const commandBarMachine = createMachine(
|
||||
if (!command.args) return {}
|
||||
const args: { [x: string]: unknown } = {}
|
||||
for (const [argName, arg] of Object.entries(command.args)) {
|
||||
args[argName] = arg.skip ? arg.defaultValue : undefined
|
||||
args[argName] =
|
||||
arg.skip && 'defaultValue' in arg ? arg.defaultValue : undefined
|
||||
}
|
||||
return args
|
||||
},
|
||||
@ -406,8 +427,12 @@ export const commandBarMachine = createMachine(
|
||||
let argConfig = context.selectedCommand!.args![argName]
|
||||
|
||||
if (
|
||||
(argConfig.defaultValue &&
|
||||
typeof arg !== typeof argConfig.defaultValue) ||
|
||||
('defaultValue' in argConfig &&
|
||||
argConfig.defaultValue &&
|
||||
typeof arg !== typeof argConfig.defaultValue &&
|
||||
argConfig.inputType !== 'kcl') ||
|
||||
(argConfig.inputType === 'kcl' &&
|
||||
!(arg as Partial<KclCommandValue>).valueAst) ||
|
||||
('options' in argConfig &&
|
||||
typeof arg !== typeof argConfig.options[0].value)
|
||||
) {
|
||||
|