Compare commits

..

9 Commits

Author SHA1 Message Date
ae068b7ed6 closer
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-19 17:02:37 -07:00
5bdc899831 updates
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-19 16:25:08 -07:00
e0e6acf231 updates
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-19 16:08:11 -07:00
668bc863df updates
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-19 16:06:56 -07:00
59ce602d6e updates
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-19 16:06:44 -07:00
8ce819b28f updates
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-19 13:58:08 -07:00
ed5e276377 working
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-19 13:47:38 -07:00
8549bd9486 updates
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-19 13:41:03 -07:00
56f9078812 init
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-19 13:33:31 -07:00
347 changed files with 5898 additions and 11329 deletions

View File

@ -1,35 +0,0 @@
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

View File

@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
dir: ['src/wasm-lib', 'src-tauri'] dir: ['src/wasm-lib']
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install latest rust - name: Install latest rust
@ -31,22 +31,9 @@ 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 \ sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
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

View File

@ -1,57 +0,0 @@
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

View File

@ -50,7 +50,7 @@ jobs:
- run: yarn tsc - run: yarn tsc
check-typos: check-typos:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@ -98,7 +98,7 @@ jobs:
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'yarn' cache: 'yarn'
- name: Set nightly version - name: Set nightly version
if: github.event_name == 'schedule' if: github.event_name == 'schedule'
run: | run: |
@ -143,21 +143,21 @@ jobs:
ls -l artifact ls -l artifact
cp artifact/package.json package.json cp artifact/package.json package.json
cp artifact/src-tauri/tauri.conf.json src-tauri/tauri.conf.json cp artifact/src-tauri/tauri.conf.json src-tauri/tauri.conf.json
cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json
- 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
@ -383,7 +383,7 @@ jobs:
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: 'artifact/*/Zoo*' files: 'artifact/*/Zoo*'
announce_release: announce_release:
needs: [publish-apps-release] needs: [publish-apps-release]
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -391,17 +391,17 @@ jobs:
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: '3.x' python-version: '3.x'
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install requests pip install requests
- name: Announce Release - name: Announce Release
env: env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}

View File

@ -122,8 +122,3 @@ 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

2
.nvmrc
View File

@ -1 +1 @@
v21.7.1 v20.5.0

View File

