Compare commits
33 Commits
parser-in-
...
v0.19.1
Author | SHA1 | Date | |
---|---|---|---|
a3eeff65c8 | |||
fab3d2b130 | |||
0a96dc6fd2 | |||
e123a00d4b | |||
b950cc0583 | |||
c89780a489 | |||
1afed68dd7 | |||
dcbed4f06f | |||
379f154a5c | |||
60c4969322 | |||
cc6dee8ad4 | |||
2fc7c0d5fd | |||
bf2dcd808f | |||
ee21e486d4 | |||
b5a3eb9e9c | |||
c85645c9f2 | |||
cfa4dd2e33 | |||
c620f7269c | |||
2d8d29b345 | |||
00da062586 | |||
aafbaf6c50 | |||
2894c84a4e | |||
c01084feb0 | |||
c461db5f54 | |||
03fcb73aca | |||
8065e7e51a | |||
2d0ac249df | |||
3d73b82c23 | |||
0b235dc1cd | |||
0857415793 | |||
1da4fd03ef | |||
39d84c12ab | |||
537d86c8ff |
35
.github/workflows/build-and-store-wasm.yml
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
name: Build and Store WASM
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-upload:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
cache: 'yarn'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn
|
||||||
|
- name: Install Playwright Browsers
|
||||||
|
run: yarn playwright install --with-deps
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
- name: Cache wasm
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: './src/wasm-lib'
|
||||||
|
- name: build wasm
|
||||||
|
run: yarn build:wasm
|
||||||
|
|
||||||
|
|
||||||
|
# Upload the WASM bundle as an artifact
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: wasm-bundle
|
||||||
|
path: src/wasm-lib/pkg
|
17
.github/workflows/cargo-clippy.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
dir: ['src/wasm-lib']
|
dir: ['src/wasm-lib', 'src-tauri']
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install latest rust
|
- name: Install latest rust
|
||||||
@ -31,9 +31,22 @@ jobs:
|
|||||||
|
|
||||||
- name: install dependencies
|
- name: install dependencies
|
||||||
if: matrix.dir == 'src-tauri'
|
if: matrix.dir == 'src-tauri'
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
sudo apt-get install -y \
|
||||||
|
libgtk-3-dev \
|
||||||
|
libayatana-appindicator3-dev \
|
||||||
|
webkit2gtk-driver \
|
||||||
|
libsoup-3.0-dev \
|
||||||
|
libjavascriptcoregtk-4.1-dev \
|
||||||
|
libwebkit2gtk-4.1-dev \
|
||||||
|
at-spi2-core \
|
||||||
|
xvfb
|
||||||
|
yarn install
|
||||||
|
yarn build:wasm
|
||||||
|
yarn build:local
|
||||||
|
|
||||||
- name: Rust Cache
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2.6.1
|
uses: Swatinem/rust-cache@v2.6.1
|
||||||
|
|
||||||
|
57
.github/workflows/cargo-test-tauri.yml
vendored
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'src-tauri/**.rs'
|
||||||
|
- '**/Cargo.toml'
|
||||||
|
- '**/Cargo.lock'
|
||||||
|
- '**/rust-toolchain.toml'
|
||||||
|
- .github/workflows/cargo-test-tauri.yml
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'src-tauri/**.rs'
|
||||||
|
- '**/Cargo.toml'
|
||||||
|
- '**/Cargo.lock'
|
||||||
|
- '**/rust-toolchain.toml'
|
||||||
|
- .github/workflows/cargo-test-tauri.yml
|
||||||
|
workflow_dispatch:
|
||||||
|
permissions: read-all
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
name: cargo test of tauri
|
||||||
|
jobs:
|
||||||
|
cargotest:
|
||||||
|
name: cargo test
|
||||||
|
runs-on: ubuntu-latest-8-cores
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
dir: ['src-tauri']
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install latest rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
- name: install dependencies
|
||||||
|
if: matrix.dir == 'src-tauri'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
libgtk-3-dev \
|
||||||
|
libayatana-appindicator3-dev \
|
||||||
|
webkit2gtk-driver \
|
||||||
|
libsoup-3.0-dev \
|
||||||
|
libjavascriptcoregtk-4.1-dev \
|
||||||
|
libwebkit2gtk-4.1-dev \
|
||||||
|
at-spi2-core \
|
||||||
|
xvfb
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2.6.1
|
||||||
|
- name: cargo test
|
||||||
|
shell: bash
|
||||||
|
run: |-
|
||||||
|
cd "${{ matrix.dir }}"
|
||||||
|
cargo test --all
|
22
.github/workflows/ci.yml
vendored
@ -147,17 +147,17 @@ jobs:
|
|||||||
|
|
||||||
- name: Install ubuntu system dependencies
|
- name: Install ubuntu system dependencies
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: >
|
run: |
|
||||||
sudo apt-get update &&
|
sudo apt-get update
|
||||||
sudo apt-get install -y
|
sudo apt-get install -y \
|
||||||
libgtk-3-dev
|
libgtk-3-dev \
|
||||||
libayatana-appindicator3-dev
|
libayatana-appindicator3-dev \
|
||||||
webkit2gtk-driver
|
webkit2gtk-driver \
|
||||||
libsoup-3.0-dev
|
libsoup-3.0-dev \
|
||||||
libjavascriptcoregtk-4.1-dev
|
libjavascriptcoregtk-4.1-dev \
|
||||||
libwebkit2gtk-4.1-dev
|
libwebkit2gtk-4.1-dev \
|
||||||
at-spi2-core
|
at-spi2-core \
|
||||||
xvfb
|
xvfb
|
||||||
|
|
||||||
- name: Sync node version and setup cache
|
- name: Sync node version and setup cache
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
5
.github/workflows/playwright.yml
vendored
@ -122,3 +122,8 @@ jobs:
|
|||||||
name: playwright-report
|
name: playwright-report
|
||||||
path: playwright-report/
|
path: playwright-report/
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
with:
|
||||||
|
name: wasm-bundle
|
||||||
|
path: src/wasm-lib/pkg
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
presets: ["@babel/preset-env"],
|
presets: ['@babel/preset-env'],
|
||||||
}
|
}
|
||||||
|
7410
docs/kcl/std.json
@ -104,6 +104,7 @@ test('Basic sketch', async ({ page }) => {
|
|||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([0, ${commonPoints.num1}], %)`)
|
|> line([0, ${commonPoints.num1}], %)`)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
@ -328,9 +329,7 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
|||||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
/* Ignore this test for now since its causing engine to crash
|
test('if your kcl gets an error from the engine it is inlined', async ({
|
||||||
*
|
|
||||||
* test('if your kcl gets an error from the engine it is inlined', async ({
|
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const u = getUtils(page)
|
const u = getUtils(page)
|
||||||
@ -349,7 +348,7 @@ const sketch001 = startSketchOn(box, "revolveAxis")
|
|||||||
|> startProfileAt([5, 10], %)
|
|> startProfileAt([5, 10], %)
|
||||||
|> line([0, -10], %)
|
|> line([0, -10], %)
|
||||||
|> line([2, 0], %)
|
|> line([2, 0], %)
|
||||||
|> line([0, 10], %)
|
|> line([0, -10], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> revolve({
|
|> revolve({
|
||||||
axis: getEdge('revolveAxis', box),
|
axis: getEdge('revolveAxis', box),
|
||||||
@ -364,7 +363,7 @@ angle: 90
|
|||||||
|
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
@ -378,7 +377,7 @@ angle: 90
|
|||||||
'sketch profile must lie entirely on one side of the revolution axis'
|
'sketch profile must lie entirely on one side of the revolution axis'
|
||||||
)
|
)
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
})*/
|
})
|
||||||
|
|
||||||
test('executes on load', async ({ page }) => {
|
test('executes on load', async ({ page }) => {
|
||||||
const u = getUtils(page)
|
const u = getUtils(page)
|
||||||
@ -566,7 +565,9 @@ test('Auto complete works', async ({ page }) => {
|
|||||||
|
|
||||||
await page.keyboard.press('Tab')
|
await page.keyboard.press('Tab')
|
||||||
await page.keyboard.type('12')
|
await page.keyboard.type('12')
|
||||||
|
await page.waitForTimeout(100)
|
||||||
await page.keyboard.press('Tab')
|
await page.keyboard.press('Tab')
|
||||||
|
await page.waitForTimeout(100)
|
||||||
await page.keyboard.press('Tab')
|
await page.keyboard.press('Tab')
|
||||||
await page.keyboard.press('Tab')
|
await page.keyboard.press('Tab')
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
@ -595,13 +596,12 @@ test('Auto complete works', async ({ page }) => {
|
|||||||
|
|
||||||
test('Stored settings are validated and fall back to defaults', async ({
|
test('Stored settings are validated and fall back to defaults', async ({
|
||||||
page,
|
page,
|
||||||
context,
|
|
||||||
}) => {
|
}) => {
|
||||||
const u = getUtils(page)
|
const u = getUtils(page)
|
||||||
|
|
||||||
// Override beforeEach test setup
|
// Override beforeEach test setup
|
||||||
// with corrupted settings
|
// with corrupted settings
|
||||||
await context.addInitScript(
|
await page.addInitScript(
|
||||||
async ({ settingsKey, settings }) => {
|
async ({ settingsKey, settings }) => {
|
||||||
localStorage.setItem(settingsKey, settings)
|
localStorage.setItem(settingsKey, settings)
|
||||||
},
|
},
|
||||||
@ -618,18 +618,18 @@ test('Stored settings are validated and fall back to defaults', async ({
|
|||||||
// Check the settings were reset
|
// Check the settings were reset
|
||||||
const storedSettings = TOML.parse(
|
const storedSettings = TOML.parse(
|
||||||
await page.evaluate(
|
await page.evaluate(
|
||||||
({ settingsKey }) => localStorage.getItem(settingsKey) || '{}',
|
({ settingsKey }) => localStorage.getItem(settingsKey) || '',
|
||||||
{ settingsKey: TEST_SETTINGS_KEY }
|
{ settingsKey: TEST_SETTINGS_KEY }
|
||||||
)
|
)
|
||||||
) as { settings: SaveSettingsPayload }
|
) as { settings: SaveSettingsPayload }
|
||||||
|
|
||||||
expect(storedSettings.settings.app?.theme).toBe('dark')
|
expect(storedSettings.settings?.app?.theme).toBe(undefined)
|
||||||
|
|
||||||
// Check that the invalid settings were removed
|
// Check that the invalid settings were removed
|
||||||
expect(storedSettings.settings.modeling?.defaultUnit).toBe(undefined)
|
expect(storedSettings.settings?.modeling?.defaultUnit).toBe(undefined)
|
||||||
expect(storedSettings.settings.modeling?.mouseControls).toBe(undefined)
|
expect(storedSettings.settings?.modeling?.mouseControls).toBe(undefined)
|
||||||
expect(storedSettings.settings.app?.projectDirectory).toBe(undefined)
|
expect(storedSettings.settings?.app?.projectDirectory).toBe(undefined)
|
||||||
expect(storedSettings.settings.projects?.defaultProjectName).toBe(undefined)
|
expect(storedSettings.settings?.projects?.defaultProjectName).toBe(undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Project settings can be set and override user settings', async ({
|
test('Project settings can be set and override user settings', async ({
|
||||||
@ -736,7 +736,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
|||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
|
|
||||||
const xAxisClick = () =>
|
const xAxisClick = () =>
|
||||||
page.mouse.click(700, 250).then(() => page.waitForTimeout(100))
|
page.mouse.click(700, 253).then(() => page.waitForTimeout(100))
|
||||||
const emptySpaceClick = () =>
|
const emptySpaceClick = () =>
|
||||||
page.mouse.click(728, 343).then(() => page.waitForTimeout(100))
|
page.mouse.click(728, 343).then(() => page.waitForTimeout(100))
|
||||||
const topHorzSegmentClick = () =>
|
const topHorzSegmentClick = () =>
|
||||||
@ -761,6 +761,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
|||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)`)
|
|> startProfileAt(${commonPoints.startAt}, %)`)
|
||||||
|
|
||||||
|
await page.waitForTimeout(100)
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
@ -768,12 +769,14 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
|||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)`)
|
|> line([${commonPoints.num1}, 0], %)`)
|
||||||
|
|
||||||
|
await page.waitForTimeout(100)
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([0, ${commonPoints.num1}], %)`)
|
|> line([0, ${commonPoints.num1}], %)`)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
@ -786,10 +789,14 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
|||||||
await page.getByRole('button', { name: 'Line' }).click()
|
await page.getByRole('button', { name: 'Line' }).click()
|
||||||
|
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
const selectionSequence = async () => {
|
const selectionSequence = async (isSecondTime = false) => {
|
||||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||||
|
|
||||||
await page.mouse.move(startXPx + PUR * 15, 500 - PUR * 10)
|
await page.waitForTimeout(100)
|
||||||
|
await page.mouse.move(
|
||||||
|
startXPx + PUR * 15,
|
||||||
|
isSecondTime ? 430 : 500 - PUR * 10
|
||||||
|
)
|
||||||
|
|
||||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||||
// bg-yellow-200 is more brittle than hover-highlight, but is closer to the user experience
|
// bg-yellow-200 is more brittle than hover-highlight, but is closer to the user experience
|
||||||
@ -799,7 +806,10 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
|||||||
// check mousing off, than mousing onto another line
|
// check mousing off, than mousing onto another line
|
||||||
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 15) // mouse off
|
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 15) // mouse off
|
||||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||||
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 20) // mouse onto another line
|
await page.mouse.move(
|
||||||
|
startXPx + PUR * 10,
|
||||||
|
isSecondTime ? 295 : 500 - PUR * 20
|
||||||
|
) // mouse onto another line
|
||||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||||
|
|
||||||
// now check clicking works including axis
|
// now check clicking works including axis
|
||||||
@ -809,6 +819,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
|||||||
await page.keyboard.down('Shift')
|
await page.keyboard.down('Shift')
|
||||||
const absYButton = page.getByRole('button', { name: 'ABS Y' })
|
const absYButton = page.getByRole('button', { name: 'ABS Y' })
|
||||||
await expect(absYButton).toBeDisabled()
|
await expect(absYButton).toBeDisabled()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
await xAxisClick()
|
await xAxisClick()
|
||||||
await page.keyboard.up('Shift')
|
await page.keyboard.up('Shift')
|
||||||
await absYButton.and(page.locator(':not([disabled])')).waitFor()
|
await absYButton.and(page.locator(':not([disabled])')).waitFor()
|
||||||
@ -817,10 +828,12 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
|||||||
// clear selection by clicking on nothing
|
// clear selection by clicking on nothing
|
||||||
await emptySpaceClick()
|
await emptySpaceClick()
|
||||||
|
|
||||||
|
await page.waitForTimeout(100)
|
||||||
// same selection but click the axis first
|
// same selection but click the axis first
|
||||||
await xAxisClick()
|
await xAxisClick()
|
||||||
await expect(absYButton).toBeDisabled()
|
await expect(absYButton).toBeDisabled()
|
||||||
await page.keyboard.down('Shift')
|
await page.keyboard.down('Shift')
|
||||||
|
await page.waitForTimeout(100)
|
||||||
await topHorzSegmentClick()
|
await topHorzSegmentClick()
|
||||||
|
|
||||||
await page.keyboard.up('Shift')
|
await page.keyboard.up('Shift')
|
||||||
@ -833,6 +846,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
|||||||
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click()
|
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click()
|
||||||
await page.keyboard.down('Shift')
|
await page.keyboard.down('Shift')
|
||||||
await expect(absYButton).toBeDisabled()
|
await expect(absYButton).toBeDisabled()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
await xAxisClick()
|
await xAxisClick()
|
||||||
await page.keyboard.up('Shift')
|
await page.keyboard.up('Shift')
|
||||||
await expect(absYButton).not.toBeDisabled()
|
await expect(absYButton).not.toBeDisabled()
|
||||||
@ -875,11 +889,16 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
|||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
// enter sketch again
|
// enter sketch again
|
||||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
await u.doAndWaitForCmd(
|
||||||
|
() => page.getByRole('button', { name: 'Edit Sketch' }).click(),
|
||||||
|
'default_camera_get_settings'
|
||||||
|
)
|
||||||
|
await page.waitForTimeout(150)
|
||||||
|
|
||||||
await page.waitForTimeout(300) // wait for animation
|
await page.waitForTimeout(300) // wait for animation
|
||||||
|
|
||||||
// hover again and check it works
|
// hover again and check it works
|
||||||
await selectionSequence()
|
await selectionSequence(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test.describe('Command bar tests', () => {
|
test.describe('Command bar tests', () => {
|
||||||
@ -1015,6 +1034,7 @@ const part001 = startSketchOn('-XZ')
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('Can add multiple sketches', async ({ page }) => {
|
test('Can add multiple sketches', async ({ page }) => {
|
||||||
|
test.skip(process.platform === 'darwin', 'Can add multiple sketches')
|
||||||
const u = getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
@ -1065,6 +1085,7 @@ test('Can add multiple sketches', async ({ page }) => {
|
|||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([0, ${commonPoints.num1}], %)`)
|
|> line([0, ${commonPoints.num1}], %)`)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||||
const finalCodeFirstSketch = `const part001 = startSketchOn('-XZ')
|
const finalCodeFirstSketch = `const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
@ -1080,24 +1101,33 @@ test('Can add multiple sketches', async ({ page }) => {
|
|||||||
|
|
||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
|
||||||
await u.updateCamPosition([0, 100, 100])
|
await u.updateCamPosition([100, 100, 100])
|
||||||
|
await page.waitForTimeout(250)
|
||||||
|
|
||||||
// start a new sketch
|
// start a new sketch
|
||||||
await u.clearCommandLogs()
|
await u.clearCommandLogs()
|
||||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(400)
|
||||||
await page.mouse.click(673, 384)
|
await page.mouse.click(650, 450)
|
||||||
|
|
||||||
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||||
await u.clearAndCloseDebugPanel()
|
await u.clearAndCloseDebugPanel()
|
||||||
|
|
||||||
|
// on mock os there are issues with getting the camera to update
|
||||||
|
// it should not be selecting the 'XZ' plane here if the camera updated
|
||||||
|
// properly, but if we just role with it we can still verify everything
|
||||||
|
// in the rest of the test
|
||||||
|
const plane = process.platform === 'darwin' ? 'XZ' : 'XY'
|
||||||
|
|
||||||
|
await page.waitForTimeout(100)
|
||||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
const startAt2 = '[0.93,-1.25]'
|
const startAt2 =
|
||||||
|
process.platform === 'darwin' ? '[9.75, -13.16]' : '[0.93, -1.25]'
|
||||||
await expect(
|
await expect(
|
||||||
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
||||||
).toBe(
|
).toBe(
|
||||||
`${finalCodeFirstSketch}
|
`${finalCodeFirstSketch}
|
||||||
const part002 = startSketchOn('XY')
|
const part002 = startSketchOn('${plane}')
|
||||||
|> startProfileAt(${startAt2}, %)`.replace(/\s/g, '')
|
|> startProfileAt(${startAt2}, %)`.replace(/\s/g, '')
|
||||||
)
|
)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
@ -1106,12 +1136,12 @@ const part002 = startSketchOn('XY')
|
|||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
const num2 = 0.94
|
const num2 = process.platform === 'darwin' ? 9.84 : 0.94
|
||||||
await expect(
|
await expect(
|
||||||
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
||||||
).toBe(
|
).toBe(
|
||||||
`${finalCodeFirstSketch}
|
`${finalCodeFirstSketch}
|
||||||
const part002 = startSketchOn('XY')
|
const part002 = startSketchOn('${plane}')
|
||||||
|> startProfileAt(${startAt2}, %)
|
|> startProfileAt(${startAt2}, %)
|
||||||
|> line([${num2}, 0], %)`.replace(/\s/g, '')
|
|> line([${num2}, 0], %)`.replace(/\s/g, '')
|
||||||
)
|
)
|
||||||
@ -1121,21 +1151,29 @@ const part002 = startSketchOn('XY')
|
|||||||
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
||||||
).toBe(
|
).toBe(
|
||||||
`${finalCodeFirstSketch}
|
`${finalCodeFirstSketch}
|
||||||
const part002 = startSketchOn('XY')
|
const part002 = startSketchOn('${plane}')
|
||||||
|> startProfileAt(${startAt2}, %)
|
|> startProfileAt(${startAt2}, %)
|
||||||
|> line([${num2}, 0], %)
|
|> line([${num2}, 0], %)
|
||||||
|> line([0, ${roundOff(num2 - 0.01)}], %)`.replace(/\s/g, '')
|
|> line([0, ${roundOff(
|
||||||
|
num2 + (process.platform === 'darwin' ? 0.01 : -0.01)
|
||||||
|
)}], %)`.replace(/\s/g, '')
|
||||||
)
|
)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||||
await expect(
|
await expect(
|
||||||
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
||||||
).toBe(
|
).toBe(
|
||||||
`${finalCodeFirstSketch}
|
`${finalCodeFirstSketch}
|
||||||
const part002 = startSketchOn('XY')
|
const part002 = startSketchOn('${plane}')
|
||||||
|> startProfileAt(${startAt2}, %)
|
|> startProfileAt(${startAt2}, %)
|
||||||
|> line([${num2}, 0], %)
|
|> line([${num2}, 0], %)
|
||||||
|> line([0, ${roundOff(num2 - 0.01)}], %)
|
|> line([0, ${roundOff(
|
||||||
|> line([-1.87, 0], %)`.replace(/\s/g, '')
|
num2 + (process.platform === 'darwin' ? 0.01 : -0.01)
|
||||||
|
)}], %)
|
||||||
|
|> line([-${process.platform === 'darwin' ? 19.59 : 1.87}, 0], %)`.replace(
|
||||||
|
/\s/g,
|
||||||
|
''
|
||||||
|
)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1339,10 +1377,12 @@ test('Deselecting line tool should mean nothing happens on click', async ({
|
|||||||
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||||
previousCodeContent = await page.locator('.cm-content').innerText()
|
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
|
await page.waitForTimeout(100)
|
||||||
await page.mouse.click(700, 300)
|
await page.mouse.click(700, 300)
|
||||||
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||||
previousCodeContent = await page.locator('.cm-content').innerText()
|
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
|
await page.waitForTimeout(100)
|
||||||
await page.mouse.click(750, 300)
|
await page.mouse.click(750, 300)
|
||||||
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||||
previousCodeContent = await page.locator('.cm-content').innerText()
|
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||||
@ -1367,16 +1407,16 @@ test('Can edit segments by dragging their handles', async ({ page }) => {
|
|||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
).not.toBeDisabled()
|
).not.toBeDisabled()
|
||||||
|
|
||||||
const startPX = [652, 418]
|
const startPX = [665, 458]
|
||||||
const lineEndPX = [794, 416]
|
const lineEndPX = [842, 458]
|
||||||
const arcEndPX = [893, 318]
|
const arcEndPX = [971, 342]
|
||||||
|
|
||||||
const dragPX = 30
|
const dragPX = 30
|
||||||
|
|
||||||
await page.getByText('startProfileAt([4.61, -14.01], %)').click()
|
await page.getByText('startProfileAt([4.61, -14.01], %)').click()
|
||||||
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
|
||||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(400)
|
||||||
let prevContent = await page.locator('.cm-content').innerText()
|
let prevContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
const step5 = { steps: 5 }
|
const step5 = { steps: 5 }
|
||||||
@ -1386,7 +1426,7 @@ test('Can edit segments by dragging their handles', async ({ page }) => {
|
|||||||
await page.mouse.down()
|
await page.mouse.down()
|
||||||
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
|
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
|
||||||
await page.mouse.up()
|
await page.mouse.up()
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||||
prevContent = await page.locator('.cm-content').innerText()
|
prevContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
@ -1414,9 +1454,9 @@ test('Can edit segments by dragging their handles', async ({ page }) => {
|
|||||||
// expect the code to have changed
|
// expect the code to have changed
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt([7.01, -11.79], %)
|
|> startProfileAt([6.44, -12.07], %)
|
||||||
|> line([14.69, 2.73], %)
|
|> line([14.04, 2.03], %)
|
||||||
|> tangentialArcTo([27.6, -3.25], %)`)
|
|> tangentialArcTo([27.19, -4.2], %)`)
|
||||||
})
|
})
|
||||||
|
|
||||||
const doSnapAtDifferentScales = async (
|
const doSnapAtDifferentScales = async (
|
||||||
@ -1535,38 +1575,46 @@ test('Sketch on face', async ({ page }) => {
|
|||||||
).not.toBeDisabled()
|
).not.toBeDisabled()
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
|
await page.waitForTimeout(300)
|
||||||
|
|
||||||
let previousCodeContent = await page.locator('.cm-content').innerText()
|
let previousCodeContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
await page.mouse.click(793, 133)
|
await u.openAndClearDebugPanel()
|
||||||
|
await u.doAndWaitForCmd(
|
||||||
|
() => page.mouse.click(793, 133),
|
||||||
|
'default_camera_get_settings',
|
||||||
|
true
|
||||||
|
)
|
||||||
|
await page.waitForTimeout(150)
|
||||||
|
|
||||||
const firstClickPosition = [612, 238]
|
const firstClickPosition = [612, 238]
|
||||||
const secondClickPosition = [661, 242]
|
const secondClickPosition = [661, 242]
|
||||||
const thirdClickPosition = [609, 267]
|
const thirdClickPosition = [609, 267]
|
||||||
|
|
||||||
await page.waitForTimeout(300)
|
|
||||||
|
|
||||||
await page.mouse.click(firstClickPosition[0], firstClickPosition[1])
|
await page.mouse.click(firstClickPosition[0], firstClickPosition[1])
|
||||||
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||||
previousCodeContent = await page.locator('.cm-content').innerText()
|
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
|
await page.waitForTimeout(100)
|
||||||
await page.mouse.click(secondClickPosition[0], secondClickPosition[1])
|
await page.mouse.click(secondClickPosition[0], secondClickPosition[1])
|
||||||
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||||
previousCodeContent = await page.locator('.cm-content').innerText()
|
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
|
await page.waitForTimeout(100)
|
||||||
await page.mouse.click(thirdClickPosition[0], thirdClickPosition[1])
|
await page.mouse.click(thirdClickPosition[0], thirdClickPosition[1])
|
||||||
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||||
previousCodeContent = await page.locator('.cm-content').innerText()
|
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
|
await page.waitForTimeout(100)
|
||||||
await page.mouse.click(firstClickPosition[0], firstClickPosition[1])
|
await page.mouse.click(firstClickPosition[0], firstClickPosition[1])
|
||||||
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||||
previousCodeContent = await page.locator('.cm-content').innerText()
|
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|
||||||
|> startProfileAt([1.03, 1.03], %)
|
|> startProfileAt([-12.83, 6.7], %)
|
||||||
|> line([4.18, -0.35], %)
|
|> line([2.87, -0.23], %)
|
||||||
|> line([-4.44, -2.13], %)
|
|> line([-3.05, -1.47], %)
|
||||||
|> close(%)`)
|
|> close(%)`)
|
||||||
|
|
||||||
await u.openAndClearDebugPanel()
|
await u.openAndClearDebugPanel()
|
||||||
@ -1576,9 +1624,14 @@ test('Sketch on face', async ({ page }) => {
|
|||||||
await u.updateCamPosition([1049, 239, 686])
|
await u.updateCamPosition([1049, 239, 686])
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
await page.getByText('startProfileAt([1.03, 1.03], %)').click()
|
await page.getByText('startProfileAt([-12.83, 6.7], %)').click()
|
||||||
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
|
||||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
await u.doAndWaitForCmd(
|
||||||
|
() => page.getByRole('button', { name: 'Edit Sketch' }).click(),
|
||||||
|
'default_camera_get_settings',
|
||||||
|
true
|
||||||
|
)
|
||||||
|
await page.waitForTimeout(150)
|
||||||
await page.setViewportSize({ width: 1200, height: 1200 })
|
await page.setViewportSize({ width: 1200, height: 1200 })
|
||||||
await u.openAndClearDebugPanel()
|
await u.openAndClearDebugPanel()
|
||||||
await u.updateCamPosition([452, -152, 1166])
|
await u.updateCamPosition([452, -152, 1166])
|
||||||
@ -1598,11 +1651,11 @@ test('Sketch on face', async ({ page }) => {
|
|||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|
||||||
|> startProfileAt([1.03, 1.03], %)
|
|> startProfileAt([-12.83, 6.7], %)
|
||||||
|> line([${process?.env?.CI ? 2.74 : 2.93}, -${
|
|> line([${process?.env?.CI ? 2.28 : 2.28}, -${
|
||||||
process?.env?.CI ? 0.24 : 0.2
|
process?.env?.CI ? 0.07 : 0.07
|
||||||
}], %)
|
}], %)
|
||||||
|> line([-4.44, -2.13], %)
|
|> line([-3.05, -1.47], %)
|
||||||
|> close(%)`)
|
|> close(%)`)
|
||||||
|
|
||||||
// exit sketch
|
// exit sketch
|
||||||
@ -1610,7 +1663,7 @@ test('Sketch on face', async ({ page }) => {
|
|||||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
|
||||||
await page.getByText('startProfileAt([1.03, 1.03], %)').click()
|
await page.getByText('startProfileAt([-12.83, 6.7], %)').click()
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Extrude' })).not.toBeDisabled()
|
await expect(page.getByRole('button', { name: 'Extrude' })).not.toBeDisabled()
|
||||||
await page.getByRole('button', { name: 'Extrude' }).click()
|
await page.getByRole('button', { name: 'Extrude' }).click()
|
||||||
@ -1624,11 +1677,11 @@ test('Sketch on face', async ({ page }) => {
|
|||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|
||||||
|> startProfileAt([1.03, 1.03], %)
|
|> startProfileAt([-12.83, 6.7], %)
|
||||||
|> line([${process?.env?.CI ? 2.74 : 2.93}, -${
|
|> line([${process?.env?.CI ? 2.28 : 2.28}, -${
|
||||||
process?.env?.CI ? 0.24 : 0.2
|
process?.env?.CI ? 0.07 : 0.07
|
||||||
}], %)
|
}], %)
|
||||||
|> line([-4.44, -2.13], %)
|
|> line([-3.05, -1.47], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(5 + 7, %)`)
|
|> extrude(5 + 7, %)`)
|
||||||
})
|
})
|
||||||
@ -1661,11 +1714,11 @@ test('Can code mod a line length', async ({ page }) => {
|
|||||||
|
|
||||||
// enter sketch again
|
// enter sketch again
|
||||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||||
await page.waitForTimeout(300) // wait for animation
|
await page.waitForTimeout(350) // wait for animation
|
||||||
|
|
||||||
const startXPx = 500
|
const startXPx = 500
|
||||||
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
|
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
|
||||||
await page.mouse.click(615, 133)
|
await page.mouse.click(615, 102)
|
||||||
await page.getByRole('button', { name: 'length', exact: true }).click()
|
await page.getByRole('button', { name: 'length', exact: true }).click()
|
||||||
await page.getByText('Add constraining value').click()
|
await page.getByText('Add constraining value').click()
|
||||||
|
|
||||||
@ -1673,3 +1726,42 @@ test('Can code mod a line length', async ({ page }) => {
|
|||||||
`const length001 = 20const part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> line([0, 20], %) |> xLine(-length001, %)`
|
`const length001 = 20const part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> line([0, 20], %) |> xLine(-length001, %)`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Extrude from command bar selects extrude line after', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.addInitScript(async () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`const part001 = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-10, -10], %)
|
||||||
|
|> line([20, 0], %)
|
||||||
|
|> line([0, 20], %)
|
||||||
|
|> xLine(-20, %)
|
||||||
|
|> close(%)
|
||||||
|
`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const u = getUtils(page)
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
|
// Click the line of code for xLine.
|
||||||
|
await page.getByText(`close(%)`).click() // TODO remove this and reinstate // await topHorzSegmentClick()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Extrude' }).click()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await expect(page.locator('.cm-activeLine')).toHaveText(
|
||||||
|
` |> extrude(5 + 7, %)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 46 KiB |
@ -1,12 +1,13 @@
|
|||||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||||
import { Themes } from 'lib/theme'
|
import { Themes } from 'lib/theme'
|
||||||
|
|
||||||
export const TEST_SETTINGS_KEY = '/user.toml'
|
export const TEST_SETTINGS_KEY = '/settings.toml'
|
||||||
export const TEST_SETTINGS = {
|
export const TEST_SETTINGS = {
|
||||||
app: {
|
app: {
|
||||||
theme: Themes.Dark,
|
theme: Themes.Dark,
|
||||||
onboardingStatus: 'dismissed',
|
onboardingStatus: 'dismissed',
|
||||||
projectDirectory: '',
|
projectDirectory: '',
|
||||||
|
enableSSAO: false,
|
||||||
},
|
},
|
||||||
modeling: {
|
modeling: {
|
||||||
defaultUnit: 'in',
|
defaultUnit: 'in',
|
||||||
@ -23,7 +24,7 @@ export const TEST_SETTINGS = {
|
|||||||
|
|
||||||
export const TEST_SETTINGS_ONBOARDING = {
|
export const TEST_SETTINGS_ONBOARDING = {
|
||||||
...TEST_SETTINGS,
|
...TEST_SETTINGS,
|
||||||
app: { ...TEST_SETTINGS.app, onboardingStatus: '/export ' },
|
app: { ...TEST_SETTINGS.app, onboardingStatus: '/export' },
|
||||||
} satisfies Partial<SaveSettingsPayload>
|
} satisfies Partial<SaveSettingsPayload>
|
||||||
|
|
||||||
export const TEST_SETTINGS_CORRUPTED = {
|
export const TEST_SETTINGS_CORRUPTED = {
|
||||||
|
@ -71,7 +71,7 @@ describe('ZMA (Tauri, Linux)', () => {
|
|||||||
|
|
||||||
// Now should be signed in
|
// Now should be signed in
|
||||||
const newFileButton = await $('[data-testid="home-new-file"]')
|
const newFileButton = await $('[data-testid="home-new-file"]')
|
||||||
expect(await newFileButton.getText()).toEqual('New file')
|
expect(await newFileButton.getText()).toEqual('New project')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('opens the settings page, checks filesystem settings, and closes the settings page', async () => {
|
it('opens the settings page, checks filesystem settings, and closes the settings page', async () => {
|
||||||
|
@ -57,7 +57,7 @@ echo "New version number without 'v': $new_version_number"
|
|||||||
git checkout -b "cut-release-$new_version"
|
git checkout -b "cut-release-$new_version"
|
||||||
|
|
||||||
echo "$(jq --arg v "$new_version_number" '.version=$v' package.json --indent 2)" > package.json
|
echo "$(jq --arg v "$new_version_number" '.version=$v' package.json --indent 2)" > package.json
|
||||||
echo "$(jq --arg v "$new_version_number" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)" > src-tauri/tauri.conf.json
|
echo "$(jq --arg v "$new_version_number" '.version=$v' src-tauri/tauri.conf.json --indent 2)" > src-tauri/tauri.conf.json
|
||||||
|
|
||||||
git add package.json src-tauri/tauri.conf.json
|
git add package.json src-tauri/tauri.conf.json
|
||||||
git commit -m "Cut release $new_version"
|
git commit -m "Cut release $new_version"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "untitled-app",
|
"name": "untitled-app",
|
||||||
"version": "0.17.3",
|
"version": "0.19.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.16.0",
|
"@codemirror/autocomplete": "^6.16.0",
|
||||||
@ -8,7 +8,7 @@
|
|||||||
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@headlessui/react": "^1.7.18",
|
"@headlessui/react": "^1.7.19",
|
||||||
"@headlessui/tailwindcss": "^0.2.0",
|
"@headlessui/tailwindcss": "^0.2.0",
|
||||||
"@kittycad/lib": "^0.0.58",
|
"@kittycad/lib": "^0.0.58",
|
||||||
"@lezer/javascript": "^1.4.9",
|
"@lezer/javascript": "^1.4.9",
|
||||||
@ -84,8 +84,8 @@
|
|||||||
"test:e2e:tauri": "E2E_TAURI_ENABLED=true TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' wdio run wdio.conf.ts",
|
"test:e2e:tauri": "E2E_TAURI_ENABLED=true TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' wdio run wdio.conf.ts",
|
||||||
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
|
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
|
||||||
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
|
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
|
||||||
"fmt": "prettier --write ./src && prettier --write ./e2e",
|
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e",
|
||||||
"fmt-check": "prettier --check ./src && prettier --check ./e2e",
|
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e",
|
||||||
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
|
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
|
||||||
"build:wasm": "(cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
|
"build:wasm": "(cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
|
||||||
"build:wasm-clean": "yarn wasm-prep && yarn build:wasm",
|
"build:wasm-clean": "yarn wasm-prep && yarn build:wasm",
|
||||||
@ -132,6 +132,7 @@
|
|||||||
"@types/wicg-file-system-access": "^2023.10.5",
|
"@types/wicg-file-system-access": "^2023.10.5",
|
||||||
"@types/ws": "^8.5.10",
|
"@types/ws": "^8.5.10",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"@vitest/web-worker": "^1.5.0",
|
||||||
"@wdio/cli": "^8.24.3",
|
"@wdio/cli": "^8.24.3",
|
||||||
"@wdio/globals": "^8.36.0",
|
"@wdio/globals": "^8.36.0",
|
||||||
"@wdio/local-runner": "^8.36.0",
|
"@wdio/local-runner": "^8.36.0",
|
||||||
|
@ -49,8 +49,6 @@ export default defineConfig({
|
|||||||
// use: { ...devices['Desktop Chrome'] },
|
// use: { ...devices['Desktop Chrome'] },
|
||||||
// },
|
// },
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Test against mobile viewports. */
|
/* Test against mobile viewports. */
|
||||||
// {
|
// {
|
||||||
// name: 'Mobile Chrome',
|
// name: 'Mobile Chrome',
|
||||||
|
2441
src-tauri/Cargo.lock
generated
@ -8,27 +8,27 @@ repository = "https://github.com/KittyCAD/modeling-app"
|
|||||||
default-run = "app"
|
default-run = "app"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.70"
|
rust-version = "1.70"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2.0.0-beta.12", features = [] }
|
tauri-build = { version = "2.0.0-beta.13", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
kittycad = "0.2.67"
|
kcl-lib = { version = "0.1.52", path = "../src/wasm-lib/kcl" }
|
||||||
|
kittycad = "0.3.0"
|
||||||
oauth2 = "4.4.2"
|
oauth2 = "4.4.2"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] }
|
tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] }
|
||||||
tauri-plugin-dialog = { version = "2.0.0-beta.5" }
|
tauri-plugin-cli = { version = "2.0.0-beta.3" }
|
||||||
tauri-plugin-fs = { version = "2.0.0-beta.5" }
|
tauri-plugin-dialog = { version = "2.0.0-beta.6" }
|
||||||
tauri-plugin-http = { version = "2.0.0-beta.5" }
|
tauri-plugin-fs = { version = "2.0.0-beta.6" }
|
||||||
|
tauri-plugin-http = { version = "2.0.0-beta.6" }
|
||||||
tauri-plugin-os = { version = "2.0.0-beta.2" }
|
tauri-plugin-os = { version = "2.0.0-beta.2" }
|
||||||
tauri-plugin-process = { version = "2.0.0-beta.2" }
|
tauri-plugin-process = { version = "2.0.0-beta.2" }
|
||||||
tauri-plugin-shell = { version = "2.0.0-beta.2" }
|
tauri-plugin-shell = { version = "2.0.0-beta.2" }
|
||||||
tauri-plugin-updater = { version = "2.0.0-beta.4" }
|
tauri-plugin-updater = { version = "2.0.0-beta.4" }
|
||||||
tokio = { version = "1.37.0", features = ["time"] }
|
tokio = { version = "1.37.0", features = ["time", "fs"] }
|
||||||
toml = "0.8.2"
|
toml = "0.8.2"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
"main"
|
"main"
|
||||||
],
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
|
"cli:default",
|
||||||
"path:default",
|
"path:default",
|
||||||
"event:default",
|
"event:default",
|
||||||
"window:default",
|
"window:default",
|
||||||
@ -23,7 +24,6 @@
|
|||||||
"fs:allow-copy-file",
|
"fs:allow-copy-file",
|
||||||
"fs:allow-mkdir",
|
"fs:allow-mkdir",
|
||||||
"fs:allow-remove",
|
"fs:allow-remove",
|
||||||
"fs:allow-remove",
|
|
||||||
"fs:allow-rename",
|
"fs:allow-rename",
|
||||||
"fs:allow-exists",
|
"fs:allow-exists",
|
||||||
"fs:allow-stat",
|
"fs:allow-stat",
|
||||||
|
6
src-tauri/rustfmt.toml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
max_width = 120
|
||||||
|
edition = "2018"
|
||||||
|
format_code_in_doc_comments = true
|
||||||
|
format_strings = false
|
||||||
|
imports_granularity = "Crate"
|
||||||
|
group_imports = "StdExternalCrate"
|
@ -1,91 +1,205 @@
|
|||||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
use std::env;
|
pub(crate) mod state;
|
||||||
use std::fs;
|
|
||||||
use std::io::Read;
|
use std::{
|
||||||
use std::path::Path;
|
env,
|
||||||
use std::path::PathBuf;
|
path::{Path, PathBuf},
|
||||||
|
process::Command,
|
||||||
|
};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use kcl_lib::settings::types::{
|
||||||
|
file::{FileEntry, Project, ProjectState},
|
||||||
|
project::ProjectConfiguration,
|
||||||
|
Configuration, DEFAULT_PROJECT_KCL_FILE,
|
||||||
|
};
|
||||||
use oauth2::TokenResponse;
|
use oauth2::TokenResponse;
|
||||||
use serde::Serialize;
|
use tauri::{ipc::InvokeError, Manager};
|
||||||
use std::process::Command;
|
use tauri_plugin_cli::CliExt;
|
||||||
use tauri::ipc::InvokeError;
|
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
const DEFAULT_HOST: &str = "https://api.kittycad.io";
|
|
||||||
|
|
||||||
/// This command returns the a json string parse from a toml file at the path.
|
const DEFAULT_HOST: &str = "https://api.zoo.dev";
|
||||||
|
const SETTINGS_FILE_NAME: &str = "settings.toml";
|
||||||
|
const PROJECT_SETTINGS_FILE_NAME: &str = "project.toml";
|
||||||
|
const PROJECT_FOLDER: &str = "zoo-modeling-app-projects";
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn read_toml(path: &str) -> Result<String, InvokeError> {
|
fn get_initial_default_dir(app: tauri::AppHandle) -> Result<PathBuf, InvokeError> {
|
||||||
let mut file = std::fs::File::open(path).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
let dir = match app.path().document_dir() {
|
||||||
let mut contents = String::new();
|
Ok(dir) => dir,
|
||||||
file.read_to_string(&mut contents)
|
Err(_) => {
|
||||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
// for headless Linux (eg. Github Actions)
|
||||||
let value =
|
let home_dir = app.path().home_dir()?;
|
||||||
toml::from_str::<toml::Value>(&contents).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
home_dir.join("Documents")
|
||||||
let value = serde_json::to_string(&value).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
}
|
||||||
Ok(value)
|
};
|
||||||
|
|
||||||
|
Ok(dir.join(PROJECT_FOLDER))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
|
|
||||||
/// Removed from tauri v2
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
pub struct DiskEntry {
|
|
||||||
/// The path to the entry.
|
|
||||||
pub path: PathBuf,
|
|
||||||
/// The name of the entry (file name with extension or directory name).
|
|
||||||
pub name: Option<String>,
|
|
||||||
/// The children of this entry if it's a directory.
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub children: Option<Vec<DiskEntry>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
|
|
||||||
/// Removed from tauri v2
|
|
||||||
fn is_dir<P: AsRef<Path>>(path: P) -> Result<bool> {
|
|
||||||
std::fs::metadata(path)
|
|
||||||
.map(|md| md.is_dir())
|
|
||||||
.map_err(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
|
|
||||||
/// Removed from tauri v2
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn read_dir_recursive(path: &str) -> Result<Vec<DiskEntry>, InvokeError> {
|
async fn get_state(app: tauri::AppHandle) -> Result<Option<ProjectState>, InvokeError> {
|
||||||
let mut files_and_dirs: Vec<DiskEntry> = vec![];
|
let store = app.state::<state::Store>();
|
||||||
// let path = path.as_ref();
|
Ok(store.get().await)
|
||||||
for entry in fs::read_dir(path).map_err(|e| InvokeError::from_anyhow(e.into()))? {
|
}
|
||||||
let path = entry
|
|
||||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?
|
|
||||||
.path();
|
|
||||||
|
|
||||||
if let Ok(flag) = is_dir(&path) {
|
#[tauri::command]
|
||||||
files_and_dirs.push(DiskEntry {
|
async fn set_state(app: tauri::AppHandle, state: Option<ProjectState>) -> Result<(), InvokeError> {
|
||||||
path: path.clone(),
|
let store = app.state::<state::Store>();
|
||||||
children: if flag {
|
store.set(state).await;
|
||||||
Some(read_dir_recursive(path.to_str().expect("No path"))?)
|
Ok(())
|
||||||
} else {
|
}
|
||||||
None
|
|
||||||
},
|
fn get_app_settings_file_path(app: &tauri::AppHandle) -> Result<PathBuf, InvokeError> {
|
||||||
name: path
|
let app_config_dir = app.path().app_config_dir()?;
|
||||||
.file_name()
|
Ok(app_config_dir.join(SETTINGS_FILE_NAME))
|
||||||
.map(|name| name.to_string_lossy())
|
}
|
||||||
.map(|name| name.to_string()),
|
|
||||||
});
|
#[tauri::command]
|
||||||
|
async fn read_app_settings_file(app: tauri::AppHandle) -> Result<Configuration, InvokeError> {
|
||||||
|
let mut settings_path = get_app_settings_file_path(&app)?;
|
||||||
|
let mut needs_migration = false;
|
||||||
|
|
||||||
|
// Check if this file exists.
|
||||||
|
if !settings_path.exists() {
|
||||||
|
// Try the backwards compatible path.
|
||||||
|
// TODO: Remove this after a few releases.
|
||||||
|
let app_config_dir = app.path().app_config_dir()?;
|
||||||
|
settings_path = format!(
|
||||||
|
"{}user.toml",
|
||||||
|
app_config_dir.display().to_string().trim_end_matches('/')
|
||||||
|
)
|
||||||
|
.into();
|
||||||
|
needs_migration = true;
|
||||||
|
// Check if this path exists.
|
||||||
|
if !settings_path.exists() {
|
||||||
|
let mut default = Configuration::default();
|
||||||
|
default.settings.project.directory = get_initial_default_dir(app.clone())?;
|
||||||
|
// Return the default configuration.
|
||||||
|
return Ok(default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(files_and_dirs)
|
|
||||||
|
let contents = tokio::fs::read_to_string(&settings_path)
|
||||||
|
.await
|
||||||
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
|
let mut parsed = Configuration::backwards_compatible_toml_parse(&contents).map_err(InvokeError::from_anyhow)?;
|
||||||
|
if parsed.settings.project.directory == PathBuf::new() {
|
||||||
|
parsed.settings.project.directory = get_initial_default_dir(app.clone())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Remove this after a few releases.
|
||||||
|
if needs_migration {
|
||||||
|
write_app_settings_file(app, parsed.clone()).await?;
|
||||||
|
// Delete the old file.
|
||||||
|
tokio::fs::remove_file(settings_path)
|
||||||
|
.await
|
||||||
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(parsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This command returns a string that is the contents of a file at the path.
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn read_txt_file(path: &str) -> Result<String, InvokeError> {
|
async fn write_app_settings_file(app: tauri::AppHandle, configuration: Configuration) -> Result<(), InvokeError> {
|
||||||
let mut file = std::fs::File::open(path).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
let settings_path = get_app_settings_file_path(&app)?;
|
||||||
let mut contents = String::new();
|
let contents = toml::to_string_pretty(&configuration).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
file.read_to_string(&mut contents)
|
tokio::fs::write(settings_path, contents.as_bytes())
|
||||||
|
.await
|
||||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
Ok(contents)
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_project_settings_file_path(app_settings: Configuration, project_name: &str) -> Result<PathBuf, InvokeError> {
|
||||||
|
Ok(app_settings
|
||||||
|
.settings
|
||||||
|
.project
|
||||||
|
.directory
|
||||||
|
.join(project_name)
|
||||||
|
.join(PROJECT_SETTINGS_FILE_NAME))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn read_project_settings_file(
|
||||||
|
app_settings: Configuration,
|
||||||
|
project_name: &str,
|
||||||
|
) -> Result<ProjectConfiguration, InvokeError> {
|
||||||
|
let settings_path = get_project_settings_file_path(app_settings, project_name)?;
|
||||||
|
|
||||||
|
// Check if this file exists.
|
||||||
|
if !settings_path.exists() {
|
||||||
|
// Return the default configuration.
|
||||||
|
return Ok(ProjectConfiguration::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
let contents = tokio::fs::read_to_string(&settings_path)
|
||||||
|
.await
|
||||||
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
|
let parsed = ProjectConfiguration::backwards_compatible_toml_parse(&contents).map_err(InvokeError::from_anyhow)?;
|
||||||
|
|
||||||
|
Ok(parsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn write_project_settings_file(
|
||||||
|
app_settings: Configuration,
|
||||||
|
project_name: &str,
|
||||||
|
configuration: ProjectConfiguration,
|
||||||
|
) -> Result<(), InvokeError> {
|
||||||
|
let settings_path = get_project_settings_file_path(app_settings, project_name)?;
|
||||||
|
let contents = toml::to_string_pretty(&configuration).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
|
tokio::fs::write(settings_path, contents.as_bytes())
|
||||||
|
.await
|
||||||
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize the directory that holds all the projects.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn initialize_project_directory(configuration: Configuration) -> Result<PathBuf, InvokeError> {
|
||||||
|
configuration
|
||||||
|
.ensure_project_directory_exists()
|
||||||
|
.await
|
||||||
|
.map_err(InvokeError::from_anyhow)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new project directory.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn create_new_project_directory(
|
||||||
|
configuration: Configuration,
|
||||||
|
project_name: &str,
|
||||||
|
initial_code: Option<&str>,
|
||||||
|
) -> Result<Project, InvokeError> {
|
||||||
|
configuration
|
||||||
|
.create_new_project_directory(project_name, initial_code)
|
||||||
|
.await
|
||||||
|
.map_err(InvokeError::from_anyhow)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List all the projects in the project directory.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn list_projects(configuration: Configuration) -> Result<Vec<Project>, InvokeError> {
|
||||||
|
configuration.list_projects().await.map_err(InvokeError::from_anyhow)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get information about a project.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn get_project_info(configuration: Configuration, project_path: &str) -> Result<Project, InvokeError> {
|
||||||
|
configuration
|
||||||
|
.get_project_info(project_path)
|
||||||
|
.await
|
||||||
|
.map_err(InvokeError::from_anyhow)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn read_dir_recursive(path: &str) -> Result<FileEntry, InvokeError> {
|
||||||
|
kcl_lib::settings::utils::walk_dir(&Path::new(path).to_path_buf())
|
||||||
|
.await
|
||||||
|
.map_err(InvokeError::from_anyhow)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This command instantiates a new window with auth.
|
/// This command instantiates a new window with auth.
|
||||||
@ -103,8 +217,7 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
|
|||||||
let auth_client = oauth2::basic::BasicClient::new(
|
let auth_client = oauth2::basic::BasicClient::new(
|
||||||
oauth2::ClientId::new(client_id),
|
oauth2::ClientId::new(client_id),
|
||||||
None,
|
None,
|
||||||
oauth2::AuthUrl::new(format!("{host}/authorize"))
|
oauth2::AuthUrl::new(format!("{host}/authorize")).map_err(|e| InvokeError::from_anyhow(e.into()))?,
|
||||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?,
|
|
||||||
Some(
|
Some(
|
||||||
oauth2::TokenUrl::new(format!("{host}/oauth2/device/token"))
|
oauth2::TokenUrl::new(format!("{host}/oauth2/device/token"))
|
||||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?,
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?,
|
||||||
@ -132,12 +245,10 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
|
|||||||
// and bypass the shell::open call as it fails on GitHub Actions.
|
// and bypass the shell::open call as it fails on GitHub Actions.
|
||||||
let e2e_tauri_enabled = env::var("E2E_TAURI_ENABLED").is_ok();
|
let e2e_tauri_enabled = env::var("E2E_TAURI_ENABLED").is_ok();
|
||||||
if e2e_tauri_enabled {
|
if e2e_tauri_enabled {
|
||||||
println!(
|
println!("E2E_TAURI_ENABLED is set, won't open {} externally", auth_uri.secret());
|
||||||
"E2E_TAURI_ENABLED is set, won't open {} externally",
|
tokio::fs::write("/tmp/kittycad_user_code", details.user_code().secret())
|
||||||
auth_uri.secret()
|
.await
|
||||||
);
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
fs::write("/tmp/kittycad_user_code", details.user_code().secret())
|
|
||||||
.expect("Unable to write /tmp/kittycad_user_code file");
|
|
||||||
} else {
|
} else {
|
||||||
app.shell()
|
app.shell()
|
||||||
.open(auth_uri.secret(), None)
|
.open(auth_uri.secret(), None)
|
||||||
@ -160,10 +271,7 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
|
|||||||
///This command returns the KittyCAD user info given a token.
|
///This command returns the KittyCAD user info given a token.
|
||||||
/// The string returned from this method is the user info as a json string.
|
/// The string returned from this method is the user info as a json string.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn get_user(
|
async fn get_user(token: &str, hostname: &str) -> Result<kittycad::types::User, InvokeError> {
|
||||||
token: Option<String>,
|
|
||||||
hostname: &str,
|
|
||||||
) -> Result<kittycad::types::User, InvokeError> {
|
|
||||||
// Use the host passed in if it's set.
|
// Use the host passed in if it's set.
|
||||||
// Otherwise, use the default host.
|
// Otherwise, use the default host.
|
||||||
let host = if hostname.is_empty() {
|
let host = if hostname.is_empty() {
|
||||||
@ -183,7 +291,7 @@ async fn get_user(
|
|||||||
println!("Getting user info...");
|
println!("Getting user info...");
|
||||||
|
|
||||||
// use kittycad library to fetch the user info from /user/me
|
// use kittycad library to fetch the user info from /user/me
|
||||||
let mut client = kittycad::Client::new(token.unwrap());
|
let mut client = kittycad::Client::new(token);
|
||||||
|
|
||||||
if baseurl != DEFAULT_HOST {
|
if baseurl != DEFAULT_HOST {
|
||||||
client.set_base_url(&baseurl);
|
client.set_base_url(&baseurl);
|
||||||
@ -202,50 +310,170 @@ async fn get_user(
|
|||||||
/// From this GitHub comment: https://github.com/tauri-apps/tauri/issues/4062#issuecomment-1338048169
|
/// From this GitHub comment: https://github.com/tauri-apps/tauri/issues/4062#issuecomment-1338048169
|
||||||
/// But with the Linux support removed since we don't need it for now.
|
/// But with the Linux support removed since we don't need it for now.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn show_in_folder(path: String) {
|
fn show_in_folder(path: &str) -> Result<(), InvokeError> {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(not(unix))]
|
||||||
{
|
{
|
||||||
Command::new("explorer")
|
Command::new("explorer")
|
||||||
.args(["/select,", &path]) // The comma after select is not a typo
|
.args(["/select,", &path]) // The comma after select is not a typo
|
||||||
.spawn()
|
.spawn()
|
||||||
.unwrap();
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
Command::new("open").args(["-R", &path]).spawn().unwrap();
|
Command::new("open")
|
||||||
|
.args(["-R", &path])
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() -> Result<()> {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.setup(|_app| {
|
.setup(|_app| {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
{
|
{
|
||||||
use tauri::Manager;
|
|
||||||
_app.get_webview("main").unwrap().open_devtools();
|
_app.get_webview("main").unwrap().open_devtools();
|
||||||
}
|
}
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
{
|
{
|
||||||
_app.handle()
|
_app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;
|
||||||
.plugin(tauri_plugin_updater::Builder::new().build())?;
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
get_state,
|
||||||
|
set_state,
|
||||||
|
get_initial_default_dir,
|
||||||
|
initialize_project_directory,
|
||||||
|
create_new_project_directory,
|
||||||
|
list_projects,
|
||||||
|
get_project_info,
|
||||||
get_user,
|
get_user,
|
||||||
login,
|
login,
|
||||||
read_toml,
|
|
||||||
read_txt_file,
|
|
||||||
read_dir_recursive,
|
read_dir_recursive,
|
||||||
show_in_folder,
|
show_in_folder,
|
||||||
|
read_app_settings_file,
|
||||||
|
write_app_settings_file,
|
||||||
|
read_project_settings_file,
|
||||||
|
write_project_settings_file,
|
||||||
])
|
])
|
||||||
|
.plugin(tauri_plugin_cli::init())
|
||||||
|
.setup(|app| {
|
||||||
|
let mut verbose = false;
|
||||||
|
let mut source_path: Option<PathBuf> = None;
|
||||||
|
match app.cli().matches() {
|
||||||
|
// `matches` here is a Struct with { args, subcommand }.
|
||||||
|
// `args` is `HashMap<String, ArgData>` where `ArgData` is a struct with { value, occurrences }.
|
||||||
|
// `subcommand` is `Option<Box<SubcommandMatches>>` where `SubcommandMatches` is a struct with { name, matches }.
|
||||||
|
Ok(matches) => {
|
||||||
|
if let Some(verbose_flag) = matches.args.get("verbose") {
|
||||||
|
let Some(value) = verbose_flag.value.as_bool() else {
|
||||||
|
return Err(
|
||||||
|
anyhow::anyhow!("Error parsing CLI arguments: verbose flag is not a boolean").into(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
verbose = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the path we are trying to open.
|
||||||
|
if let Some(source_arg) = matches.args.get("source") {
|
||||||
|
// We don't do an else here because this can be null.
|
||||||
|
if let Some(value) = source_arg.value.as_str() {
|
||||||
|
source_path = Some(Path::new(value).to_path_buf());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(anyhow::anyhow!("Error parsing CLI arguments: {:?}", err).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a source path to open, make sure it exists.
|
||||||
|
let Some(source_path) = source_path else {
|
||||||
|
// The user didn't provide a source path to open.
|
||||||
|
// Run the app as normal.
|
||||||
|
app.manage(state::Store::default());
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
if !source_path.exists() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Error: the path `{}` you are trying to open does not exist",
|
||||||
|
source_path.display()
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let runner: tauri::async_runtime::JoinHandle<Result<ProjectState>> =
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
// If the path is a directory, let's assume it is a project directory.
|
||||||
|
if source_path.is_dir() {
|
||||||
|
// Load the details about the project from the path.
|
||||||
|
let project = Project::from_path(&source_path).await.map_err(|e| {
|
||||||
|
anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
println!("Project loaded from path: {}", source_path.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the default file in the project.
|
||||||
|
// Write the initial project file.
|
||||||
|
let project_file = source_path.join(DEFAULT_PROJECT_KCL_FILE);
|
||||||
|
tokio::fs::write(&project_file, vec![]).await?;
|
||||||
|
|
||||||
|
return Ok(ProjectState {
|
||||||
|
project,
|
||||||
|
current_file: Some(project_file.display().to_string()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// We were given a file path, not a directory.
|
||||||
|
// Let's get the parent directory of the file.
|
||||||
|
let parent = source_path.parent().ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"Error getting the parent directory of the file: {}",
|
||||||
|
source_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Load the details about the project from the parent directory.
|
||||||
|
let project = Project::from_path(&parent).await.map_err(|e| {
|
||||||
|
anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
println!(
|
||||||
|
"Project loaded from path: {}, current file: {}",
|
||||||
|
parent.display(),
|
||||||
|
source_path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ProjectState {
|
||||||
|
project,
|
||||||
|
current_file: Some(source_path.display().to_string()),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Block on the handle.
|
||||||
|
let store = tauri::async_runtime::block_on(runner)??;
|
||||||
|
|
||||||
|
// Create a state object to hold the project.
|
||||||
|
app.manage(state::Store::new(store));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_fs::init())
|
.plugin(tauri_plugin_fs::init())
|
||||||
.plugin(tauri_plugin_http::init())
|
.plugin(tauri_plugin_http::init())
|
||||||
.plugin(tauri_plugin_os::init())
|
.plugin(tauri_plugin_os::init())
|
||||||
.plugin(tauri_plugin_process::init())
|
.plugin(tauri_plugin_process::init())
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())?;
|
||||||
.expect("error while running tauri application");
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
21
src-tauri/src/state.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
//! State management for the application.
|
||||||
|
|
||||||
|
use kcl_lib::settings::types::file::ProjectState;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Store(Mutex<Option<ProjectState>>);
|
||||||
|
|
||||||
|
impl Store {
|
||||||
|
pub fn new(p: ProjectState) -> Self {
|
||||||
|
Self(Mutex::new(Some(p)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get(&self) -> Option<ProjectState> {
|
||||||
|
self.0.lock().await.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set(&self, p: Option<ProjectState>) {
|
||||||
|
*self.0.lock().await = p;
|
||||||
|
}
|
||||||
|
}
|
@ -50,10 +50,26 @@
|
|||||||
},
|
},
|
||||||
"identifier": "dev.zoo.modeling-app",
|
"identifier": "dev.zoo.modeling-app",
|
||||||
"plugins": {
|
"plugins": {
|
||||||
|
"cli": {
|
||||||
|
"description": "Zoo Modeling App CLI",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"short": "v",
|
||||||
|
"name": "verbose",
|
||||||
|
"description": "Verbosity level"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "source",
|
||||||
|
"index": 1,
|
||||||
|
"takesValue": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subcommands": {}
|
||||||
|
},
|
||||||
"shell": {
|
"shell": {
|
||||||
"open": true
|
"open": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"productName": "Zoo Modeling App",
|
"productName": "Zoo Modeling App",
|
||||||
"version": "0.17.3"
|
"version": "0.19.1"
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,7 @@ import SettingsAuthProvider from 'components/SettingsAuthProvider'
|
|||||||
import LspProvider from 'components/LspProvider'
|
import LspProvider from 'components/LspProvider'
|
||||||
import { KclContextProvider } from 'lang/KclProvider'
|
import { KclContextProvider } from 'lang/KclProvider'
|
||||||
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||||
|
import { getState, setState } from 'lib/tauri'
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@ -52,10 +53,30 @@ const router = createBrowserRouter([
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: paths.INDEX,
|
path: paths.INDEX,
|
||||||
loader: () =>
|
loader: async () => {
|
||||||
isTauri()
|
const inTauri = isTauri()
|
||||||
|
if (inTauri) {
|
||||||
|
const appState = await getState()
|
||||||
|
|
||||||
|
if (appState) {
|
||||||
|
console.log('appState', appState)
|
||||||
|
// Reset the state.
|
||||||
|
// We do this so that we load the initial state from the cli but everything
|
||||||
|
// else we can ignore.
|
||||||
|
await setState(undefined)
|
||||||
|
// Redirect to the file if we have a file path.
|
||||||
|
if (appState.current_file) {
|
||||||
|
return redirect(
|
||||||
|
paths.FILE + '/' + encodeURIComponent(appState.current_file)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inTauri
|
||||||
? redirect(paths.HOME)
|
? redirect(paths.HOME)
|
||||||
: redirect(paths.FILE + '/%2F' + BROWSER_PROJECT_NAME),
|
: redirect(paths.FILE + '/%2F' + BROWSER_PROJECT_NAME)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loader: fileLoader,
|
loader: fileLoader,
|
||||||
|
@ -246,13 +246,31 @@ export class CameraControls {
|
|||||||
camSettings.center.y,
|
camSettings.center.y,
|
||||||
camSettings.center.z
|
camSettings.center.z
|
||||||
)
|
)
|
||||||
|
this.camera.up.set(camSettings.up.x, camSettings.up.y, camSettings.up.z)
|
||||||
|
if (this.camera instanceof PerspectiveCamera && camSettings.ortho) {
|
||||||
|
this.useOrthographicCamera()
|
||||||
|
}
|
||||||
|
if (this.camera instanceof OrthographicCamera && !camSettings.ortho) {
|
||||||
|
this.usePerspectiveCamera()
|
||||||
|
}
|
||||||
if (this.camera instanceof PerspectiveCamera && camSettings.fov_y) {
|
if (this.camera instanceof PerspectiveCamera && camSettings.fov_y) {
|
||||||
this.camera.fov = camSettings.fov_y
|
this.camera.fov = camSettings.fov_y
|
||||||
} else if (
|
} else if (
|
||||||
this.camera instanceof OrthographicCamera &&
|
this.camera instanceof OrthographicCamera &&
|
||||||
camSettings.ortho_scale
|
camSettings.ortho_scale
|
||||||
) {
|
) {
|
||||||
this.camera.zoom = camSettings.ortho_scale
|
const distanceToTarget = new Vector3(
|
||||||
|
camSettings.pos.x,
|
||||||
|
camSettings.pos.y,
|
||||||
|
camSettings.pos.z
|
||||||
|
).distanceTo(
|
||||||
|
new Vector3(
|
||||||
|
camSettings.center.x,
|
||||||
|
camSettings.center.y,
|
||||||
|
camSettings.center.z
|
||||||
|
)
|
||||||
|
)
|
||||||
|
this.camera.zoom = (camSettings.ortho_scale * 40) / distanceToTarget
|
||||||
}
|
}
|
||||||
this.onCameraChange()
|
this.onCameraChange()
|
||||||
}
|
}
|
||||||
@ -965,10 +983,10 @@ export class CameraControls {
|
|||||||
// Pure function helpers
|
// Pure function helpers
|
||||||
|
|
||||||
function calculateNearFarFromFOV(fov: number) {
|
function calculateNearFarFromFOV(fov: number) {
|
||||||
const nearFarRatio = (fov - 3) / (45 - 3)
|
// const nearFarRatio = (fov - 3) / (45 - 3)
|
||||||
// const z_near = 0.1 + nearFarRatio * (5 - 0.1)
|
// const z_near = 0.1 + nearFarRatio * (5 - 0.1)
|
||||||
const z_far = 1000 + nearFarRatio * (100000 - 1000)
|
// const z_far = 1000 + nearFarRatio * (100000 - 1000)
|
||||||
return { z_near: 0.1, z_far }
|
return { z_near: 0.1, z_far: 1000 }
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertThreeCamValuesToEngineCam({
|
function convertThreeCamValuesToEngineCam({
|
||||||
@ -1043,3 +1061,62 @@ function _getInteractionType(
|
|||||||
if (enableZoom && interactionGuards.zoom.dragCallback(event)) return 'zoom'
|
if (enableZoom && interactionGuards.zoom.dragCallback(event)) return 'zoom'
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells the engine to fire it's animation waits for it to finish and then requests camera settings
|
||||||
|
* to ensure the client-side camera is synchronized with the engine's camera state.
|
||||||
|
*
|
||||||
|
* @param engineCommandManager Our websocket singleton
|
||||||
|
* @param entityId - The ID of face or sketchPlane.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function letEngineAnimateAndSyncCamAfter(
|
||||||
|
engineCommandManager: EngineCommandManager,
|
||||||
|
entityId: string
|
||||||
|
) {
|
||||||
|
await engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'enable_sketch_mode',
|
||||||
|
adjust_camera: true,
|
||||||
|
animated: !isReducedMotion(),
|
||||||
|
ortho: false,
|
||||||
|
entity_id: entityId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// wait 600ms (animation takes 500, + 100 for safety)
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, isReducedMotion() ? 100 : 600)
|
||||||
|
)
|
||||||
|
await engineCommandManager.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',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'enable_sketch_mode',
|
||||||
|
adjust_camera: true,
|
||||||
|
animated: false,
|
||||||
|
ortho: true,
|
||||||
|
entity_id: entityId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||||
|
await engineCommandManager.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',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -3,7 +3,6 @@ import { useModelingContext } from 'hooks/useModelingContext'
|
|||||||
|
|
||||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { useStore } from 'useStore'
|
|
||||||
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
|
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
|
||||||
import { ReactCameraProperties } from './CameraControls'
|
import { ReactCameraProperties } from './CameraControls'
|
||||||
import { throttle } from 'lib/utils'
|
import { throttle } from 'lib/utils'
|
||||||
@ -47,10 +46,6 @@ export const ClientSideScene = ({
|
|||||||
const canvasRef = useRef<HTMLDivElement>(null)
|
const canvasRef = useRef<HTMLDivElement>(null)
|
||||||
const { state, send, context } = useModelingContext()
|
const { state, send, context } = useModelingContext()
|
||||||
const { hideClient, hideServer } = useShouldHideScene()
|
const { hideClient, hideServer } = useShouldHideScene()
|
||||||
const { setHighlightRange } = useStore((s) => ({
|
|
||||||
setHighlightRange: s.setHighlightRange,
|
|
||||||
highlightRange: s.highlightRange,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Listen for changes to the camera controls setting
|
// Listen for changes to the camera controls setting
|
||||||
// and update the client-side scene's controls accordingly.
|
// and update the client-side scene's controls accordingly.
|
||||||
@ -69,7 +64,6 @@ export const ClientSideScene = ({
|
|||||||
const canvas = canvasRef.current
|
const canvas = canvasRef.current
|
||||||
canvas.appendChild(sceneInfra.renderer.domElement)
|
canvas.appendChild(sceneInfra.renderer.domElement)
|
||||||
sceneInfra.animate()
|
sceneInfra.animate()
|
||||||
sceneInfra.setHighlightCallback(setHighlightRange)
|
|
||||||
canvas.addEventListener('mousemove', sceneInfra.onMouseMove, false)
|
canvas.addEventListener('mousemove', sceneInfra.onMouseMove, false)
|
||||||
canvas.addEventListener('mousedown', sceneInfra.onMouseDown, false)
|
canvas.addEventListener('mousedown', sceneInfra.onMouseDown, false)
|
||||||
canvas.addEventListener('mouseup', sceneInfra.onMouseUp, false)
|
canvas.addEventListener('mouseup', sceneInfra.onMouseUp, false)
|
||||||
|
@ -57,6 +57,7 @@ import {
|
|||||||
kclManager,
|
kclManager,
|
||||||
sceneInfra,
|
sceneInfra,
|
||||||
codeManager,
|
codeManager,
|
||||||
|
editorManager,
|
||||||
} from 'lib/singletons'
|
} from 'lib/singletons'
|
||||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||||
import { executeAst, useStore } from 'useStore'
|
import { executeAst, useStore } from 'useStore'
|
||||||
@ -214,8 +215,9 @@ export class SceneEntities {
|
|||||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||||
const baseXColor = 0x000055
|
const baseXColor = 0x000055
|
||||||
const baseYColor = 0x550000
|
const baseYColor = 0x550000
|
||||||
const xAxisGeometry = new BoxGeometry(100000, 0.3, 0.01)
|
const axisPixelWidth = 1.6
|
||||||
const yAxisGeometry = new BoxGeometry(0.3, 100000, 0.01)
|
const xAxisGeometry = new BoxGeometry(100000, axisPixelWidth, 0.01)
|
||||||
|
const yAxisGeometry = new BoxGeometry(axisPixelWidth, 100000, 0.01)
|
||||||
const xAxisMaterial = new MeshBasicMaterial({
|
const xAxisMaterial = new MeshBasicMaterial({
|
||||||
color: baseXColor,
|
color: baseXColor,
|
||||||
depthTest: false,
|
depthTest: false,
|
||||||
@ -1323,30 +1325,31 @@ export class SceneEntities {
|
|||||||
selected.material.color = defaultPlaneColor(type)
|
selected.material.color = defaultPlaneColor(type)
|
||||||
},
|
},
|
||||||
onClick: async (args) => {
|
onClick: async (args) => {
|
||||||
const checkExtrudeFaceClick = async (): Promise<boolean> => {
|
const checkExtrudeFaceClick = async (): Promise<
|
||||||
|
['face' | 'plane' | 'other', string]
|
||||||
|
> => {
|
||||||
const { streamDimensions } = useStore.getState()
|
const { streamDimensions } = useStore.getState()
|
||||||
const { entity_id } = await sendSelectEventToEngine(
|
const { entity_id } = await sendSelectEventToEngine(
|
||||||
args?.mouseEvent,
|
args?.mouseEvent,
|
||||||
document.getElementById('video-stream') as HTMLVideoElement,
|
document.getElementById('video-stream') as HTMLVideoElement,
|
||||||
streamDimensions
|
streamDimensions
|
||||||
)
|
)
|
||||||
if (!entity_id) return false
|
if (!entity_id) return ['other', '']
|
||||||
|
if (
|
||||||
|
engineCommandManager.defaultPlanes?.xy === entity_id ||
|
||||||
|
engineCommandManager.defaultPlanes?.xz === entity_id ||
|
||||||
|
engineCommandManager.defaultPlanes?.yz === entity_id
|
||||||
|
) {
|
||||||
|
return ['plane', entity_id]
|
||||||
|
}
|
||||||
const artifact = this.engineCommandManager.artifactMap[entity_id]
|
const artifact = this.engineCommandManager.artifactMap[entity_id]
|
||||||
if (artifact?.commandType !== 'solid3d_get_extrusion_face_info')
|
if (artifact?.commandType !== 'solid3d_get_extrusion_face_info')
|
||||||
return false
|
return ['other', entity_id]
|
||||||
const faceInfo: Models['FaceIsPlanar_type'] = (
|
|
||||||
await this.engineCommandManager.sendSceneCommand({
|
const faceInfo = await getFaceDetails(entity_id)
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'face_is_planar',
|
|
||||||
object_id: entity_id,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)?.data?.data
|
|
||||||
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
|
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
|
||||||
return false
|
return ['other', entity_id]
|
||||||
const { z_axis, origin, y_axis } = faceInfo
|
const { z_axis, y_axis, origin } = faceInfo
|
||||||
const pathToNode = getNodePathFromSourceRange(
|
const pathToNode = getNodePathFromSourceRange(
|
||||||
kclManager.ast,
|
kclManager.ast,
|
||||||
artifact.range
|
artifact.range
|
||||||
@ -1366,12 +1369,15 @@ export class SceneEntities {
|
|||||||
artifact?.additionalData?.type === 'cap'
|
artifact?.additionalData?.type === 'cap'
|
||||||
? artifact.additionalData.info
|
? artifact.additionalData.info
|
||||||
: 'none',
|
: 'none',
|
||||||
|
faceId: entity_id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return true
|
return ['face', entity_id]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await checkExtrudeFaceClick()) return
|
const faceResult = await checkExtrudeFaceClick()
|
||||||
|
console.log('faceResult', faceResult)
|
||||||
|
if (faceResult[0] === 'face') return
|
||||||
|
|
||||||
if (!args || !args.intersects?.[0]) return
|
if (!args || !args.intersects?.[0]) return
|
||||||
if (args.mouseEvent.which !== 1) return
|
if (args.mouseEvent.which !== 1) return
|
||||||
@ -1397,6 +1403,7 @@ export class SceneEntities {
|
|||||||
plane: planeString,
|
plane: planeString,
|
||||||
zAxis,
|
zAxis,
|
||||||
yAxis,
|
yAxis,
|
||||||
|
planeId: faceResult[1],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -1423,7 +1430,7 @@ export class SceneEntities {
|
|||||||
parent.userData.pathToNode,
|
parent.userData.pathToNode,
|
||||||
'CallExpression'
|
'CallExpression'
|
||||||
).node
|
).node
|
||||||
sceneInfra.highlightCallback([node.start, node.end])
|
editorManager.setHighlightRange([node.start, node.end])
|
||||||
const yellow = 0xffff00
|
const yellow = 0xffff00
|
||||||
colorSegment(selected, yellow)
|
colorSegment(selected, yellow)
|
||||||
const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
||||||
@ -1459,10 +1466,10 @@ export class SceneEntities {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sceneInfra.highlightCallback([0, 0])
|
editorManager.setHighlightRange([0, 0])
|
||||||
},
|
},
|
||||||
onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => {
|
onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => {
|
||||||
sceneInfra.highlightCallback([0, 0])
|
editorManager.setHighlightRange([0, 0])
|
||||||
const parent = getParentGroup(selected, [
|
const parent = getParentGroup(selected, [
|
||||||
STRAIGHT_SEGMENT,
|
STRAIGHT_SEGMENT,
|
||||||
TANGENTIAL_ARC_TO_SEGMENT,
|
TANGENTIAL_ARC_TO_SEGMENT,
|
||||||
@ -1680,7 +1687,7 @@ export async function getSketchOrientationDetails(
|
|||||||
sketchPathToNode: PathToNode
|
sketchPathToNode: PathToNode
|
||||||
): Promise<{
|
): Promise<{
|
||||||
quat: Quaternion
|
quat: Quaternion
|
||||||
sketchDetails: SketchDetails
|
sketchDetails: SketchDetails & { faceId?: string }
|
||||||
}> {
|
}> {
|
||||||
const sketchGroup = sketchGroupFromPathToNode({
|
const sketchGroup = sketchGroupFromPathToNode({
|
||||||
pathToNode: sketchPathToNode,
|
pathToNode: sketchPathToNode,
|
||||||
@ -1696,20 +1703,13 @@ export async function getSketchOrientationDetails(
|
|||||||
zAxis: [zAxis.x, zAxis.y, zAxis.z],
|
zAxis: [zAxis.x, zAxis.y, zAxis.z],
|
||||||
yAxis: [sketchGroup.yAxis.x, sketchGroup.yAxis.y, sketchGroup.yAxis.z],
|
yAxis: [sketchGroup.yAxis.x, sketchGroup.yAxis.y, sketchGroup.yAxis.z],
|
||||||
origin: [0, 0, 0],
|
origin: [0, 0, 0],
|
||||||
|
faceId: sketchGroup.on.id,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (sketchGroup.on.type === 'face') {
|
if (sketchGroup.on.type === 'face') {
|
||||||
const faceInfo: Models['FaceIsPlanar_type'] = (
|
const faceInfo = await getFaceDetails(sketchGroup.on.faceId)
|
||||||
await engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'face_is_planar',
|
|
||||||
object_id: sketchGroup.on.faceId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)?.data?.data
|
|
||||||
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
|
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
|
||||||
throw new Error('faceInfo')
|
throw new Error('faceInfo')
|
||||||
const { z_axis, y_axis, origin } = faceInfo
|
const { z_axis, y_axis, origin } = faceInfo
|
||||||
@ -1724,6 +1724,7 @@ export async function getSketchOrientationDetails(
|
|||||||
zAxis: [z_axis.x, z_axis.y, z_axis.z],
|
zAxis: [z_axis.x, z_axis.y, z_axis.z],
|
||||||
yAxis: [y_axis.x, y_axis.y, y_axis.z],
|
yAxis: [y_axis.x, y_axis.y, y_axis.z],
|
||||||
origin: [origin.x, origin.y, origin.z],
|
origin: [origin.x, origin.y, origin.z],
|
||||||
|
faceId: sketchGroup.on.faceId,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1732,6 +1733,46 @@ export async function getSketchOrientationDetails(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves orientation details for a given entity representing a face (brep face or default plane).
|
||||||
|
* This function asynchronously fetches and returns the origin, x-axis, y-axis, and z-axis details
|
||||||
|
* for a specified entity ID. It is primarily used to obtain the orientation of a face in the scene,
|
||||||
|
* which is essential for calculating the correct positioning and alignment of the client side sketch.
|
||||||
|
*
|
||||||
|
* @param entityId - The ID of the entity for which orientation details are being fetched.
|
||||||
|
* @returns A promise that resolves with the orientation details of the face.
|
||||||
|
*/
|
||||||
|
async function getFaceDetails(
|
||||||
|
entityId: string
|
||||||
|
): Promise<Models['FaceIsPlanar_type']> {
|
||||||
|
// TODO mode engine connection to allow batching returns and batch the following
|
||||||
|
await engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'enable_sketch_mode',
|
||||||
|
adjust_camera: false,
|
||||||
|
animated: false,
|
||||||
|
ortho: false,
|
||||||
|
entity_id: entityId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// TODO change typing to get_sketch_mode_plane once lib is updated
|
||||||
|
const faceInfo: Models['FaceIsPlanar_type'] = (
|
||||||
|
await engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: { type: 'get_sketch_mode_plane' },
|
||||||
|
})
|
||||||
|
)?.data?.data
|
||||||
|
await engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: { type: 'sketch_mode_disable' },
|
||||||
|
})
|
||||||
|
return faceInfo
|
||||||
|
}
|
||||||
|
|
||||||
export function getQuaternionFromZAxis(zAxis: Vector3): Quaternion {
|
export function getQuaternionFromZAxis(zAxis: Vector3): Quaternion {
|
||||||
const dummyCam = new PerspectiveCamera()
|
const dummyCam = new PerspectiveCamera()
|
||||||
dummyCam.up.set(0, 0, 1)
|
dummyCam.up.set(0, 0, 1)
|
||||||
|
@ -24,7 +24,6 @@ import {
|
|||||||
import { compareVec2Epsilon2 } from 'lang/std/sketch'
|
import { compareVec2Epsilon2 } from 'lang/std/sketch'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
import * as TWEEN from '@tweenjs/tween.js'
|
import * as TWEEN from '@tweenjs/tween.js'
|
||||||
import { SourceRange } from 'lang/wasm'
|
|
||||||
import { Axis } from 'lib/selections'
|
import { Axis } from 'lib/selections'
|
||||||
import { type BaseUnit } from 'lib/settings/settingsTypes'
|
import { type BaseUnit } from 'lib/settings/settingsTypes'
|
||||||
import { CameraControls } from './CameraControls'
|
import { CameraControls } from './CameraControls'
|
||||||
@ -149,10 +148,6 @@ export class SceneInfra {
|
|||||||
onMouseLeave: () => {},
|
onMouseLeave: () => {},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
highlightCallback: (a: SourceRange) => void = () => {}
|
|
||||||
setHighlightCallback(cb: (a: SourceRange) => void) {
|
|
||||||
this.highlightCallback = cb
|
|
||||||
}
|
|
||||||
|
|
||||||
modelingSend: SendType = (() => {}) as any
|
modelingSend: SendType = (() => {}) as any
|
||||||
setSend(send: SendType) {
|
setSend(send: SendType) {
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
import { kclManager } from 'lib/singletons'
|
import { editorManager, kclManager } from 'lib/singletons'
|
||||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useStore } from 'useStore'
|
|
||||||
|
|
||||||
export function AstExplorer() {
|
export function AstExplorer() {
|
||||||
const setHighlightRange = useStore((s) => s.setHighlightRange)
|
|
||||||
const { context } = useModelingContext()
|
const { context } = useModelingContext()
|
||||||
const pathToNode = getNodePathFromSourceRange(
|
const pathToNode = getNodePathFromSourceRange(
|
||||||
// TODO maybe need to have callback to make sure it stays in sync
|
// TODO maybe need to have callback to make sure it stays in sync
|
||||||
@ -42,7 +40,7 @@ export function AstExplorer() {
|
|||||||
<div
|
<div
|
||||||
className="h-full relative"
|
className="h-full relative"
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
setHighlightRange([0, 0])
|
editorManager.setHighlightRange([0, 0])
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<pre className="text-xs">
|
<pre className="text-xs">
|
||||||
@ -88,7 +86,6 @@ function DisplayObj({
|
|||||||
filterKeys: string[]
|
filterKeys: string[]
|
||||||
node: any
|
node: any
|
||||||
}) {
|
}) {
|
||||||
const setHighlightRange = useStore((s) => s.setHighlightRange)
|
|
||||||
const { send } = useModelingContext()
|
const { send } = useModelingContext()
|
||||||
const ref = useRef<HTMLPreElement>(null)
|
const ref = useRef<HTMLPreElement>(null)
|
||||||
const [hasCursor, setHasCursor] = useState(false)
|
const [hasCursor, setHasCursor] = useState(false)
|
||||||
@ -112,12 +109,12 @@ function DisplayObj({
|
|||||||
hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : ''
|
hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : ''
|
||||||
}`}
|
}`}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
setHighlightRange([obj?.start || 0, obj.end])
|
editorManager.setHighlightRange([obj?.start || 0, obj.end])
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}}
|
}}
|
||||||
onMouseMove={(e) => {
|
onMouseMove={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setHighlightRange([obj?.start || 0, obj.end])
|
editorManager.setHighlightRange([obj?.start || 0, obj.end])
|
||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
send({
|
send({
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useMachine } from '@xstate/react'
|
import { useMachine } from '@xstate/react'
|
||||||
|
import { editorManager } from 'lib/singletons'
|
||||||
import { commandBarMachine } from 'machines/commandBarMachine'
|
import { commandBarMachine } from 'machines/commandBarMachine'
|
||||||
import { createContext } from 'react'
|
import { createContext, useEffect } from 'react'
|
||||||
import { EventFrom, StateFrom } from 'xstate'
|
import { EventFrom, StateFrom } from 'xstate'
|
||||||
|
|
||||||
type CommandsContextType = {
|
type CommandsContextType = {
|
||||||
@ -30,6 +31,10 @@ export const CommandBarProvider = ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
editorManager.setCommandBarSend(commandBarSend)
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommandsContext.Provider
|
<CommandsContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
@ -3,13 +3,12 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
|
|||||||
import { useKclContext } from 'lang/KclProvider'
|
import { useKclContext } from 'lang/KclProvider'
|
||||||
import { CommandArgument } from 'lib/commandTypes'
|
import { CommandArgument } from 'lib/commandTypes'
|
||||||
import {
|
import {
|
||||||
ResolvedSelectionType,
|
|
||||||
canSubmitSelectionArg,
|
canSubmitSelectionArg,
|
||||||
getSelectionType,
|
getSelectionType,
|
||||||
getSelectionTypeDisplayText,
|
getSelectionTypeDisplayText,
|
||||||
} from 'lib/selections'
|
} from 'lib/selections'
|
||||||
import { modelingMachine } from 'machines/modelingMachine'
|
import { modelingMachine } from 'machines/modelingMachine'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { StateFrom } from 'xstate'
|
import { StateFrom } from 'xstate'
|
||||||
|
|
||||||
@ -30,13 +29,13 @@ function CommandBarSelectionInput({
|
|||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
const [hasSubmitted, setHasSubmitted] = useState(false)
|
const [hasSubmitted, setHasSubmitted] = useState(false)
|
||||||
const selection = useSelector(arg.machineActor, selectionSelector)
|
const selection = useSelector(arg.machineActor, selectionSelector)
|
||||||
const [selectionsByType, setSelectionsByType] = useState<
|
const initSelectionsByType = useCallback(() => {
|
||||||
'none' | ResolvedSelectionType[]
|
const selectionRangeEnd = selection.codeBasedSelections[0]?.range[1]
|
||||||
>(
|
return !selectionRangeEnd || selectionRangeEnd === code.length
|
||||||
selection.codeBasedSelections[0]?.range[1] === code.length
|
|
||||||
? 'none'
|
? 'none'
|
||||||
: getSelectionType(selection)
|
: getSelectionType(selection)
|
||||||
)
|
}, [selection, code])
|
||||||
|
const selectionsByType = initSelectionsByType()
|
||||||
const [canSubmitSelection, setCanSubmitSelection] = useState<boolean>(
|
const [canSubmitSelection, setCanSubmitSelection] = useState<boolean>(
|
||||||
canSubmitSelectionArg(selectionsByType, arg)
|
canSubmitSelectionArg(selectionsByType, arg)
|
||||||
)
|
)
|
||||||
@ -51,17 +50,14 @@ function CommandBarSelectionInput({
|
|||||||
inputRef.current?.focus()
|
inputRef.current?.focus()
|
||||||
}, [selection, inputRef])
|
}, [selection, inputRef])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSelectionsByType(
|
|
||||||
selection.codeBasedSelections[0]?.range[1] === code.length
|
|
||||||
? 'none'
|
|
||||||
: getSelectionType(selection)
|
|
||||||
)
|
|
||||||
}, [selection])
|
|
||||||
|
|
||||||
// Fast-forward through this arg if it's marked as skippable
|
// Fast-forward through this arg if it's marked as skippable
|
||||||
// and we have a valid selection already
|
// and we have a valid selection already
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log('selection input effect', {
|
||||||
|
selectionsByType,
|
||||||
|
canSubmitSelection,
|
||||||
|
arg,
|
||||||
|
})
|
||||||
setCanSubmitSelection(canSubmitSelectionArg(selectionsByType, arg))
|
setCanSubmitSelection(canSubmitSelectionArg(selectionsByType, arg))
|
||||||
const argValue = commandBarState.context.argumentsToSubmit[arg.name]
|
const argValue = commandBarState.context.argumentsToSubmit[arg.name]
|
||||||
if (canSubmitSelection && arg.skip && argValue === undefined) {
|
if (canSubmitSelection && arg.skip && argValue === undefined) {
|
||||||
|
@ -15,10 +15,10 @@ import {
|
|||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { fileMachine } from 'machines/fileMachine'
|
import { fileMachine } from 'machines/fileMachine'
|
||||||
import { mkdir, remove, rename, create } from '@tauri-apps/plugin-fs'
|
import { mkdir, remove, rename, create } from '@tauri-apps/plugin-fs'
|
||||||
import { readProject } from 'lib/tauriFS'
|
|
||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
import { join, sep } from '@tauri-apps/api/path'
|
import { join, sep } from '@tauri-apps/api/path'
|
||||||
import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants'
|
import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants'
|
||||||
|
import { getProjectInfo } from 'lib/tauri'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -62,7 +62,7 @@ export const FileMachineProvider = ({
|
|||||||
services: {
|
services: {
|
||||||
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
|
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
|
||||||
const newFiles = isTauri()
|
const newFiles = isTauri()
|
||||||
? await readProject(context.project.path)
|
? (await getProjectInfo(context.project.path)).children
|
||||||
: []
|
: []
|
||||||
return {
|
return {
|
||||||
...context.project,
|
...context.project,
|
||||||
|
@ -3,7 +3,7 @@ import { paths } from 'lib/paths'
|
|||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import Tooltip from './Tooltip'
|
import Tooltip from './Tooltip'
|
||||||
import { Dispatch, useEffect, useRef, useState } from 'react'
|
import { Dispatch, useEffect, useRef, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
|
||||||
import { Dialog, Disclosure } from '@headlessui/react'
|
import { Dialog, Disclosure } from '@headlessui/react'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
|
import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
|
||||||
@ -133,18 +133,13 @@ const FileTreeItem = ({
|
|||||||
project,
|
project,
|
||||||
currentFile,
|
currentFile,
|
||||||
fileOrDir,
|
fileOrDir,
|
||||||
closePanel,
|
onDoubleClick,
|
||||||
level = 0,
|
level = 0,
|
||||||
}: {
|
}: {
|
||||||
project?: IndexLoaderData['project']
|
project?: IndexLoaderData['project']
|
||||||
currentFile?: IndexLoaderData['file']
|
currentFile?: IndexLoaderData['file']
|
||||||
fileOrDir: FileEntry
|
fileOrDir: FileEntry
|
||||||
closePanel: (
|
onDoubleClick?: () => void
|
||||||
focusableElement?:
|
|
||||||
| HTMLElement
|
|
||||||
| React.MutableRefObject<HTMLElement | null>
|
|
||||||
| undefined
|
|
||||||
) => void
|
|
||||||
level?: number
|
level?: number
|
||||||
}) => {
|
}) => {
|
||||||
const { send, context } = useFileContext()
|
const { send, context } = useFileContext()
|
||||||
@ -186,7 +181,7 @@ const FileTreeItem = ({
|
|||||||
// Open kcl files
|
// Open kcl files
|
||||||
navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`)
|
navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`)
|
||||||
}
|
}
|
||||||
closePanel()
|
onDoubleClick?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -194,8 +189,10 @@ const FileTreeItem = ({
|
|||||||
{fileOrDir.children === undefined ? (
|
{fileOrDir.children === undefined ? (
|
||||||
<li
|
<li
|
||||||
className={
|
className={
|
||||||
'group m-0 p-0 border-solid border-0 hover:text-primary hover:bg-primary/5 focus-within:bg-primary/5 ' +
|
'group m-0 p-0 border-solid border-0 hover:bg-primary/5 focus-within:bg-primary/5 dark:hover:bg-primary/20 dark:focus-within:bg-primary/20 ' +
|
||||||
(isCurrentFile ? '!bg-primary/10 !text-primary' : '')
|
(isCurrentFile
|
||||||
|
? '!bg-primary/10 !text-primary dark:!bg-primary/20 dark:!text-inherit'
|
||||||
|
: '')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{!isRenaming ? (
|
{!isRenaming ? (
|
||||||
@ -227,9 +224,9 @@ const FileTreeItem = ({
|
|||||||
{!isRenaming ? (
|
{!isRenaming ? (
|
||||||
<Disclosure.Button
|
<Disclosure.Button
|
||||||
className={
|
className={
|
||||||
' group border-none text-sm rounded-none p-0 m-0 flex items-center justify-start w-full py-0.5 hover:text-primary hover:bg-primary/5' +
|
' group border-none text-sm rounded-none p-0 m-0 flex items-center justify-start w-full py-0.5 hover:text-primary hover:bg-primary/5 dark:hover:text-inherit dark:hover:bg-primary/10' +
|
||||||
(context.selectedDirectory.path.includes(fileOrDir.path)
|
(context.selectedDirectory.path.includes(fileOrDir.path)
|
||||||
? ' ui-open:text-primary'
|
? ' ui-open:bg-primary/10'
|
||||||
: '')
|
: '')
|
||||||
}
|
}
|
||||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||||
@ -293,7 +290,7 @@ const FileTreeItem = ({
|
|||||||
fileOrDir={child}
|
fileOrDir={child}
|
||||||
project={project}
|
project={project}
|
||||||
currentFile={currentFile}
|
currentFile={currentFile}
|
||||||
closePanel={closePanel}
|
onDoubleClick={onDoubleClick}
|
||||||
level={level + 1}
|
level={level + 1}
|
||||||
key={level + '-' + child.path}
|
key={level + '-' + child.path}
|
||||||
/>
|
/>
|
||||||
@ -325,20 +322,8 @@ interface FileTreeProps {
|
|||||||
) => void
|
) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileTree = ({
|
export const FileTreeMenu = () => {
|
||||||
className = '',
|
const { send } = useFileContext()
|
||||||
file,
|
|
||||||
closePanel,
|
|
||||||
}: FileTreeProps) => {
|
|
||||||
const { send, context } = useFileContext()
|
|
||||||
const docuemntHasFocus = useDocumentHasFocus()
|
|
||||||
useHotkeys('meta + n', createFile)
|
|
||||||
useHotkeys('meta + shift + n', createFolder)
|
|
||||||
|
|
||||||
// Refresh the file tree when the document gets focus
|
|
||||||
useEffect(() => {
|
|
||||||
send({ type: 'Refresh' })
|
|
||||||
}, [docuemntHasFocus])
|
|
||||||
|
|
||||||
async function createFile() {
|
async function createFile() {
|
||||||
send({ type: 'Create file', data: { name: '', makeDir: false } })
|
send({ type: 'Create file', data: { name: '', makeDir: false } })
|
||||||
@ -348,58 +333,88 @@ export const FileTree = ({
|
|||||||
send({ type: 'Create file', data: { name: '', makeDir: true } })
|
send({ type: 'Create file', data: { name: '', makeDir: true } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useHotkeys('meta + n', createFile)
|
||||||
|
useHotkeys('meta + shift + n', createFolder)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
icon={{
|
||||||
|
icon: 'filePlus',
|
||||||
|
iconClassName: '!text-current',
|
||||||
|
bgClassName: 'bg-transparent',
|
||||||
|
}}
|
||||||
|
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
|
||||||
|
onClick={createFile}
|
||||||
|
>
|
||||||
|
<Tooltip position="bottom-right" delay={750}>
|
||||||
|
Create file
|
||||||
|
</Tooltip>
|
||||||
|
</ActionButton>
|
||||||
|
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
icon={{
|
||||||
|
icon: 'folderPlus',
|
||||||
|
iconClassName: '!text-current',
|
||||||
|
bgClassName: 'bg-transparent',
|
||||||
|
}}
|
||||||
|
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
|
||||||
|
onClick={createFolder}
|
||||||
|
>
|
||||||
|
<Tooltip position="bottom-right" delay={750}>
|
||||||
|
Create folder
|
||||||
|
</Tooltip>
|
||||||
|
</ActionButton>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileTree = ({ className = '', closePanel }: FileTreeProps) => {
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<div className="flex items-center gap-1 px-4 py-1 bg-chalkboard-20/40 dark:bg-chalkboard-80/50 border-b border-b-chalkboard-30 dark:border-b-chalkboard-80">
|
<div className="flex items-center gap-1 px-4 py-1 bg-chalkboard-20/40 dark:bg-chalkboard-80/50 border-b border-b-chalkboard-30 dark:border-b-chalkboard-80">
|
||||||
<h2 className="flex-1 m-0 p-0 text-sm mono">Files</h2>
|
<h2 className="flex-1 m-0 p-0 text-sm mono">Files</h2>
|
||||||
<ActionButton
|
<FileTreeMenu />
|
||||||
Element="button"
|
|
||||||
icon={{
|
|
||||||
icon: 'filePlus',
|
|
||||||
iconClassName: '!text-current',
|
|
||||||
bgClassName: 'bg-transparent',
|
|
||||||
}}
|
|
||||||
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
|
|
||||||
onClick={createFile}
|
|
||||||
>
|
|
||||||
<Tooltip position="bottom-right" delay={750}>
|
|
||||||
Create file
|
|
||||||
</Tooltip>
|
|
||||||
</ActionButton>
|
|
||||||
|
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
icon={{
|
|
||||||
icon: 'folderPlus',
|
|
||||||
iconClassName: '!text-current',
|
|
||||||
bgClassName: 'bg-transparent',
|
|
||||||
}}
|
|
||||||
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
|
|
||||||
onClick={createFolder}
|
|
||||||
>
|
|
||||||
<Tooltip position="bottom-right" delay={750}>
|
|
||||||
Create folder
|
|
||||||
</Tooltip>
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
<div className="overflow-auto max-h-full pb-12">
|
|
||||||
<ul
|
|
||||||
className="m-0 p-0 text-sm"
|
|
||||||
onClickCapture={(e) => {
|
|
||||||
send({ type: 'Set selected directory', data: context.project })
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{sortProject(context.project.children || []).map((fileOrDir) => (
|
|
||||||
<FileTreeItem
|
|
||||||
project={context.project}
|
|
||||||
currentFile={file}
|
|
||||||
fileOrDir={fileOrDir}
|
|
||||||
closePanel={closePanel}
|
|
||||||
key={fileOrDir.path}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
<FileTreeInner onDoubleClick={closePanel} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileTreeInner = ({
|
||||||
|
onDoubleClick,
|
||||||
|
}: {
|
||||||
|
onDoubleClick?: () => void
|
||||||
|
}) => {
|
||||||
|
const loaderData = useRouteLoaderData(paths.FILE) as IndexLoaderData
|
||||||
|
const { send, context } = useFileContext()
|
||||||
|
const documentHasFocus = useDocumentHasFocus()
|
||||||
|
|
||||||
|
// Refresh the file tree when the document gets focus
|
||||||
|
useEffect(() => {
|
||||||
|
send({ type: 'Refresh' })
|
||||||
|
}, [documentHasFocus])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-auto max-h-full pb-12">
|
||||||
|
<ul
|
||||||
|
className="m-0 p-0 text-sm"
|
||||||
|
onClickCapture={(e) => {
|
||||||
|
send({ type: 'Set selected directory', data: context.project })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sortProject(context.project.children || []).map((fileOrDir) => (
|
||||||
|
<FileTreeItem
|
||||||
|
project={context.project}
|
||||||
|
currentFile={loaderData?.file}
|
||||||
|
fileOrDir={fileOrDir}
|
||||||
|
onDoubleClick={onDoubleClick}
|
||||||
|
key={fileOrDir.path}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -94,10 +94,7 @@ export function HelpMenu(props: React.PropsWithChildren) {
|
|||||||
if (isInProject) {
|
if (isInProject) {
|
||||||
navigate('onboarding')
|
navigate('onboarding')
|
||||||
} else {
|
} else {
|
||||||
createAndOpenNewProject(
|
createAndOpenNewProject(navigate)
|
||||||
settings.context.app.projectDirectory.current,
|
|
||||||
navigate
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -17,6 +17,7 @@ import {
|
|||||||
sceneInfra,
|
sceneInfra,
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
codeManager,
|
codeManager,
|
||||||
|
editorManager,
|
||||||
} from 'lib/singletons'
|
} from 'lib/singletons'
|
||||||
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
|
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
|
||||||
import {
|
import {
|
||||||
@ -53,10 +54,9 @@ import { exportFromEngine } from 'lib/exportFromEngine'
|
|||||||
import { Models } from '@kittycad/lib/dist/types/src'
|
import { Models } from '@kittycad/lib/dist/types/src'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { EditorSelection } from '@uiw/react-codemirror'
|
import { EditorSelection } from '@uiw/react-codemirror'
|
||||||
import { Vector3 } from 'three'
|
|
||||||
import { quaternionFromUpNForward } from 'clientSideScene/helpers'
|
|
||||||
import { CoreDumpManager } from 'lib/coredump'
|
import { CoreDumpManager } from 'lib/coredump'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -77,7 +77,7 @@ export const ModelingMachineProvider = ({
|
|||||||
auth,
|
auth,
|
||||||
settings: {
|
settings: {
|
||||||
context: {
|
context: {
|
||||||
app: { theme },
|
app: { theme, enableSSAO },
|
||||||
modeling: { defaultUnit, highlightEdges },
|
modeling: { defaultUnit, highlightEdges },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -87,6 +87,7 @@ export const ModelingMachineProvider = ({
|
|||||||
useSetupEngineManager(streamRef, token, {
|
useSetupEngineManager(streamRef, token, {
|
||||||
theme: theme.current,
|
theme: theme.current,
|
||||||
highlightEdges: highlightEdges.current,
|
highlightEdges: highlightEdges.current,
|
||||||
|
enableSSAO: enableSSAO.current,
|
||||||
})
|
})
|
||||||
const { htmlRef } = useStore((s) => ({
|
const { htmlRef } = useStore((s) => ({
|
||||||
htmlRef: s.htmlRef,
|
htmlRef: s.htmlRef,
|
||||||
@ -98,17 +99,6 @@ export const ModelingMachineProvider = ({
|
|||||||
)
|
)
|
||||||
useHotkeys('meta + shift + .', () => coreDump(coreDumpManager, true))
|
useHotkeys('meta + shift + .', () => coreDump(coreDumpManager, true))
|
||||||
|
|
||||||
const {
|
|
||||||
isShiftDown,
|
|
||||||
editorView,
|
|
||||||
setLastCodeMirrorSelectionUpdatedFromScene,
|
|
||||||
} = useStore((s) => ({
|
|
||||||
isShiftDown: s.isShiftDown,
|
|
||||||
editorView: s.editorView,
|
|
||||||
setLastCodeMirrorSelectionUpdatedFromScene:
|
|
||||||
s.setLastCodeMirrorSelectionUpdatedFromScene,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Settings machine setup
|
// Settings machine setup
|
||||||
// const retrievedSettings = useRef(
|
// const retrievedSettings = useRef(
|
||||||
// localStorage?.getItem(MODELING_PERSIST_KEY) || '{}'
|
// localStorage?.getItem(MODELING_PERSIST_KEY) || '{}'
|
||||||
@ -135,29 +125,33 @@ export const ModelingMachineProvider = ({
|
|||||||
'Set selection': assign(({ selectionRanges }, event) => {
|
'Set selection': assign(({ selectionRanges }, event) => {
|
||||||
if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
|
if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
|
||||||
const setSelections = event.data
|
const setSelections = event.data
|
||||||
if (!editorView) return {}
|
if (!editorManager.editorView) return {}
|
||||||
const dispatchSelection = (selection?: EditorSelection) => {
|
const dispatchSelection = (selection?: EditorSelection) => {
|
||||||
if (!selection) return // TODO less of hack for the below please
|
if (!selection) return // TODO less of hack for the below please
|
||||||
setLastCodeMirrorSelectionUpdatedFromScene(Date.now())
|
editorManager.lastSelectionEvent = Date.now()
|
||||||
setTimeout(() => editorView.dispatch({ selection }))
|
setTimeout(() => {
|
||||||
|
if (editorManager.editorView) {
|
||||||
|
editorManager.editorView.dispatch({ selection })
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
let selections: Selections = {
|
let selections: Selections = {
|
||||||
codeBasedSelections: [],
|
codeBasedSelections: [],
|
||||||
otherSelections: [],
|
otherSelections: [],
|
||||||
}
|
}
|
||||||
if (setSelections.selectionType === 'singleCodeCursor') {
|
if (setSelections.selectionType === 'singleCodeCursor') {
|
||||||
if (!setSelections.selection && isShiftDown) {
|
if (!setSelections.selection && editorManager.isShiftDown) {
|
||||||
} else if (!setSelections.selection && !isShiftDown) {
|
} else if (!setSelections.selection && !editorManager.isShiftDown) {
|
||||||
selections = {
|
selections = {
|
||||||
codeBasedSelections: [],
|
codeBasedSelections: [],
|
||||||
otherSelections: [],
|
otherSelections: [],
|
||||||
}
|
}
|
||||||
} else if (setSelections.selection && !isShiftDown) {
|
} else if (setSelections.selection && !editorManager.isShiftDown) {
|
||||||
selections = {
|
selections = {
|
||||||
codeBasedSelections: [setSelections.selection],
|
codeBasedSelections: [setSelections.selection],
|
||||||
otherSelections: [],
|
otherSelections: [],
|
||||||
}
|
}
|
||||||
} else if (setSelections.selection && isShiftDown) {
|
} else if (setSelections.selection && editorManager.isShiftDown) {
|
||||||
selections = {
|
selections = {
|
||||||
codeBasedSelections: [
|
codeBasedSelections: [
|
||||||
...selectionRanges.codeBasedSelections,
|
...selectionRanges.codeBasedSelections,
|
||||||
@ -180,6 +174,7 @@ export const ModelingMachineProvider = ({
|
|||||||
engineCommandManager.sendSceneCommand(event)
|
engineCommandManager.sendSceneCommand(event)
|
||||||
)
|
)
|
||||||
updateSceneObjectColors()
|
updateSceneObjectColors()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectionRanges: selections,
|
selectionRanges: selections,
|
||||||
}
|
}
|
||||||
@ -192,7 +187,7 @@ export const ModelingMachineProvider = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (setSelections.selectionType === 'otherSelection') {
|
if (setSelections.selectionType === 'otherSelection') {
|
||||||
if (isShiftDown) {
|
if (editorManager.isShiftDown) {
|
||||||
selections = {
|
selections = {
|
||||||
codeBasedSelections: selectionRanges.codeBasedSelections,
|
codeBasedSelections: selectionRanges.codeBasedSelections,
|
||||||
otherSelections: [setSelections.selection],
|
otherSelections: [setSelections.selection],
|
||||||
@ -273,10 +268,12 @@ export const ModelingMachineProvider = ({
|
|||||||
'has valid extrude selection': ({ selectionRanges }) => {
|
'has valid extrude selection': ({ selectionRanges }) => {
|
||||||
// A user can begin extruding if they either have 1+ faces selected or nothing selected
|
// A user can begin extruding if they either have 1+ faces selected or nothing selected
|
||||||
// TODO: I believe this guard only allows for extruding a single face at a time
|
// TODO: I believe this guard only allows for extruding a single face at a time
|
||||||
if (selectionRanges.codeBasedSelections.length < 1) return false
|
|
||||||
const isPipe = isSketchPipe(selectionRanges)
|
const isPipe = isSketchPipe(selectionRanges)
|
||||||
|
|
||||||
if (isSelectionLastLine(selectionRanges, codeManager.code))
|
if (
|
||||||
|
selectionRanges.codeBasedSelections.length === 0 ||
|
||||||
|
isSelectionLastLine(selectionRanges, codeManager.code)
|
||||||
|
)
|
||||||
return true
|
return true
|
||||||
if (!isPipe) return false
|
if (!isPipe) return false
|
||||||
|
|
||||||
@ -324,16 +321,9 @@ export const ModelingMachineProvider = ({
|
|||||||
)
|
)
|
||||||
await kclManager.executeAstMock(modifiedAst)
|
await kclManager.executeAstMock(modifiedAst)
|
||||||
|
|
||||||
const forward = new Vector3(...data.zAxis)
|
await letEngineAnimateAndSyncCamAfter(
|
||||||
const up = new Vector3(...data.yAxis)
|
engineCommandManager,
|
||||||
|
data.faceId
|
||||||
let target = new Vector3(...data.position).multiplyScalar(
|
|
||||||
sceneInfra._baseUnitMultiplier
|
|
||||||
)
|
|
||||||
const quaternion = quaternionFromUpNForward(up, forward)
|
|
||||||
await sceneInfra.camControls.tweenCameraToQuaternion(
|
|
||||||
quaternion,
|
|
||||||
target
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -348,6 +338,7 @@ export const ModelingMachineProvider = ({
|
|||||||
data.plane
|
data.plane
|
||||||
)
|
)
|
||||||
await kclManager.updateAst(modifiedAst, false)
|
await kclManager.updateAst(modifiedAst, false)
|
||||||
|
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||||||
const quat = await getSketchQuaternion(pathToNode, data.zAxis)
|
const quat = await getSketchQuaternion(pathToNode, data.zAxis)
|
||||||
await sceneInfra.camControls.tweenCameraToQuaternion(quat)
|
await sceneInfra.camControls.tweenCameraToQuaternion(quat)
|
||||||
return {
|
return {
|
||||||
@ -364,9 +355,9 @@ export const ModelingMachineProvider = ({
|
|||||||
sourceRange
|
sourceRange
|
||||||
)
|
)
|
||||||
const info = await getSketchOrientationDetails(sketchPathToNode || [])
|
const info = await getSketchOrientationDetails(sketchPathToNode || [])
|
||||||
await sceneInfra.camControls.tweenCameraToQuaternion(
|
await letEngineAnimateAndSyncCamAfter(
|
||||||
info.quat,
|
engineCommandManager,
|
||||||
new Vector3(...info.sketchDetails.origin)
|
info?.sketchDetails?.faceId || ''
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
sketchPathToNode: sketchPathToNode || [],
|
sketchPathToNode: sketchPathToNode || [],
|
||||||
@ -516,6 +507,19 @@ export const ModelingMachineProvider = ({
|
|||||||
})
|
})
|
||||||
}, [modelingSend])
|
}, [modelingSend])
|
||||||
|
|
||||||
|
// Give the state back to the editorManager.
|
||||||
|
useEffect(() => {
|
||||||
|
editorManager.modelingSend = modelingSend
|
||||||
|
}, [modelingSend])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
editorManager.modelingEvent = modelingState.event
|
||||||
|
}, [modelingState.event])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
editorManager.selectionRanges = modelingState.context.selectionRanges
|
||||||
|
}, [modelingState.context.selectionRanges])
|
||||||
|
|
||||||
useStateMachineCommands({
|
useStateMachineCommands({
|
||||||
machineId: 'modeling',
|
machineId: 'modeling',
|
||||||
state: modelingState,
|
state: modelingState,
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
import { undo, redo } from '@codemirror/commands'
|
|
||||||
import ReactCodeMirror from '@uiw/react-codemirror'
|
import ReactCodeMirror from '@uiw/react-codemirror'
|
||||||
import { TEST } from 'env'
|
import { TEST } from 'env'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
|
||||||
import { Themes, getSystemTheme } from 'lib/theme'
|
import { Themes, getSystemTheme } from 'lib/theme'
|
||||||
import { useEffect, useMemo, useRef } from 'react'
|
import { useEffect, useMemo } from 'react'
|
||||||
import { useStore } from 'useStore'
|
|
||||||
import { processCodeMirrorRanges } from 'lib/selections'
|
|
||||||
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'
|
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'
|
||||||
import { lineHighlightField } from 'editor/highlightextension'
|
import { lineHighlightField } from 'editor/highlightextension'
|
||||||
import { roundOff } from 'lib/utils'
|
import { roundOff } from 'lib/utils'
|
||||||
@ -21,7 +16,6 @@ import {
|
|||||||
EditorView,
|
EditorView,
|
||||||
dropCursor,
|
dropCursor,
|
||||||
drawSelection,
|
drawSelection,
|
||||||
ViewUpdate,
|
|
||||||
} from '@codemirror/view'
|
} from '@codemirror/view'
|
||||||
import {
|
import {
|
||||||
indentWithTab,
|
indentWithTab,
|
||||||
@ -29,7 +23,7 @@ import {
|
|||||||
historyKeymap,
|
historyKeymap,
|
||||||
history,
|
history,
|
||||||
} from '@codemirror/commands'
|
} from '@codemirror/commands'
|
||||||
import { lintGutter, lintKeymap, linter } from '@codemirror/lint'
|
import { lintGutter, lintKeymap } from '@codemirror/lint'
|
||||||
import {
|
import {
|
||||||
foldGutter,
|
foldGutter,
|
||||||
foldKeymap,
|
foldKeymap,
|
||||||
@ -39,25 +33,20 @@ import {
|
|||||||
syntaxHighlighting,
|
syntaxHighlighting,
|
||||||
defaultHighlightStyle,
|
defaultHighlightStyle,
|
||||||
} from '@codemirror/language'
|
} from '@codemirror/language'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
|
||||||
import interact from '@replit/codemirror-interact'
|
import interact from '@replit/codemirror-interact'
|
||||||
import { engineCommandManager, sceneInfra, kclManager } from 'lib/singletons'
|
import { kclManager, editorManager, codeManager } from 'lib/singletons'
|
||||||
import { useKclContext } from 'lang/KclProvider'
|
|
||||||
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { paths } from 'lib/paths'
|
import { paths } from 'lib/paths'
|
||||||
import makeUrlPathRelative from 'lib/makeUrlPathRelative'
|
import makeUrlPathRelative from 'lib/makeUrlPathRelative'
|
||||||
import { useLspContext } from 'components/LspProvider'
|
import { useLspContext } from 'components/LspProvider'
|
||||||
import { Prec, EditorState, Extension, SelectionRange } from '@codemirror/state'
|
import { Prec, EditorState, Extension } from '@codemirror/state'
|
||||||
import {
|
import {
|
||||||
closeBrackets,
|
closeBrackets,
|
||||||
closeBracketsKeymap,
|
closeBracketsKeymap,
|
||||||
completionKeymap,
|
completionKeymap,
|
||||||
hasNextSnippetField,
|
|
||||||
} from '@codemirror/autocomplete'
|
} from '@codemirror/autocomplete'
|
||||||
import { kclErrorsToDiagnostics } from 'lang/errors'
|
|
||||||
|
|
||||||
export const editorShortcutMeta = {
|
export const editorShortcutMeta = {
|
||||||
formatCode: {
|
formatCode: {
|
||||||
@ -77,13 +66,6 @@ export const KclEditorPane = () => {
|
|||||||
context.app.theme.current === Themes.System
|
context.app.theme.current === Themes.System
|
||||||
? getSystemTheme()
|
? getSystemTheme()
|
||||||
: context.app.theme.current
|
: context.app.theme.current
|
||||||
const { editorView, setEditorView, isShiftDown } = useStore((s) => ({
|
|
||||||
editorView: s.editorView,
|
|
||||||
setEditorView: s.setEditorView,
|
|
||||||
isShiftDown: s.isShiftDown,
|
|
||||||
}))
|
|
||||||
const { editorCode, errors } = useKclContext()
|
|
||||||
const lastEvent = useRef({ event: '', time: Date.now() })
|
|
||||||
const { copilotLSP, kclLSP } = useLspContext()
|
const { copilotLSP, kclLSP } = useLspContext()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
@ -96,90 +78,15 @@ export const KclEditorPane = () => {
|
|||||||
|
|
||||||
useHotkeys('mod+z', (e) => {
|
useHotkeys('mod+z', (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (editorView) {
|
editorManager.undo()
|
||||||
undo(editorView)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
useHotkeys('mod+shift+z', (e) => {
|
useHotkeys('mod+shift+z', (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (editorView) {
|
editorManager.redo()
|
||||||
redo(editorView)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const {
|
const textWrapping = context.textEditor.textWrapping
|
||||||
context: { selectionRanges },
|
const cursorBlinking = context.textEditor.blinkingCursor
|
||||||
send,
|
|
||||||
state,
|
|
||||||
} = useModelingContext()
|
|
||||||
|
|
||||||
const { settings } = useSettingsAuthContext()
|
|
||||||
const textWrapping = settings.context.textEditor.textWrapping
|
|
||||||
const cursorBlinking = settings.context.textEditor.blinkingCursor
|
|
||||||
const { commandBarSend } = useCommandsContext()
|
|
||||||
const { enable: convertEnabled, handleClick: convertCallback } =
|
|
||||||
useConvertToVariable()
|
|
||||||
|
|
||||||
const lastSelection = useRef('')
|
|
||||||
const onUpdate = (viewUpdate: ViewUpdate) => {
|
|
||||||
// If we are just fucking around in a snippet, return early and don't
|
|
||||||
// trigger stuff below that might cause the component to re-render.
|
|
||||||
// Otherwise we will not be able to tab thru the snippet portions.
|
|
||||||
// We explicitly dont check HasPrevSnippetField because we always add
|
|
||||||
// a ${} to the end of the function so that's fine.
|
|
||||||
if (hasNextSnippetField(viewUpdate.view.state)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!editorView) {
|
|
||||||
setEditorView(viewUpdate.view)
|
|
||||||
}
|
|
||||||
const selString = stringifyRanges(
|
|
||||||
viewUpdate?.state?.selection?.ranges || []
|
|
||||||
)
|
|
||||||
if (selString === lastSelection.current) {
|
|
||||||
// onUpdate is noisy and is fired a lot by extensions
|
|
||||||
// since we're only interested in selections changes we can ignore most of these.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
lastSelection.current = selString
|
|
||||||
|
|
||||||
if (
|
|
||||||
// TODO find a less lazy way of getting the last
|
|
||||||
Date.now() - useStore.getState().lastCodeMirrorSelectionUpdatedFromScene <
|
|
||||||
150
|
|
||||||
)
|
|
||||||
return // update triggered by scene selection
|
|
||||||
if (sceneInfra.selected) return // mid drag
|
|
||||||
const ignoreEvents: ModelingMachineEvent['type'][] = [
|
|
||||||
'Equip Line tool',
|
|
||||||
'Equip tangential arc to',
|
|
||||||
]
|
|
||||||
if (ignoreEvents.includes(state.event.type)) return
|
|
||||||
const eventInfo = processCodeMirrorRanges({
|
|
||||||
codeMirrorRanges: viewUpdate.state.selection.ranges,
|
|
||||||
selectionRanges,
|
|
||||||
isShiftDown,
|
|
||||||
})
|
|
||||||
if (!eventInfo) return
|
|
||||||
const deterministicEventInfo = {
|
|
||||||
...eventInfo,
|
|
||||||
engineEvents: eventInfo.engineEvents.map((e) => ({
|
|
||||||
...e,
|
|
||||||
cmd_id: 'static',
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
const stringEvent = JSON.stringify(deterministicEventInfo)
|
|
||||||
if (
|
|
||||||
stringEvent === lastEvent.current.event &&
|
|
||||||
Date.now() - lastEvent.current.time < 500
|
|
||||||
)
|
|
||||||
return // don't repeat events
|
|
||||||
lastEvent.current = { event: stringEvent, time: Date.now() }
|
|
||||||
send(eventInfo.modelingEvent)
|
|
||||||
eventInfo.engineEvents.forEach((event) =>
|
|
||||||
engineCommandManager.sendSceneCommand(event)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const editorExtensions = useMemo(() => {
|
const editorExtensions = useMemo(() => {
|
||||||
const extensions = [
|
const extensions = [
|
||||||
@ -202,7 +109,7 @@ export const KclEditorPane = () => {
|
|||||||
{
|
{
|
||||||
key: 'Meta-k',
|
key: 'Meta-k',
|
||||||
run: () => {
|
run: () => {
|
||||||
commandBarSend({ type: 'Open' })
|
editorManager.commandBarSend({ type: 'Open' })
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -216,11 +123,7 @@ export const KclEditorPane = () => {
|
|||||||
{
|
{
|
||||||
key: editorShortcutMeta.convertToVariable.codeMirror,
|
key: editorShortcutMeta.convertToVariable.codeMirror,
|
||||||
run: () => {
|
run: () => {
|
||||||
if (convertEnabled) {
|
return editorManager.convertToVariable()
|
||||||
convertCallback()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
@ -233,9 +136,6 @@ export const KclEditorPane = () => {
|
|||||||
if (!TEST) {
|
if (!TEST) {
|
||||||
extensions.push(
|
extensions.push(
|
||||||
lintGutter(),
|
lintGutter(),
|
||||||
linter((_view: EditorView) => {
|
|
||||||
return kclErrorsToDiagnostics(errors)
|
|
||||||
}),
|
|
||||||
lineNumbers(),
|
lineNumbers(),
|
||||||
highlightActiveLineGutter(),
|
highlightActiveLineGutter(),
|
||||||
highlightSpecialChars(),
|
highlightSpecialChars(),
|
||||||
@ -288,13 +188,7 @@ export const KclEditorPane = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return extensions
|
return extensions
|
||||||
}, [
|
}, [kclLSP, copilotLSP, textWrapping.current, cursorBlinking.current])
|
||||||
kclLSP,
|
|
||||||
copilotLSP,
|
|
||||||
textWrapping.current,
|
|
||||||
cursorBlinking.current,
|
|
||||||
convertCallback,
|
|
||||||
])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -302,18 +196,15 @@ export const KclEditorPane = () => {
|
|||||||
className={'absolute inset-0 ' + (cursorBlinking.current ? 'blink' : '')}
|
className={'absolute inset-0 ' + (cursorBlinking.current ? 'blink' : '')}
|
||||||
>
|
>
|
||||||
<ReactCodeMirror
|
<ReactCodeMirror
|
||||||
value={editorCode}
|
value={codeManager.code}
|
||||||
extensions={editorExtensions}
|
extensions={editorExtensions}
|
||||||
onUpdate={onUpdate}
|
|
||||||
theme={theme}
|
theme={theme}
|
||||||
onCreateEditor={(_editorView) => setEditorView(_editorView)}
|
onCreateEditor={(_editorView) =>
|
||||||
|
editorManager.setEditorView(_editorView)
|
||||||
|
}
|
||||||
indentWithTab={false}
|
indentWithTab={false}
|
||||||
basicSetup={false}
|
basicSetup={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stringifyRanges(ranges: readonly SelectionRange[]): string {
|
|
||||||
return ranges.map(({ to, from }) => `${to}->${from}`).join('&')
|
|
||||||
}
|
|
||||||
|
@ -2,7 +2,9 @@ import { processMemory } from './MemoryPane'
|
|||||||
import { enginelessExecutor } from '../../../lib/testHelpers'
|
import { enginelessExecutor } from '../../../lib/testHelpers'
|
||||||
import { initPromise, parse } from '../../../lang/wasm'
|
import { initPromise, parse } from '../../../lang/wasm'
|
||||||
|
|
||||||
beforeAll(() => initPromise)
|
beforeAll(async () => {
|
||||||
|
await initPromise
|
||||||
|
})
|
||||||
|
|
||||||
describe('processMemory', () => {
|
describe('processMemory', () => {
|
||||||
it('should grab the values and remove and geo data', async () => {
|
it('should grab the values and remove and geo data', async () => {
|
||||||
|
@ -10,21 +10,32 @@ import { KclEditorMenu } from 'components/ModelingSidebar/ModelingPanes/KclEdito
|
|||||||
import { CustomIconName } from 'components/CustomIcon'
|
import { CustomIconName } from 'components/CustomIcon'
|
||||||
import { KclEditorPane } from 'components/ModelingSidebar/ModelingPanes/KclEditorPane'
|
import { KclEditorPane } from 'components/ModelingSidebar/ModelingPanes/KclEditorPane'
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
import type { PaneType } from 'useStore'
|
|
||||||
import { MemoryPane } from './MemoryPane'
|
import { MemoryPane } from './MemoryPane'
|
||||||
import { KclErrorsPane, LogsPane } from './LoggingPanes'
|
import { KclErrorsPane, LogsPane } from './LoggingPanes'
|
||||||
import { DebugPane } from './DebugPane'
|
import { DebugPane } from './DebugPane'
|
||||||
|
import { FileTreeInner, FileTreeMenu } from 'components/FileTree'
|
||||||
|
|
||||||
export type Pane = {
|
export type SidebarType =
|
||||||
id: PaneType
|
| 'code'
|
||||||
|
| 'debug'
|
||||||
|
| 'export'
|
||||||
|
| 'files'
|
||||||
|
| 'kclErrors'
|
||||||
|
| 'logs'
|
||||||
|
| 'lspMessages'
|
||||||
|
| 'variables'
|
||||||
|
|
||||||
|
export type SidebarPane = {
|
||||||
|
id: SidebarType
|
||||||
title: string
|
title: string
|
||||||
icon: CustomIconName | IconDefinition
|
icon: CustomIconName | IconDefinition
|
||||||
|
keybinding: string
|
||||||
Content: ReactNode | React.FC
|
Content: ReactNode | React.FC
|
||||||
Menu?: ReactNode | React.FC
|
Menu?: ReactNode | React.FC
|
||||||
keybinding: string
|
hideOnPlatform?: 'desktop' | 'web'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const topPanes: Pane[] = [
|
export const topPanes: SidebarPane[] = [
|
||||||
{
|
{
|
||||||
id: 'code',
|
id: 'code',
|
||||||
title: 'KCL Code',
|
title: 'KCL Code',
|
||||||
@ -33,9 +44,18 @@ export const topPanes: Pane[] = [
|
|||||||
keybinding: 'shift + c',
|
keybinding: 'shift + c',
|
||||||
Menu: KclEditorMenu,
|
Menu: KclEditorMenu,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'files',
|
||||||
|
title: 'Project Files',
|
||||||
|
icon: 'folder',
|
||||||
|
Content: FileTreeInner,
|
||||||
|
keybinding: 'shift + f',
|
||||||
|
Menu: FileTreeMenu,
|
||||||
|
hideOnPlatform: 'web',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const bottomPanes: Pane[] = [
|
export const bottomPanes: SidebarPane[] = [
|
||||||
{
|
{
|
||||||
id: 'variables',
|
id: 'variables',
|
||||||
title: 'Variables',
|
title: 'Variables',
|
||||||
|
@ -2,13 +2,19 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|||||||
import { Resizable } from 're-resizable'
|
import { Resizable } from 're-resizable'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { PaneType, useStore } from 'useStore'
|
import { useStore } from 'useStore'
|
||||||
import { Tab } from '@headlessui/react'
|
import { Tab } from '@headlessui/react'
|
||||||
import { Pane, bottomPanes, topPanes } from './ModelingPanes'
|
import {
|
||||||
|
SidebarPane,
|
||||||
|
SidebarType,
|
||||||
|
bottomPanes,
|
||||||
|
topPanes,
|
||||||
|
} from './ModelingPanes'
|
||||||
import Tooltip from 'components/Tooltip'
|
import Tooltip from 'components/Tooltip'
|
||||||
import { ActionIcon } from 'components/ActionIcon'
|
import { ActionIcon } from 'components/ActionIcon'
|
||||||
import styles from './ModelingSidebar.module.css'
|
import styles from './ModelingSidebar.module.css'
|
||||||
import { ModelingPane } from './ModelingPane'
|
import { ModelingPane } from './ModelingPane'
|
||||||
|
import { isTauri } from 'lib/isTauri'
|
||||||
|
|
||||||
interface ModelingSidebarProps {
|
interface ModelingSidebarProps {
|
||||||
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
||||||
@ -52,7 +58,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ModelingSidebarSectionProps {
|
interface ModelingSidebarSectionProps {
|
||||||
panes: Pane[]
|
panes: SidebarPane[]
|
||||||
alignButtons?: 'start' | 'end'
|
alignButtons?: 'start' | 'end'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,11 +75,11 @@ function ModelingSidebarSection({
|
|||||||
}))
|
}))
|
||||||
const foundOpenPane = openPanes.find((pane) => paneIds.includes(pane))
|
const foundOpenPane = openPanes.find((pane) => paneIds.includes(pane))
|
||||||
const [currentPane, setCurrentPane] = useState(
|
const [currentPane, setCurrentPane] = useState(
|
||||||
foundOpenPane || ('none' as PaneType | 'none')
|
foundOpenPane || ('none' as SidebarType | 'none')
|
||||||
)
|
)
|
||||||
|
|
||||||
const togglePane = useCallback(
|
const togglePane = useCallback(
|
||||||
(newPane: PaneType | 'none') => {
|
(newPane: SidebarType | 'none') => {
|
||||||
if (newPane === 'none') {
|
if (newPane === 'none') {
|
||||||
setOpenPanes(openPanes.filter((p) => p !== currentPane))
|
setOpenPanes(openPanes.filter((p) => p !== currentPane))
|
||||||
setCurrentPane('none')
|
setCurrentPane('none')
|
||||||
@ -90,9 +96,15 @@ function ModelingSidebarSection({
|
|||||||
|
|
||||||
// Filter out the debug panel if it's not supposed to be shown
|
// Filter out the debug panel if it's not supposed to be shown
|
||||||
// TODO: abstract out for allowing user to configure which panes to show
|
// TODO: abstract out for allowing user to configure which panes to show
|
||||||
const filteredPanes = showDebugPanel.current
|
const filteredPanes = (
|
||||||
? panes
|
showDebugPanel.current ? panes : panes.filter((pane) => pane.id !== 'debug')
|
||||||
: panes.filter((pane) => pane.id !== 'debug')
|
).filter(
|
||||||
|
(pane) =>
|
||||||
|
!pane.hideOnPlatform ||
|
||||||
|
(isTauri()
|
||||||
|
? pane.hideOnPlatform === 'web'
|
||||||
|
: pane.hideOnPlatform === 'desktop')
|
||||||
|
)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
!showDebugPanel.current &&
|
!showDebugPanel.current &&
|
||||||
@ -168,8 +180,8 @@ function ModelingSidebarSection({
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ModelingPaneButtonProps {
|
interface ModelingPaneButtonProps {
|
||||||
paneConfig: Pane
|
paneConfig: SidebarPane
|
||||||
currentPane: PaneType | 'none'
|
currentPane: SidebarType | 'none'
|
||||||
togglePane: () => void
|
togglePane: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { FormEvent, useEffect, useRef, useState } from 'react'
|
import { FormEvent, useEffect, useRef, useState } from 'react'
|
||||||
import { type ProjectWithEntryPointMetadata } from 'lib/types'
|
|
||||||
import { paths } from 'lib/paths'
|
import { paths } from 'lib/paths'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
@ -9,11 +8,11 @@ import {
|
|||||||
faTrashAlt,
|
faTrashAlt,
|
||||||
faX,
|
faX,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { getPartsCount, readProject } from '../lib/tauriFS'
|
|
||||||
import { FILE_EXT } from 'lib/constants'
|
import { FILE_EXT } from 'lib/constants'
|
||||||
import { Dialog } from '@headlessui/react'
|
import { Dialog } from '@headlessui/react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import Tooltip from './Tooltip'
|
import Tooltip from './Tooltip'
|
||||||
|
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||||
|
|
||||||
function ProjectCard({
|
function ProjectCard({
|
||||||
project,
|
project,
|
||||||
@ -21,17 +20,17 @@ function ProjectCard({
|
|||||||
handleDeleteProject,
|
handleDeleteProject,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
project: ProjectWithEntryPointMetadata
|
project: Project
|
||||||
handleRenameProject: (
|
handleRenameProject: (
|
||||||
e: FormEvent<HTMLFormElement>,
|
e: FormEvent<HTMLFormElement>,
|
||||||
f: ProjectWithEntryPointMetadata
|
f: Project
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
handleDeleteProject: (f: ProjectWithEntryPointMetadata) => Promise<void>
|
handleDeleteProject: (f: Project) => Promise<void>
|
||||||
}) {
|
}) {
|
||||||
useHotkeys('esc', () => setIsEditing(false))
|
useHotkeys('esc', () => setIsEditing(false))
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
||||||
const [numberOfParts, setNumberOfParts] = useState(1)
|
const [numberOfFiles, setNumberOfFiles] = useState(1)
|
||||||
const [numberOfFolders, setNumberOfFolders] = useState(0)
|
const [numberOfFolders, setNumberOfFolders] = useState(0)
|
||||||
|
|
||||||
let inputRef = useRef<HTMLInputElement>(null)
|
let inputRef = useRef<HTMLInputElement>(null)
|
||||||
@ -41,7 +40,8 @@ function ProjectCard({
|
|||||||
void handleRenameProject(e, project).then(() => setIsEditing(false))
|
void handleRenameProject(e, project).then(() => setIsEditing(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDisplayedTime(date: Date) {
|
function getDisplayedTime(dateStr: string) {
|
||||||
|
const date = new Date(dateStr)
|
||||||
const startOfToday = new Date()
|
const startOfToday = new Date()
|
||||||
startOfToday.setHours(0, 0, 0, 0)
|
startOfToday.setHours(0, 0, 0, 0)
|
||||||
return date.getTime() < startOfToday.getTime()
|
return date.getTime() < startOfToday.getTime()
|
||||||
@ -50,15 +50,12 @@ function ProjectCard({
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getNumberOfParts() {
|
async function getNumberOfFiles() {
|
||||||
const { kclFileCount, kclDirCount } = getPartsCount(
|
setNumberOfFiles(project.kcl_file_count)
|
||||||
await readProject(project.path)
|
setNumberOfFolders(project.directory_count)
|
||||||
)
|
|
||||||
setNumberOfParts(kclFileCount)
|
|
||||||
setNumberOfFolders(kclDirCount)
|
|
||||||
}
|
}
|
||||||
void getNumberOfParts()
|
void getNumberOfFiles()
|
||||||
}, [project.path])
|
}, [project.kcl_file_count, project.directory_count])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inputRef.current) {
|
if (inputRef.current) {
|
||||||
@ -129,7 +126,7 @@ function ProjectCard({
|
|||||||
{project.name?.replace(FILE_EXT, '')}
|
{project.name?.replace(FILE_EXT, '')}
|
||||||
</Link>
|
</Link>
|
||||||
<span className="text-chalkboard-60 text-xs">
|
<span className="text-chalkboard-60 text-xs">
|
||||||
{numberOfParts} part{numberOfParts === 1 ? '' : 's'}{' '}
|
{numberOfFiles} file{numberOfFiles === 1 ? '' : 's'}{' '}
|
||||||
{numberOfFolders > 0 &&
|
{numberOfFolders > 0 &&
|
||||||
`/ ${numberOfFolders} folder${
|
`/ ${numberOfFolders} folder${
|
||||||
numberOfFolders === 1 ? '' : 's'
|
numberOfFolders === 1 ? '' : 's'
|
||||||
@ -137,8 +134,8 @@ function ProjectCard({
|
|||||||
</span>
|
</span>
|
||||||
<span className="text-chalkboard-60 text-xs">
|
<span className="text-chalkboard-60 text-xs">
|
||||||
Edited{' '}
|
Edited{' '}
|
||||||
{project.entrypointMetadata.mtime
|
{project.metadata && project.metadata?.modified
|
||||||
? getDisplayedTime(project.entrypointMetadata.mtime)
|
? getDisplayedTime(project.metadata.modified)
|
||||||
: 'never'}
|
: 'never'}
|
||||||
</span>
|
</span>
|
||||||
<div className="absolute z-10 bottom-2 right-2 flex gap-1 items-center opacity-0 group-hover:opacity-100 group-focus-within:opacity-100">
|
<div className="absolute z-10 bottom-2 right-2 flex gap-1 items-center opacity-0 group-hover:opacity-100 group-focus-within:opacity-100">
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||||
import { type ProjectWithEntryPointMetadata } from 'lib/types'
|
|
||||||
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
||||||
import { APP_NAME } from 'lib/constants'
|
import { APP_NAME } from 'lib/constants'
|
||||||
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
|
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
|
||||||
|
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const projectWellFormed = {
|
const projectWellFormed = {
|
||||||
@ -14,29 +14,17 @@ const projectWellFormed = {
|
|||||||
{
|
{
|
||||||
name: 'main.kcl',
|
name: 'main.kcl',
|
||||||
path: '/some/path/Simple Box/main.kcl',
|
path: '/some/path/Simple Box/main.kcl',
|
||||||
|
children: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
entrypointMetadata: {
|
metadata: {
|
||||||
atime: now,
|
created: now.toISOString(),
|
||||||
blksize: 32,
|
modified: now.toISOString(),
|
||||||
blocks: 32,
|
|
||||||
birthtime: now,
|
|
||||||
dev: 1,
|
|
||||||
gid: 1,
|
|
||||||
ino: 1,
|
|
||||||
isDirectory: false,
|
|
||||||
isFile: true,
|
|
||||||
isSymlink: false,
|
|
||||||
mode: 1,
|
|
||||||
mtime: now,
|
|
||||||
nlink: 1,
|
|
||||||
readonly: false,
|
|
||||||
rdev: 1,
|
|
||||||
size: 32,
|
size: 32,
|
||||||
uid: 1,
|
|
||||||
fileAttributes: null,
|
|
||||||
},
|
},
|
||||||
} satisfies ProjectWithEntryPointMetadata
|
kcl_file_count: 1,
|
||||||
|
directory_count: 0,
|
||||||
|
} satisfies Project
|
||||||
|
|
||||||
describe('ProjectSidebarMenu tests', () => {
|
describe('ProjectSidebarMenu tests', () => {
|
||||||
test('Renders the project name', () => {
|
test('Renders the project name', () => {
|
||||||
|
@ -133,13 +133,13 @@ function ProjectMenuPopover({
|
|||||||
<p className="m-0 text-mono" data-testid="projectName">
|
<p className="m-0 text-mono" data-testid="projectName">
|
||||||
{project?.name ? project.name : APP_NAME}
|
{project?.name ? project.name : APP_NAME}
|
||||||
</p>
|
</p>
|
||||||
{project?.entrypointMetadata && (
|
{project?.metadata && project.metadata.created && (
|
||||||
<p
|
<p
|
||||||
className="m-0 text-xs text-chalkboard-80 dark:text-chalkboard-40"
|
className="m-0 text-xs text-chalkboard-80 dark:text-chalkboard-40"
|
||||||
data-testid="createdAt"
|
data-testid="createdAt"
|
||||||
>
|
>
|
||||||
Created{' '}
|
Created{' '}
|
||||||
{project.entrypointMetadata.birthtime?.toLocaleDateString()}
|
{new Date(project.metadata.created).toLocaleDateString()}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,7 +7,12 @@ import React, { createContext, useEffect } from 'react'
|
|||||||
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
||||||
import { settingsMachine } from 'machines/settingsMachine'
|
import { settingsMachine } from 'machines/settingsMachine'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import { getThemeColorForEngine, setThemeClass, Themes } from 'lib/theme'
|
import {
|
||||||
|
getThemeColorForEngine,
|
||||||
|
getOppositeTheme,
|
||||||
|
setThemeClass,
|
||||||
|
Themes,
|
||||||
|
} from 'lib/theme'
|
||||||
import decamelize from 'decamelize'
|
import decamelize from 'decamelize'
|
||||||
import {
|
import {
|
||||||
AnyStateMachine,
|
AnyStateMachine,
|
||||||
@ -99,6 +104,9 @@ export const SettingsAuthProviderBase = ({
|
|||||||
{
|
{
|
||||||
context: loadedSettings,
|
context: loadedSettings,
|
||||||
actions: {
|
actions: {
|
||||||
|
//TODO: batch all these and if that's difficult to do from tsx,
|
||||||
|
// make it easy to do
|
||||||
|
|
||||||
setClientSideSceneUnits: (context, event) => {
|
setClientSideSceneUnits: (context, event) => {
|
||||||
const newBaseUnit =
|
const newBaseUnit =
|
||||||
event.type === 'set.modeling.defaultUnit'
|
event.type === 'set.modeling.defaultUnit'
|
||||||
@ -115,6 +123,16 @@ export const SettingsAuthProviderBase = ({
|
|||||||
color: getThemeColorForEngine(context.app.theme.current),
|
color: getThemeColorForEngine(context.app.theme.current),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
||||||
|
engineCommandManager.sendSceneCommand({
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd: {
|
||||||
|
type: 'set_default_system_properties',
|
||||||
|
color: getThemeColorForEngine(opposingTheme),
|
||||||
|
},
|
||||||
|
})
|
||||||
},
|
},
|
||||||
setEngineEdges: (context) => {
|
setEngineEdges: (context) => {
|
||||||
engineCommandManager.sendSceneCommand({
|
engineCommandManager.sendSceneCommand({
|
||||||
@ -150,7 +168,7 @@ export const SettingsAuthProviderBase = ({
|
|||||||
},
|
},
|
||||||
'Execute AST': () => kclManager.executeCode(true),
|
'Execute AST': () => kclManager.executeCode(true),
|
||||||
persistSettings: (context) =>
|
persistSettings: (context) =>
|
||||||
saveSettings(context, loadedProject?.project?.path),
|
saveSettings(context, loadedProject?.project?.name),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
238
src/editor/manager.ts
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
import { hasNextSnippetField } from '@codemirror/autocomplete'
|
||||||
|
import { EditorView, ViewUpdate } from '@codemirror/view'
|
||||||
|
import { EditorSelection, SelectionRange } from '@codemirror/state'
|
||||||
|
import { engineCommandManager, sceneInfra } from 'lib/singletons'
|
||||||
|
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
||||||
|
import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
|
||||||
|
import { undo, redo } from '@codemirror/commands'
|
||||||
|
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
|
||||||
|
import { addLineHighlight } from './highlightextension'
|
||||||
|
import { setDiagnostics, Diagnostic } from '@codemirror/lint'
|
||||||
|
|
||||||
|
export default class EditorManager {
|
||||||
|
private _editorView: EditorView | null = null
|
||||||
|
|
||||||
|
private _isShiftDown: boolean = false
|
||||||
|
private _selectionRanges: Selections = {
|
||||||
|
otherSelections: [],
|
||||||
|
codeBasedSelections: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
private _lastSelectionEvent: number | null = null
|
||||||
|
private _lastSelection: string = ''
|
||||||
|
private _lastEvent: { event: string; time: number } | null = null
|
||||||
|
|
||||||
|
private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {}
|
||||||
|
private _modelingEvent: ModelingMachineEvent | null = null
|
||||||
|
|
||||||
|
private _commandBarSend: (eventInfo: CommandBarMachineEvent) => void =
|
||||||
|
() => {}
|
||||||
|
|
||||||
|
private _convertToVariableEnabled: boolean = false
|
||||||
|
private _convertToVariableCallback: () => void = () => {}
|
||||||
|
|
||||||
|
private _highlightRange: [number, number] = [0, 0]
|
||||||
|
|
||||||
|
setEditorView(editorView: EditorView) {
|
||||||
|
this._editorView = editorView
|
||||||
|
}
|
||||||
|
|
||||||
|
get editorView(): EditorView | null {
|
||||||
|
return this._editorView
|
||||||
|
}
|
||||||
|
|
||||||
|
get isShiftDown(): boolean {
|
||||||
|
return this._isShiftDown
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsShiftDown(isShiftDown: boolean) {
|
||||||
|
this._isShiftDown = isShiftDown
|
||||||
|
}
|
||||||
|
|
||||||
|
set selectionRanges(selectionRanges: Selections) {
|
||||||
|
this._selectionRanges = selectionRanges
|
||||||
|
}
|
||||||
|
|
||||||
|
set lastSelectionEvent(time: number) {
|
||||||
|
this._lastSelectionEvent = time
|
||||||
|
}
|
||||||
|
|
||||||
|
set modelingSend(send: (eventInfo: ModelingMachineEvent) => void) {
|
||||||
|
this._modelingSend = send
|
||||||
|
}
|
||||||
|
|
||||||
|
set modelingEvent(event: ModelingMachineEvent) {
|
||||||
|
this._modelingEvent = event
|
||||||
|
}
|
||||||
|
|
||||||
|
setCommandBarSend(send: (eventInfo: CommandBarMachineEvent) => void) {
|
||||||
|
this._commandBarSend = send
|
||||||
|
}
|
||||||
|
|
||||||
|
commandBarSend(eventInfo: CommandBarMachineEvent): void {
|
||||||
|
return this._commandBarSend(eventInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
get highlightRange(): [number, number] {
|
||||||
|
return this._highlightRange
|
||||||
|
}
|
||||||
|
|
||||||
|
setHighlightRange(selection: Selection['range']): void {
|
||||||
|
this._highlightRange = selection
|
||||||
|
const editorView = this.editorView
|
||||||
|
const safeEnd = Math.min(
|
||||||
|
selection[1],
|
||||||
|
editorView?.state.doc.length || selection[1]
|
||||||
|
)
|
||||||
|
if (editorView) {
|
||||||
|
editorView.dispatch({
|
||||||
|
effects: addLineHighlight.of([selection[0], safeEnd]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setDiagnostics(diagnostics: Diagnostic[]): void {
|
||||||
|
if (!this.editorView) return
|
||||||
|
this.editorView.dispatch(setDiagnostics(this.editorView.state, diagnostics))
|
||||||
|
}
|
||||||
|
|
||||||
|
undo() {
|
||||||
|
if (this._editorView) {
|
||||||
|
undo(this._editorView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redo() {
|
||||||
|
if (this._editorView) {
|
||||||
|
redo(this._editorView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set convertToVariableEnabled(enabled: boolean) {
|
||||||
|
this._convertToVariableEnabled = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
set convertToVariableCallback(callback: () => void) {
|
||||||
|
this._convertToVariableCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
convertToVariable() {
|
||||||
|
if (this._convertToVariableEnabled) {
|
||||||
|
this._convertToVariableCallback()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
selectRange(selections: Selections) {
|
||||||
|
if (selections.codeBasedSelections.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.editorView) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let codeBasedSelections = []
|
||||||
|
for (const selection of selections.codeBasedSelections) {
|
||||||
|
codeBasedSelections.push(
|
||||||
|
EditorSelection.range(selection.range[0], selection.range[1])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
codeBasedSelections.push(
|
||||||
|
EditorSelection.cursor(
|
||||||
|
selections.codeBasedSelections[
|
||||||
|
selections.codeBasedSelections.length - 1
|
||||||
|
].range[1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
this.editorView.dispatch({
|
||||||
|
selection: EditorSelection.create(codeBasedSelections, 1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnViewUpdate(viewUpdate: ViewUpdate): void {
|
||||||
|
// If we are just fucking around in a snippet, return early and don't
|
||||||
|
// trigger stuff below that might cause the component to re-render.
|
||||||
|
// Otherwise we will not be able to tab thru the snippet portions.
|
||||||
|
// We explicitly dont check HasPrevSnippetField because we always add
|
||||||
|
// a ${} to the end of the function so that's fine.
|
||||||
|
if (hasNextSnippetField(viewUpdate.view.state)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.editorView === null) {
|
||||||
|
this.setEditorView(viewUpdate.view)
|
||||||
|
}
|
||||||
|
const selString = stringifyRanges(
|
||||||
|
viewUpdate?.state?.selection?.ranges || []
|
||||||
|
)
|
||||||
|
|
||||||
|
if (selString === this._lastSelection) {
|
||||||
|
// onUpdate is noisy and is fired a lot by extensions
|
||||||
|
// since we're only interested in selections changes we can ignore most of these.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this._lastSelection = selString
|
||||||
|
|
||||||
|
if (
|
||||||
|
this._lastSelectionEvent &&
|
||||||
|
Date.now() - this._lastSelectionEvent < 150
|
||||||
|
) {
|
||||||
|
return // update triggered by scene selection
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sceneInfra.selected) {
|
||||||
|
return // mid drag
|
||||||
|
}
|
||||||
|
|
||||||
|
const ignoreEvents: ModelingMachineEvent['type'][] = [
|
||||||
|
'Equip Line tool',
|
||||||
|
'Equip tangential arc to',
|
||||||
|
]
|
||||||
|
|
||||||
|
if (!this._modelingEvent) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ignoreEvents.includes(this._modelingEvent.type)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventInfo = processCodeMirrorRanges({
|
||||||
|
codeMirrorRanges: viewUpdate.state.selection.ranges,
|
||||||
|
selectionRanges: this._selectionRanges,
|
||||||
|
isShiftDown: this._isShiftDown,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!eventInfo) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const deterministicEventInfo = {
|
||||||
|
...eventInfo,
|
||||||
|
engineEvents: eventInfo.engineEvents.map((e) => ({
|
||||||
|
...e,
|
||||||
|
cmd_id: 'static',
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
const stringEvent = JSON.stringify(deterministicEventInfo)
|
||||||
|
if (
|
||||||
|
this._lastEvent &&
|
||||||
|
stringEvent === this._lastEvent.event &&
|
||||||
|
Date.now() - this._lastEvent.time < 500
|
||||||
|
) {
|
||||||
|
return // don't repeat events
|
||||||
|
}
|
||||||
|
|
||||||
|
this._lastEvent = { event: stringEvent, time: Date.now() }
|
||||||
|
this._modelingSend(eventInfo.modelingEvent)
|
||||||
|
eventInfo.engineEvents.forEach((event) =>
|
||||||
|
engineCommandManager.sendSceneCommand(event)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyRanges(ranges: readonly SelectionRange[]): string {
|
||||||
|
return ranges.map(({ to, from }) => `${to}->${from}`).join('&')
|
||||||
|
}
|
@ -21,7 +21,7 @@ import { LanguageServerClient } from 'editor/plugins/lsp'
|
|||||||
import { Marked } from '@ts-stack/markdown'
|
import { Marked } from '@ts-stack/markdown'
|
||||||
import { posToOffset } from 'editor/plugins/lsp/util'
|
import { posToOffset } from 'editor/plugins/lsp/util'
|
||||||
import { Program, ProgramMemory } from 'lang/wasm'
|
import { Program, ProgramMemory } from 'lang/wasm'
|
||||||
import { codeManager, kclManager } from 'lib/singletons'
|
import { codeManager, editorManager, kclManager } from 'lib/singletons'
|
||||||
import type { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
|
import type { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
|
||||||
import { UpdateUnitsResponse } from 'wasm-lib/kcl/bindings/UpdateUnitsResponse'
|
import { UpdateUnitsResponse } from 'wasm-lib/kcl/bindings/UpdateUnitsResponse'
|
||||||
import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecuteResponse'
|
import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecuteResponse'
|
||||||
@ -39,6 +39,8 @@ const CompletionItemKindMap = Object.fromEntries(
|
|||||||
) as Record<CompletionItemKind, string>
|
) as Record<CompletionItemKind, string>
|
||||||
|
|
||||||
const changesDelay = 600
|
const changesDelay = 600
|
||||||
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
const updateDelay = 100
|
||||||
|
|
||||||
export class LanguageServerPlugin implements PluginValue {
|
export class LanguageServerPlugin implements PluginValue {
|
||||||
public client: LanguageServerClient
|
public client: LanguageServerClient
|
||||||
@ -47,6 +49,7 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
public workspaceFolders: LSP.WorkspaceFolder[]
|
public workspaceFolders: LSP.WorkspaceFolder[]
|
||||||
private documentVersion: number
|
private documentVersion: number
|
||||||
private foldingRanges: LSP.FoldingRange[] | null = null
|
private foldingRanges: LSP.FoldingRange[] | null = null
|
||||||
|
private viewUpdate: ViewUpdate | null = null
|
||||||
private _defferer = deferExecution((code: string) => {
|
private _defferer = deferExecution((code: string) => {
|
||||||
try {
|
try {
|
||||||
// Update the state (not the editor) with the new code.
|
// Update the state (not the editor) with the new code.
|
||||||
@ -57,6 +60,10 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
},
|
},
|
||||||
contentChanges: [{ text: code }],
|
contentChanges: [{ text: code }],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (this.viewUpdate) {
|
||||||
|
editorManager.handleOnViewUpdate(this.viewUpdate)
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
@ -80,14 +87,27 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
update({ docChanged }: ViewUpdate) {
|
update(viewUpdate: ViewUpdate) {
|
||||||
if (!docChanged) return
|
this.viewUpdate = viewUpdate
|
||||||
|
if (!viewUpdate.docChanged) {
|
||||||
|
// debounce the view update.
|
||||||
|
// otherwise it is laggy for typing.
|
||||||
|
if (debounceTimer) {
|
||||||
|
clearTimeout(debounceTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
debounceTimer = setTimeout(() => {
|
||||||
|
editorManager.handleOnViewUpdate(viewUpdate)
|
||||||
|
}, updateDelay)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const newCode = this.view.state.doc.toString()
|
const newCode = this.view.state.doc.toString()
|
||||||
|
|
||||||
codeManager.code = newCode
|
codeManager.code = newCode
|
||||||
codeManager.writeToFile()
|
codeManager.writeToFile()
|
||||||
kclManager.executeCode()
|
kclManager.executeCode()
|
||||||
|
|
||||||
this.sendChange({
|
this.sendChange({
|
||||||
documentText: newCode,
|
documentText: newCode,
|
||||||
})
|
})
|
||||||
@ -357,15 +377,9 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
try {
|
try {
|
||||||
switch (notification.method) {
|
switch (notification.method) {
|
||||||
case 'textDocument/publishDiagnostics':
|
case 'textDocument/publishDiagnostics':
|
||||||
const params = notification.params as PublishDiagnosticsParams
|
//const params = notification.params as PublishDiagnosticsParams
|
||||||
this.processDiagnostics(params)
|
// this is sometimes slower than our actual typing.
|
||||||
// Update the kcl errors pane.
|
//this.processDiagnostics(params)
|
||||||
/*if (!kclManager.isExecuting) {
|
|
||||||
kclManager.kclErrors = lspDiagnosticsToKclErrors(
|
|
||||||
this.view.state.doc,
|
|
||||||
params.diagnostics
|
|
||||||
)
|
|
||||||
}*/
|
|
||||||
break
|
break
|
||||||
case 'window/logMessage':
|
case 'window/logMessage':
|
||||||
console.log(
|
console.log(
|
||||||
@ -385,17 +399,6 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
// The server has updated the AST, we should update elsewhere.
|
// The server has updated the AST, we should update elsewhere.
|
||||||
let updatedAst = notification.params as Program
|
let updatedAst = notification.params as Program
|
||||||
console.log('[lsp]: Updated AST', updatedAst)
|
console.log('[lsp]: Updated AST', updatedAst)
|
||||||
// Update the ast when we are not already executing.
|
|
||||||
/* if (!kclManager.isExecuting) {
|
|
||||||
kclManager.ast = updatedAst
|
|
||||||
// Execute the ast.
|
|
||||||
console.log('[lsp]: executing ast')
|
|
||||||
await kclManager.executeAst(updatedAst)
|
|
||||||
console.log('[lsp]: executed ast', kclManager.kclErrors)
|
|
||||||
let diagnostics = kclErrorsToDiagnostics(kclManager.kclErrors)
|
|
||||||
this.view.dispatch(setDiagnostics(this.view.state, diagnostics))
|
|
||||||
console.log('[lsp]: updated diagnostics')
|
|
||||||
}*/
|
|
||||||
|
|
||||||
// Update the folding ranges, since the AST has changed.
|
// Update the folding ranges, since the AST has changed.
|
||||||
// This is a hack since codemirror does not support async foldService.
|
// This is a hack since codemirror does not support async foldService.
|
||||||
|