@ -1,3 +1,3 @@
module.exports = { module.exports = {
presets: ['@babel/preset-env'], presets: ["@babel/preset-env"],
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -104,7 +104,6 @@ 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')
@ -329,7 +328,9 @@ 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()
}) })
test('if your kcl gets an error from the engine it is inlined', async ({ /* 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 ({
page, page,
}) => { }) => {
const u = getUtils(page) const u = getUtils(page)
@ -348,7 +349,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),
@ -363,7 +364,7 @@ angle: 90
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel() await u.closeDebugPanel()
@ -377,7 +378,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)
@ -565,9 +566,7 @@ 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')
@ -596,12 +595,13 @@ 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 page.addInitScript( await context.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(undefined) expect(storedSettings.settings.app?.theme).toBe('dark')
// 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, 253).then(() => page.waitForTimeout(100)) page.mouse.click(700, 250).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,7 +761,6 @@ 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'))
@ -769,14 +768,12 @@ 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')
@ -789,14 +786,10 @@ 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 (isSecondTime = false) => { const selectionSequence = async () => {
await expect(page.getByTestId('hover-highlight')).not.toBeVisible() await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
await page.waitForTimeout(100) await page.mouse.move(startXPx + PUR * 15, 500 - PUR * 10)
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
@ -806,10 +799,7 @@ 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( await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 20) // mouse onto another line
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
@ -819,7 +809,6 @@ 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()
@ -828,12 +817,10 @@ 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')
@ -846,7 +833,6 @@ 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()
@ -889,16 +875,11 @@ 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 u.doAndWaitForCmd( await page.getByRole('button', { name: 'Edit Sketch' }).click()
() => 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(true) await selectionSequence()
}) })
test.describe('Command bar tests', () => { test.describe('Command bar tests', () => {
@ -1034,7 +1015,6 @@ 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
@ -1085,7 +1065,6 @@ 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}, %)
@ -1101,33 +1080,24 @@ 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([100, 100, 100]) await u.updateCamPosition([0, 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(400) await page.waitForTimeout(100)
await page.mouse.click(650, 450) await page.mouse.click(673, 384)
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 = const startAt2 = '[0.93,-1.25]'
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('${plane}') const part002 = startSketchOn('XY')
|> startProfileAt(${startAt2}, %)`.replace(/\s/g, '') |> startProfileAt(${startAt2}, %)`.replace(/\s/g, '')
) )
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -1136,12 +1106,12 @@ const part002 = startSketchOn('${plane}')
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 = process.platform === 'darwin' ? 9.84 : 0.94 const num2 = 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('${plane}') const part002 = startSketchOn('XY')
|> startProfileAt(${startAt2}, %) |> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %)`.replace(/\s/g, '') |> line([${num2}, 0], %)`.replace(/\s/g, '')
) )
@ -1151,29 +1121,21 @@ const part002 = startSketchOn('${plane}')
(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('${plane}') const part002 = startSketchOn('XY')
|> startProfileAt(${startAt2}, %) |> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %) |> line([${num2}, 0], %)
|> line([0, ${roundOff( |> line([0, ${roundOff(num2 - 0.01)}], %)`.replace(/\s/g, '')
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('${plane}') const part002 = startSketchOn('XY')
|> startProfileAt(${startAt2}, %) |> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %) |> line([${num2}, 0], %)
|> line([0, ${roundOff( |> line([0, ${roundOff(num2 - 0.01)}], %)
num2 + (process.platform === 'darwin' ? 0.01 : -0.01) |> line([-1.87, 0], %)`.replace(/\s/g, '')
)}], %)
|> line([-${process.platform === 'darwin' ? 19.59 : 1.87}, 0], %)`.replace(
/\s/g,
''
)
) )
}) })
@ -1377,12 +1339,10 @@ 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()
@ -1407,16 +1367,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 = [665, 458] const startPX = [652, 418]
const lineEndPX = [842, 458] const lineEndPX = [794, 416]
const arcEndPX = [971, 342] const arcEndPX = [893, 318]
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(400) await page.waitForTimeout(100)
let prevContent = await page.locator('.cm-content').innerText() let prevContent = await page.locator('.cm-content').innerText()
const step5 = { steps: 5 } const step5 = { steps: 5 }
@ -1426,7 +1386,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()
@ -1454,9 +1414,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([6.44, -12.07], %) |> startProfileAt([7.01, -11.79], %)
|> line([14.04, 2.03], %) |> line([14.69, 2.73], %)
|> tangentialArcTo([27.19, -4.2], %)`) |> tangentialArcTo([27.6, -3.25], %)`)
}) })
const doSnapAtDifferentScales = async ( const doSnapAtDifferentScales = async (
@ -1575,46 +1535,38 @@ 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 u.openAndClearDebugPanel() await page.mouse.click(793, 133)
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([-12.83, 6.7], %) |> startProfileAt([1.03, 1.03], %)
|> line([2.87, -0.23], %) |> line([4.18, -0.35], %)
|> line([-3.05, -1.47], %) |> line([-4.44, -2.13], %)
|> close(%)`) |> close(%)`)
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
@ -1624,14 +1576,9 @@ 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([-12.83, 6.7], %)').click() await page.getByText('startProfileAt([1.03, 1.03], %)').click()
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible() await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
await u.doAndWaitForCmd( await page.getByRole('button', { name: 'Edit Sketch' }).click()
() => 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])
@ -1651,11 +1598,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([-12.83, 6.7], %) |> startProfileAt([1.03, 1.03], %)
|> line([${process?.env?.CI ? 2.28 : 2.28}, -${ |> line([${process?.env?.CI ? 2.74 : 2.93}, -${
process?.env?.CI ? 0.07 : 0.07 process?.env?.CI ? 0.24 : 0.2
}], %) }], %)
|> line([-3.05, -1.47], %) |> line([-4.44, -2.13], %)
|> close(%)`) |> close(%)`)
// exit sketch // exit sketch
@ -1663,7 +1610,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([-12.83, 6.7], %)').click() await page.getByText('startProfileAt([1.03, 1.03], %)').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()
@ -1677,11 +1624,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([-12.83, 6.7], %) |> startProfileAt([1.03, 1.03], %)
|> line([${process?.env?.CI ? 2.28 : 2.28}, -${ |> line([${process?.env?.CI ? 2.74 : 2.93}, -${
process?.env?.CI ? 0.07 : 0.07 process?.env?.CI ? 0.24 : 0.2
}], %) }], %)
|> line([-3.05, -1.47], %) |> line([-4.44, -2.13], %)
|> close(%) |> close(%)
|> extrude(5 + 7, %)`) |> extrude(5 + 7, %)`)
}) })
@ -1714,11 +1661,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(350) // wait for animation await page.waitForTimeout(300) // 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, 102) await page.mouse.click(615, 133)
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()
@ -1726,42 +1673,3 @@ 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, %)`
)
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -1,13 +1,12 @@
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 = '/settings.toml' export const TEST_SETTINGS_KEY = '/user.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',
@ -24,7 +23,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 = {

View File

@ -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 project') expect(await newFileButton.getText()).toEqual('New file')
}) })
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 () => {

View File

@ -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" '.version=$v' src-tauri/tauri.conf.json --indent 2)" > src-tauri/tauri.conf.json echo "$(jq --arg v "$new_version_number" '.package.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"

View File

@ -1,6 +1,6 @@
{ {
"name": "untitled-app", "name": "untitled-app",
"version": "0.19.0", "version": "0.17.3",
"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.19", "@headlessui/react": "^1.7.18",
"@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 *.ts *.json *.js ./e2e", "fmt": "prettier --write ./src && prettier --write ./e2e",
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e", "fmt-check": "prettier --check ./src && prettier --check ./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,7 +132,6 @@
"@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",

View File

@ -49,6 +49,8 @@ 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',
@ -76,4 +78,4 @@ export default defineConfig({
// url: 'http://127.0.0.1:3000', // url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
}, },
}) })

2310
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -8,26 +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.13", features = [] } tauri-build = { version = "2.0.0-beta.12", features = [] }
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
kcl-lib = { version = "0.1.51", path = "../src/wasm-lib/kcl" } kittycad = "0.2.67"
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.6" } tauri-plugin-dialog = { version = "2.0.0-beta.5" }
tauri-plugin-fs = { version = "2.0.0-beta.6" } tauri-plugin-fs = { version = "2.0.0-beta.5" }
tauri-plugin-http = { version = "2.0.0-beta.6" } tauri-plugin-http = { version = "2.0.0-beta.5" }
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", "fs"] } tokio = { version = "1.37.0", features = ["time"] }
toml = "0.8.2" toml = "0.8.2"
[features] [features]

View File

@ -1,6 +0,0 @@
max_width = 120
edition = "2018"
format_code_in_doc_comments = true
format_strings = false
imports_granularity = "Crate"
group_imports = "StdExternalCrate"

View File

@ -1,189 +1,91 @@
// 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::{ use std::env;
env, use std::fs;
path::{Path, PathBuf}, use std::io::Read;
process::Command, use std::path::Path;
}; use std::path::PathBuf;
use anyhow::Result; use anyhow::Result;
use kcl_lib::settings::types::{
file::{FileEntry, Project},
project::ProjectConfiguration,
Configuration,
};
use oauth2::TokenResponse; use oauth2::TokenResponse;
use tauri::{ipc::InvokeError, Manager}; use serde::Serialize;
use std::process::Command;
use tauri::ipc::InvokeError;
use tauri_plugin_shell::ShellExt; use tauri_plugin_shell::ShellExt;
const DEFAULT_HOST: &str = "https://api.kittycad.io";
const DEFAULT_HOST: &str = "https://api.zoo.dev"; /// This command returns the a json string parse from a toml file at the path.
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 get_initial_default_dir(app: tauri::AppHandle) -> Result<PathBuf, InvokeError> { fn read_toml(path: &str) -> Result<String, InvokeError> {
let dir = match app.path().document_dir() { let mut file = std::fs::File::open(path).map_err(|e| InvokeError::from_anyhow(e.into()))?;
Ok(dir) => dir, let mut contents = String::new();
Err(_) => { file.read_to_string(&mut contents)
// for headless Linux (eg. Github Actions) .map_err(|e| InvokeError::from_anyhow(e.into()))?;
let home_dir = app.path().home_dir()?; let value =
home_dir.join("Documents") toml::from_str::<toml::Value>(&contents).map_err(|e| InvokeError::from_anyhow(e.into()))?;
} let value = serde_json::to_string(&value).map_err(|e| InvokeError::from_anyhow(e.into()))?;
}; Ok(value)
Ok(dir.join(PROJECT_FOLDER))
} }
fn get_app_settings_file_path(app: &tauri::AppHandle) -> Result<PathBuf, InvokeError> { /// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
let app_config_dir = app.path().app_config_dir()?; /// Removed from tauri v2
Ok(app_config_dir.join(SETTINGS_FILE_NAME)) #[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>>,
} }
#[tauri::command] /// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
async fn read_app_settings_file(app: tauri::AppHandle) -> Result<Configuration, InvokeError> { /// Removed from tauri v2
let mut settings_path = get_app_settings_file_path(&app)?; fn is_dir<P: AsRef<Path>>(path: P) -> Result<bool> {
let mut needs_migration = false; std::fs::metadata(path)
.map(|md| md.is_dir())
.map_err(Into::into)
}
// Check if this file exists. /// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
if !settings_path.exists() { /// Removed from tauri v2
// Try the backwards compatible path. #[tauri::command]
// TODO: Remove this after a few releases. fn read_dir_recursive(path: &str) -> Result<Vec<DiskEntry>, InvokeError> {
let app_config_dir = app.path().app_config_dir()?; let mut files_and_dirs: Vec<DiskEntry> = vec![];
settings_path = format!( // let path = path.as_ref();
"{}user.toml", for entry in fs::read_dir(path).map_err(|e| InvokeError::from_anyhow(e.into()))? {
app_config_dir.display().to_string().trim_end_matches('/') let path = entry
) .map_err(|e| InvokeError::from_anyhow(e.into()))?
.into(); .path();
needs_migration = true;
// Check if this path exists. if let Ok(flag) = is_dir(&path) {
if !settings_path.exists() { files_and_dirs.push(DiskEntry {
let mut default = Configuration::default(); path: path.clone(),
default.settings.project.directory = get_initial_default_dir(app.clone())?; children: if flag {
// Return the default configuration. Some(read_dir_recursive(path.to_str().expect("No path"))?)
return Ok(default); } else {
None
},
name: path
.file_name()
.map(|name| name.to_string_lossy())
.map(|name| name.to_string()),
});
} }
} }
Ok(files_and_dirs)
}
let contents = tokio::fs::read_to_string(&settings_path) /// This command returns a string that is the contents of a file at the path.
.await #[tauri::command]
fn read_txt_file(path: &str) -> Result<String, InvokeError> {
let mut file = std::fs::File::open(path).map_err(|e| InvokeError::from_anyhow(e.into()))?;
let mut contents = String::new();
file.read_to_string(&mut contents)
.map_err(|e| InvokeError::from_anyhow(e.into()))?; .map_err(|e| InvokeError::from_anyhow(e.into()))?;
let mut parsed = Configuration::backwards_compatible_toml_parse(&contents).map_err(InvokeError::from_anyhow)?; Ok(contents)
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)
}
#[tauri::command]
async fn write_app_settings_file(app: tauri::AppHandle, configuration: Configuration) -> Result<(), InvokeError> {
let settings_path = get_app_settings_file_path(&app)?;
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(())
}
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_name: &str) -> Result<Project, InvokeError> {
configuration
.get_project_info(project_name)
.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.
@ -201,7 +103,8 @@ 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")).map_err(|e| InvokeError::from_anyhow(e.into()))?, oauth2::AuthUrl::new(format!("{host}/authorize"))
.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()))?,
@ -229,10 +132,12 @@ 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!("E2E_TAURI_ENABLED is set, won't open {} externally", auth_uri.secret()); println!(
tokio::fs::write("/tmp/kittycad_user_code", details.user_code().secret()) "E2E_TAURI_ENABLED is set, won't open {} externally",
.await auth_uri.secret()
.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)
@ -255,7 +160,10 @@ 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(token: &str, hostname: &str) -> Result<kittycad::types::User, InvokeError> { async fn get_user(
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() {
@ -275,7 +183,7 @@ async fn get_user(token: &str, hostname: &str) -> Result<kittycad::types::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); let mut client = kittycad::Client::new(token.unwrap());
if baseurl != DEFAULT_HOST { if baseurl != DEFAULT_HOST {
client.set_base_url(&baseurl); client.set_base_url(&baseurl);
@ -294,53 +202,43 @@ async fn get_user(token: &str, hostname: &str) -> Result<kittycad::types::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: &str) -> Result<(), InvokeError> { fn show_in_folder(path: String) {
#[cfg(not(unix))] #[cfg(target_os = "windows")]
{ {
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()
.map_err(|e| InvokeError::from_anyhow(e.into()))?; .unwrap();
} }
#[cfg(unix)] #[cfg(target_os = "macos")]
{ {
Command::new("open") Command::new("open").args(["-R", &path]).spawn().unwrap();
.args(["-R", &path])
.spawn()
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
} }
Ok(())
} }
fn main() -> Result<()> { fn main() {
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().plugin(tauri_plugin_updater::Builder::new().build())?; _app.handle()
.plugin(tauri_plugin_updater::Builder::new().build())?;
} }
Ok(()) Ok(())
}) })
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
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_dialog::init()) .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
@ -348,7 +246,6 @@ fn main() -> Result<()> {
.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(())
} }

View File

@ -55,5 +55,5 @@
} }
}, },
"productName": "Zoo Modeling App", "productName": "Zoo Modeling App",
"version": "0.19.0" "version": "0.17.3"
} }

View File

@ -246,31 +246,13 @@ 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
) { ) {
const distanceToTarget = new Vector3( this.camera.zoom = camSettings.ortho_scale
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()
} }
@ -983,10 +965,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: 1000 } return { z_near: 0.1, z_far }
} }
function convertThreeCamValuesToEngineCam({ function convertThreeCamValuesToEngineCam({
@ -1061,62 +1043,3 @@ 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',
},
})
}

View File

@ -3,6 +3,7 @@ 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'
@ -46,6 +47,10 @@ 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.
@ -64,6 +69,7 @@ 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)

View File

@ -57,7 +57,6 @@ 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'
@ -215,9 +214,8 @@ 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 axisPixelWidth = 1.6 const xAxisGeometry = new BoxGeometry(100000, 0.3, 0.01)
const xAxisGeometry = new BoxGeometry(100000, axisPixelWidth, 0.01) const yAxisGeometry = new BoxGeometry(0.3, 100000, 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,
@ -1325,31 +1323,30 @@ export class SceneEntities {
selected.material.color = defaultPlaneColor(type) selected.material.color = defaultPlaneColor(type)
}, },
onClick: async (args) => { onClick: async (args) => {
const checkExtrudeFaceClick = async (): Promise< const checkExtrudeFaceClick = async (): Promise<boolean> => {
['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 ['other', ''] if (!entity_id) return false
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 ['other', entity_id] return false
const faceInfo: Models['FaceIsPlanar_type'] = (
const faceInfo = await getFaceDetails(entity_id) await this.engineCommandManager.sendSceneCommand({
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 ['other', entity_id] return false
const { z_axis, y_axis, origin } = faceInfo const { z_axis, origin, y_axis } = faceInfo
const pathToNode = getNodePathFromSourceRange( const pathToNode = getNodePathFromSourceRange(
kclManager.ast, kclManager.ast,
artifact.range artifact.range
@ -1369,15 +1366,12 @@ export class SceneEntities {
artifact?.additionalData?.type === 'cap' artifact?.additionalData?.type === 'cap'
? artifact.additionalData.info ? artifact.additionalData.info
: 'none', : 'none',
faceId: entity_id,
}, },
}) })
return ['face', entity_id] return true
} }
const faceResult = await checkExtrudeFaceClick() if (await checkExtrudeFaceClick()) return
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
@ -1403,7 +1397,6 @@ export class SceneEntities {
plane: planeString, plane: planeString,
zAxis, zAxis,
yAxis, yAxis,
planeId: faceResult[1],
}, },
}) })
}, },
@ -1430,7 +1423,7 @@ export class SceneEntities {
parent.userData.pathToNode, parent.userData.pathToNode,
'CallExpression' 'CallExpression'
).node ).node
editorManager.setHighlightRange([node.start, node.end]) sceneInfra.highlightCallback([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)
@ -1466,10 +1459,10 @@ export class SceneEntities {
} }
return return
} }
editorManager.setHighlightRange([0, 0]) sceneInfra.highlightCallback([0, 0])
}, },
onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => { onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => {
editorManager.setHighlightRange([0, 0]) sceneInfra.highlightCallback([0, 0])
const parent = getParentGroup(selected, [ const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT, STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT,
@ -1687,7 +1680,7 @@ export async function getSketchOrientationDetails(
sketchPathToNode: PathToNode sketchPathToNode: PathToNode
): Promise<{ ): Promise<{
quat: Quaternion quat: Quaternion
sketchDetails: SketchDetails & { faceId?: string } sketchDetails: SketchDetails
}> { }> {
const sketchGroup = sketchGroupFromPathToNode({ const sketchGroup = sketchGroupFromPathToNode({
pathToNode: sketchPathToNode, pathToNode: sketchPathToNode,
@ -1703,13 +1696,20 @@ 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 = await getFaceDetails(sketchGroup.on.faceId) const faceInfo: Models['FaceIsPlanar_type'] = (
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,7 +1724,6 @@ 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,
}, },
} }
} }
@ -1733,46 +1732,6 @@ 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)

View File

@ -24,6 +24,7 @@ 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'
@ -148,6 +149,10 @@ 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) {

View File

@ -1,9 +1,11 @@
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { editorManager, kclManager } from 'lib/singletons' import { 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
@ -40,7 +42,7 @@ export function AstExplorer() {
<div <div
className="h-full relative" className="h-full relative"
onMouseLeave={(e) => { onMouseLeave={(e) => {
editorManager.setHighlightRange([0, 0]) setHighlightRange([0, 0])
}} }}
> >
<pre className="text-xs"> <pre className="text-xs">
@ -86,6 +88,7 @@ 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)
@ -109,12 +112,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) => {
editorManager.setHighlightRange([obj?.start || 0, obj.end]) setHighlightRange([obj?.start || 0, obj.end])
e.stopPropagation() e.stopPropagation()
}} }}
onMouseMove={(e) => { onMouseMove={(e) => {
e.stopPropagation() e.stopPropagation()
editorManager.setHighlightRange([obj?.start || 0, obj.end]) setHighlightRange([obj?.start || 0, obj.end])
}} }}
onClick={(e) => { onClick={(e) => {
send({ send({

View File

@ -137,33 +137,34 @@ export function useCalc({
setAvailableVarInfo(varInfo) setAvailableVarInfo(varInfo)
}, [kclManager.ast, kclManager.programMemory, selectionRange]) }, [kclManager.ast, kclManager.programMemory, selectionRange])
useEffect(() => { useEffect(async () => {
try { try {
const code = `const __result__ = ${value}` const code = `const __result__ = ${value}`
const ast = parse(code) parse(code).then((ast) => {
const _programMem: any = { root: {}, return: null } const _programMem: any = { root: {}, return: null }
availableVarInfo.variables.forEach(({ key, value }) => { availableVarInfo.variables.forEach(({ key, value }) => {
_programMem.root[key] = { type: 'userVal', value, __meta: [] } _programMem.root[key] = { type: 'userVal', value, __meta: [] }
}) })
executeAst({ executeAst({
ast, ast,
engineCommandManager, engineCommandManager,
useFakeExecutor: true, useFakeExecutor: true,
programMemoryOverride: JSON.parse( programMemoryOverride: JSON.parse(
JSON.stringify(kclManager.programMemory) JSON.stringify(kclManager.programMemory)
), ),
}).then(({ programMemory }) => { }).then(({ programMemory }) => {
const resultDeclaration = ast.body.find( const resultDeclaration = ast.body.find(
(a) => (a) =>
a.type === 'VariableDeclaration' && a.type === 'VariableDeclaration' &&
a.declarations?.[0]?.id?.name === '__result__' a.declarations?.[0]?.id?.name === '__result__'
) )
const init = const init =
resultDeclaration?.type === 'VariableDeclaration' && resultDeclaration?.type === 'VariableDeclaration' &&
resultDeclaration?.declarations?.[0]?.init resultDeclaration?.declarations?.[0]?.init
const result = programMemory?.root?.__result__?.value const result = programMemory?.root?.__result__?.value
setCalcResult(typeof result === 'number' ? String(result) : 'NAN') setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init) init && setValueNode(init)
})
}) })
} catch (e) { } catch (e) {
setCalcResult('NAN') setCalcResult('NAN')

View File

@ -1,7 +1,6 @@
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, useEffect } from 'react' import { createContext } from 'react'
import { EventFrom, StateFrom } from 'xstate' import { EventFrom, StateFrom } from 'xstate'
type CommandsContextType = { type CommandsContextType = {
@ -31,10 +30,6 @@ export const CommandBarProvider = ({
}, },
}) })
useEffect(() => {
editorManager.setCommandBarSend(commandBarSend)
})
return ( return (
<CommandsContext.Provider <CommandsContext.Provider
value={{ value={{

View File

@ -3,12 +3,13 @@ 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 { useCallback, useEffect, useRef, useState } from 'react' import { 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'
@ -29,13 +30,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 initSelectionsByType = useCallback(() => { const [selectionsByType, setSelectionsByType] = useState<
const selectionRangeEnd = selection.codeBasedSelections[0]?.range[1] 'none' | ResolvedSelectionType[]
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)
) )
@ -50,14 +51,17 @@ 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) {

View File

@ -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 getProjectInfo(context.project.name)).children ? await readProject(context.project.path)
: [] : []
return { return {
...context.project, ...context.project,

View File

@ -94,7 +94,10 @@ export function HelpMenu(props: React.PropsWithChildren) {
if (isInProject) { if (isInProject) {
navigate('onboarding') navigate('onboarding')
} else { } else {
createAndOpenNewProject(navigate) createAndOpenNewProject(
settings.context.app.projectDirectory.current,
navigate
)
} }
}} }}
> >

View File

@ -17,7 +17,6 @@ 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 {
@ -54,9 +53,10 @@ 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, enableSSAO }, app: { theme },
modeling: { defaultUnit, highlightEdges }, modeling: { defaultUnit, highlightEdges },
}, },
}, },
@ -87,7 +87,6 @@ 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,
@ -99,6 +98,17 @@ 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) || '{}'
@ -125,33 +135,29 @@ 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 (!editorManager.editorView) return {} if (!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
editorManager.lastSelectionEvent = Date.now() setLastCodeMirrorSelectionUpdatedFromScene(Date.now())
setTimeout(() => { setTimeout(() => editorView.dispatch({ selection }))
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 && editorManager.isShiftDown) { if (!setSelections.selection && isShiftDown) {
} else if (!setSelections.selection && !editorManager.isShiftDown) { } else if (!setSelections.selection && !isShiftDown) {
selections = { selections = {
codeBasedSelections: [], codeBasedSelections: [],
otherSelections: [], otherSelections: [],
} }
} else if (setSelections.selection && !editorManager.isShiftDown) { } else if (setSelections.selection && !isShiftDown) {
selections = { selections = {
codeBasedSelections: [setSelections.selection], codeBasedSelections: [setSelections.selection],
otherSelections: [], otherSelections: [],
} }
} else if (setSelections.selection && editorManager.isShiftDown) { } else if (setSelections.selection && isShiftDown) {
selections = { selections = {
codeBasedSelections: [ codeBasedSelections: [
...selectionRanges.codeBasedSelections, ...selectionRanges.codeBasedSelections,
@ -174,7 +180,6 @@ export const ModelingMachineProvider = ({
engineCommandManager.sendSceneCommand(event) engineCommandManager.sendSceneCommand(event)
) )
updateSceneObjectColors() updateSceneObjectColors()
return { return {
selectionRanges: selections, selectionRanges: selections,
} }
@ -187,7 +192,7 @@ export const ModelingMachineProvider = ({
} }
if (setSelections.selectionType === 'otherSelection') { if (setSelections.selectionType === 'otherSelection') {
if (editorManager.isShiftDown) { if (isShiftDown) {
selections = { selections = {
codeBasedSelections: selectionRanges.codeBasedSelections, codeBasedSelections: selectionRanges.codeBasedSelections,
otherSelections: [setSelections.selection], otherSelections: [setSelections.selection],
@ -268,12 +273,10 @@ 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 ( if (isSelectionLastLine(selectionRanges, codeManager.code))
selectionRanges.codeBasedSelections.length === 0 ||
isSelectionLastLine(selectionRanges, codeManager.code)
)
return true return true
if (!isPipe) return false if (!isPipe) return false
@ -321,9 +324,16 @@ export const ModelingMachineProvider = ({
) )
await kclManager.executeAstMock(modifiedAst) await kclManager.executeAstMock(modifiedAst)
await letEngineAnimateAndSyncCamAfter( const forward = new Vector3(...data.zAxis)
engineCommandManager, const up = new Vector3(...data.yAxis)
data.faceId
let target = new Vector3(...data.position).multiplyScalar(
sceneInfra._baseUnitMultiplier
)
const quaternion = quaternionFromUpNForward(up, forward)
await sceneInfra.camControls.tweenCameraToQuaternion(
quaternion,
target
) )
return { return {
@ -338,7 +348,6 @@ 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 {
@ -355,9 +364,9 @@ export const ModelingMachineProvider = ({
sourceRange sourceRange
) )
const info = await getSketchOrientationDetails(sketchPathToNode || []) const info = await getSketchOrientationDetails(sketchPathToNode || [])
await letEngineAnimateAndSyncCamAfter( await sceneInfra.camControls.tweenCameraToQuaternion(
engineCommandManager, info.quat,
info?.sketchDetails?.faceId || '' new Vector3(...info.sketchDetails.origin)
) )
return { return {
sketchPathToNode: sketchPathToNode || [], sketchPathToNode: sketchPathToNode || [],
@ -507,19 +516,6 @@ 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,

View File

@ -1,8 +1,13 @@
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 } from 'react' import { useEffect, useMemo, useRef } 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'
@ -16,6 +21,7 @@ import {
EditorView, EditorView,
dropCursor, dropCursor,
drawSelection, drawSelection,
ViewUpdate,
} from '@codemirror/view' } from '@codemirror/view'
import { import {
indentWithTab, indentWithTab,
@ -23,7 +29,7 @@ import {
historyKeymap, historyKeymap,
history, history,
} from '@codemirror/commands' } from '@codemirror/commands'
import { lintGutter, lintKeymap } from '@codemirror/lint' import { lintGutter, lintKeymap, linter } from '@codemirror/lint'
import { import {
foldGutter, foldGutter,
foldKeymap, foldKeymap,
@ -33,20 +39,25 @@ 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 { kclManager, editorManager, codeManager } from 'lib/singletons' import { engineCommandManager, sceneInfra, kclManager } 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 } from '@codemirror/state' import { Prec, EditorState, Extension, SelectionRange } 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: {
@ -66,6 +77,13 @@ 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()
@ -78,15 +96,90 @@ export const KclEditorPane = () => {
useHotkeys('mod+z', (e) => { useHotkeys('mod+z', (e) => {
e.preventDefault() e.preventDefault()
editorManager.undo() if (editorView) {
undo(editorView)
}
}) })
useHotkeys('mod+shift+z', (e) => { useHotkeys('mod+shift+z', (e) => {
e.preventDefault() e.preventDefault()
editorManager.redo() if (editorView) {
redo(editorView)
}
}) })
const textWrapping = context.textEditor.textWrapping const {
const cursorBlinking = context.textEditor.blinkingCursor context: { selectionRanges },
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 = [
@ -109,7 +202,7 @@ export const KclEditorPane = () => {
{ {
key: 'Meta-k', key: 'Meta-k',
run: () => { run: () => {
editorManager.commandBarSend({ type: 'Open' }) commandBarSend({ type: 'Open' })
return false return false
}, },
}, },
@ -123,7 +216,11 @@ export const KclEditorPane = () => {
{ {
key: editorShortcutMeta.convertToVariable.codeMirror, key: editorShortcutMeta.convertToVariable.codeMirror,
run: () => { run: () => {
return editorManager.convertToVariable() if (convertEnabled) {
convertCallback()
return true
}
return false
}, },
}, },
]), ]),
@ -136,6 +233,9 @@ 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(),
@ -188,7 +288,13 @@ export const KclEditorPane = () => {
} }
return extensions return extensions
}, [kclLSP, copilotLSP, textWrapping.current, cursorBlinking.current]) }, [
kclLSP,
copilotLSP,
textWrapping.current,
cursorBlinking.current,
convertCallback,
])
return ( return (
<div <div
@ -196,15 +302,18 @@ export const KclEditorPane = () => {
className={'absolute inset-0 ' + (cursorBlinking.current ? 'blink' : '')} className={'absolute inset-0 ' + (cursorBlinking.current ? 'blink' : '')}
> >
<ReactCodeMirror <ReactCodeMirror
value={codeManager.code} value={editorCode}
extensions={editorExtensions} extensions={editorExtensions}
onUpdate={onUpdate}
theme={theme} theme={theme}
onCreateEditor={(_editorView) => onCreateEditor={(_editorView) => setEditorView(_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('&')
}

View File

@ -2,9 +2,7 @@ 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(async () => { beforeAll(() => initPromise)
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 () => {
@ -28,7 +26,7 @@ describe('processMemory', () => {
|> lineTo([0.98, 5.16], %) |> lineTo([0.98, 5.16], %)
|> lineTo([2.15, 4.32], %) |> lineTo([2.15, 4.32], %)
// |> rx(90, %)` // |> rx(90, %)`
const ast = parse(code) const ast = await parse(code)
const programMemory = await enginelessExecutor(ast, { const programMemory = await enginelessExecutor(ast, {
root: {}, root: {},
return: null, return: null,

View File

@ -1,4 +1,5 @@
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'
@ -8,11 +9,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,
@ -20,17 +21,17 @@ function ProjectCard({
handleDeleteProject, handleDeleteProject,
...props ...props
}: { }: {
project: Project project: ProjectWithEntryPointMetadata
handleRenameProject: ( handleRenameProject: (
e: FormEvent<HTMLFormElement>, e: FormEvent<HTMLFormElement>,
f: Project f: ProjectWithEntryPointMetadata
) => Promise<void> ) => Promise<void>
handleDeleteProject: (f: Project) => Promise<void> handleDeleteProject: (f: ProjectWithEntryPointMetadata) => 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 [numberOfFiles, setNumberOfFiles] = useState(1) const [numberOfParts, setNumberOfParts] = useState(1)
const [numberOfFolders, setNumberOfFolders] = useState(0) const [numberOfFolders, setNumberOfFolders] = useState(0)
let inputRef = useRef<HTMLInputElement>(null) let inputRef = useRef<HTMLInputElement>(null)
@ -40,8 +41,7 @@ function ProjectCard({
void handleRenameProject(e, project).then(() => setIsEditing(false)) void handleRenameProject(e, project).then(() => setIsEditing(false))
} }
function getDisplayedTime(dateStr: string) { function getDisplayedTime(date: Date) {
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,12 +50,15 @@ function ProjectCard({
} }
useEffect(() => { useEffect(() => {
async function getNumberOfFiles() { async function getNumberOfParts() {
setNumberOfFiles(project.kcl_file_count) const { kclFileCount, kclDirCount } = getPartsCount(
setNumberOfFolders(project.directory_count) await readProject(project.path)
)
setNumberOfParts(kclFileCount)
setNumberOfFolders(kclDirCount)
} }
void getNumberOfFiles() void getNumberOfParts()
}, [project.kcl_file_count, project.directory_count]) }, [project.path])
useEffect(() => { useEffect(() => {
if (inputRef.current) { if (inputRef.current) {
@ -126,7 +129,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">
{numberOfFiles} file{numberOfFiles === 1 ? '' : 's'}{' '} {numberOfParts} part{numberOfParts === 1 ? '' : 's'}{' '}
{numberOfFolders > 0 && {numberOfFolders > 0 &&
`/ ${numberOfFolders} folder${ `/ ${numberOfFolders} folder${
numberOfFolders === 1 ? '' : 's' numberOfFolders === 1 ? '' : 's'
@ -134,8 +137,8 @@ function ProjectCard({
</span> </span>
<span className="text-chalkboard-60 text-xs"> <span className="text-chalkboard-60 text-xs">
Edited{' '} Edited{' '}
{project.metadata && project.metadata?.modified {project.entrypointMetadata.mtime
? getDisplayedTime(project.metadata.modified) ? getDisplayedTime(project.entrypointMetadata.mtime)
: '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">

View File

@ -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,17 +14,29 @@ const projectWellFormed = {
{ {
name: 'main.kcl', name: 'main.kcl',
path: '/some/path/Simple Box/main.kcl', path: '/some/path/Simple Box/main.kcl',
children: [],
}, },
], ],
metadata: { entrypointMetadata: {
created: now.toISOString(), atime: now,
modified: now.toISOString(), blksize: 32,
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,
}, },
kcl_file_count: 1, } satisfies ProjectWithEntryPointMetadata
directory_count: 0,
} satisfies Project
describe('ProjectSidebarMenu tests', () => { describe('ProjectSidebarMenu tests', () => {
test('Renders the project name', () => { test('Renders the project name', () => {

View File

@ -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?.metadata && project.metadata.created && ( {project?.entrypointMetadata && (
<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{' '}
{new Date(project.metadata.created).toLocaleDateString()} {project.entrypointMetadata.birthtime?.toLocaleDateString()}
</p> </p>
)} )}
</div> </div>

View File

@ -7,12 +7,7 @@ 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 { import { getThemeColorForEngine, setThemeClass, Themes } from 'lib/theme'
getThemeColorForEngine,
getOppositeTheme,
setThemeClass,
Themes,
} from 'lib/theme'
import decamelize from 'decamelize' import decamelize from 'decamelize'
import { import {
AnyStateMachine, AnyStateMachine,
@ -104,9 +99,6 @@ 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'
@ -123,16 +115,6 @@ 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({
@ -168,7 +150,7 @@ export const SettingsAuthProviderBase = ({
}, },
'Execute AST': () => kclManager.executeCode(true), 'Execute AST': () => kclManager.executeCode(true),
persistSettings: (context) => persistSettings: (context) =>
saveSettings(context, loadedProject?.project?.name), saveSettings(context, loadedProject?.project?.path),
}, },
} }
) )

View File

@ -1,238 +0,0 @@
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('&')
}

View File

@ -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, editorManager, kclManager } from 'lib/singletons' import { codeManager, 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,8 +39,6 @@ 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
@ -49,7 +47,6 @@ 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.
@ -60,10 +57,6 @@ 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)
} }
@ -87,27 +80,14 @@ export class LanguageServerPlugin implements PluginValue {
}) })
} }
update(viewUpdate: ViewUpdate) { update({ docChanged }: ViewUpdate) {
this.viewUpdate = viewUpdate if (!docChanged) return
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,
}) })
@ -377,9 +357,15 @@ 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 is sometimes slower than our actual typing. this.processDiagnostics(params)
//this.processDiagnostics(params) // Update the kcl errors pane.
/*if (!kclManager.isExecuting) {
kclManager.kclErrors = lspDiagnosticsToKclErrors(
this.view.state.doc,
params.diagnostics
)
}*/
break break
case 'window/logMessage': case 'window/logMessage':
console.log( console.log(
@ -399,6 +385,17 @@ 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.

View File

@ -1,9 +1,14 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { editorManager, engineCommandManager } from 'lib/singletons' import { useStore } from 'useStore'
import { engineCommandManager } from 'lib/singletons'
import { useModelingContext } from './useModelingContext' import { useModelingContext } from './useModelingContext'
import { getEventForSelectWithPoint } from 'lib/selections' import { getEventForSelectWithPoint } from 'lib/selections'
export function useEngineConnectionSubscriptions() { export function useEngineConnectionSubscriptions() {
const { setHighlightRange, highlightRange } = useStore((s) => ({
setHighlightRange: s.setHighlightRange,
highlightRange: s.highlightRange,
}))
const { send, context } = useModelingContext() const { send, context } = useModelingContext()
useEffect(() => { useEffect(() => {
@ -16,13 +21,12 @@ export function useEngineConnectionSubscriptions() {
if (data?.entity_id) { if (data?.entity_id) {
const sourceRange = const sourceRange =
engineCommandManager.artifactMap?.[data.entity_id]?.range engineCommandManager.artifactMap?.[data.entity_id]?.range
editorManager.setHighlightRange(sourceRange) setHighlightRange(sourceRange)
} else if ( } else if (
!editorManager.highlightRange || !highlightRange ||
(editorManager.highlightRange[0] !== 0 && (highlightRange[0] !== 0 && highlightRange[1] !== 0)
editorManager.highlightRange[1] !== 0)
) { ) {
editorManager.setHighlightRange([0, 0]) setHighlightRange([0, 0])
} }
}, },
}) })
@ -39,5 +43,10 @@ export function useEngineConnectionSubscriptions() {
unSubHover() unSubHover()
unSubClick() unSubClick()
} }
}, [engineCommandManager, context?.sketchEnginePathId]) }, [
engineCommandManager,
setHighlightRange,
highlightRange,
context?.sketchEnginePathId,
])
} }

View File

@ -1,4 +1,4 @@
import { editorManager } from 'lib/singletons' import { useStore } from '../useStore'
import { useEffect } from 'react' import { useEffect } from 'react'
// Kurt's note: codeMirror styling overrides were needed to make this work // Kurt's note: codeMirror styling overrides were needed to make this work
@ -6,17 +6,20 @@ import { useEffect } from 'react'
// search for code-mirror-override in the repo to find the relevant styles // search for code-mirror-override in the repo to find the relevant styles
export function useHotKeyListener() { export function useHotKeyListener() {
const { setIsShiftDown } = useStore((s) => ({
setIsShiftDown: s.setIsShiftDown,
}))
const keyName = 'Shift' const keyName = 'Shift'
useEffect(() => { useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => const handleKeyDown = (event: KeyboardEvent) =>
event.key === keyName && editorManager.setIsShiftDown(true) event.key === keyName && setIsShiftDown(true)
const handleKeyUp = (event: KeyboardEvent) => const handleKeyUp = (event: KeyboardEvent) =>
event.key === keyName && editorManager.setIsShiftDown(false) event.key === keyName && setIsShiftDown(false)
window.addEventListener('keydown', handleKeyDown) window.addEventListener('keydown', handleKeyDown)
window.addEventListener('keyup', handleKeyUp) window.addEventListener('keyup', handleKeyUp)
return () => { return () => {
window.removeEventListener('keydown', handleKeyDown) window.removeEventListener('keydown', handleKeyDown)
window.removeEventListener('keyup', handleKeyUp) window.removeEventListener('keyup', handleKeyUp)
} }
}) }, [setIsShiftDown])
} }

View File

@ -11,11 +11,9 @@ export function useSetupEngineManager(
settings = { settings = {
theme: Themes.System, theme: Themes.System,
highlightEdges: true, highlightEdges: true,
enableSSAO: true,
} as { } as {
theme: Themes theme: Themes
highlightEdges: boolean highlightEdges: boolean
enableSSAO: boolean
} }
) { ) {
const { const {

View File

@ -2,7 +2,7 @@ import {
SetVarNameModal, SetVarNameModal,
createSetVarNameModal, createSetVarNameModal,
} from 'components/SetVarNameModal' } from 'components/SetVarNameModal'
import { editorManager, kclManager } from 'lib/singletons' import { kclManager } from 'lib/singletons'
import { moveValueIntoNewVariable } from 'lang/modifyAst' import { moveValueIntoNewVariable } from 'lang/modifyAst'
import { isNodeSafeToReplace } from 'lang/queryAst' import { isNodeSafeToReplace } from 'lang/queryAst'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
@ -13,11 +13,6 @@ const getModalInfo = createSetVarNameModal(SetVarNameModal)
export function useConvertToVariable() { export function useConvertToVariable() {
const { context } = useModelingContext() const { context } = useModelingContext()
const [enable, setEnabled] = useState(false) const [enable, setEnabled] = useState(false)
useEffect(() => {
editorManager.convertToVariableEnabled = enable
}, [enable])
useEffect(() => { useEffect(() => {
const { isSafe, value } = isNodeSafeToReplace( const { isSafe, value } = isNodeSafeToReplace(
kclManager.ast, kclManager.ast,
@ -50,7 +45,5 @@ export function useConvertToVariable() {
} }
} }
editorManager.convertToVariableCallback = handleClick
return { enable, handleClick } return { enable, handleClick }
} }

View File

@ -2,10 +2,12 @@ import { KCLError } from './errors'
import { createContext, useContext, useEffect, useState } from 'react' import { createContext, useContext, useEffect, useState } from 'react'
import { type IndexLoaderData } from 'lib/types' import { type IndexLoaderData } from 'lib/types'
import { useLoaderData } from 'react-router-dom' import { useLoaderData } from 'react-router-dom'
import { useParams } from 'react-router-dom'
import { codeManager, kclManager } from 'lib/singletons' import { codeManager, kclManager } from 'lib/singletons'
const KclContext = createContext({ const KclContext = createContext({
code: codeManager?.code || '', code: codeManager?.code || '',
editorCode: codeManager?.code || '',
programMemory: kclManager?.programMemory, programMemory: kclManager?.programMemory,
ast: kclManager?.ast, ast: kclManager?.ast,
isExecuting: kclManager?.isExecuting, isExecuting: kclManager?.isExecuting,
@ -28,6 +30,7 @@ export function KclContextProvider({
const { code: loadedCode } = useLoaderData() as IndexLoaderData const { code: loadedCode } = useLoaderData() as IndexLoaderData
// Both the code state and the editor state start off with the same code. // Both the code state and the editor state start off with the same code.
const [code, setCode] = useState(loadedCode || codeManager.code) const [code, setCode] = useState(loadedCode || codeManager.code)
const [editorCode, setEditorCode] = useState(code)
const [programMemory, setProgramMemory] = useState(kclManager.programMemory) const [programMemory, setProgramMemory] = useState(kclManager.programMemory)
const [ast, setAst] = useState(kclManager.ast) const [ast, setAst] = useState(kclManager.ast)
@ -39,6 +42,7 @@ export function KclContextProvider({
useEffect(() => { useEffect(() => {
codeManager.registerCallBacks({ codeManager.registerCallBacks({
setCode, setCode,
setEditorCode,
}) })
kclManager.registerCallBacks({ kclManager.registerCallBacks({
setProgramMemory, setProgramMemory,
@ -50,10 +54,15 @@ export function KclContextProvider({
}) })
}, []) }, [])
const params = useParams()
useEffect(() => {
codeManager.setParams(params)
}, [params])
return ( return (
<KclContext.Provider <KclContext.Provider
value={{ value={{
code, code,
editorCode,
programMemory, programMemory,
ast, ast,
isExecuting, isExecuting,

Some files were not shown because too many files have changed in this diff Show More