Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
e123a00d4b | |||
b950cc0583 | |||
c89780a489 | |||
1afed68dd7 | |||
dcbed4f06f | |||
379f154a5c | |||
60c4969322 | |||
cc6dee8ad4 | |||
2fc7c0d5fd | |||
bf2dcd808f | |||
ee21e486d4 | |||
b5a3eb9e9c | |||
c85645c9f2 | |||
cfa4dd2e33 | |||
c620f7269c | |||
2d8d29b345 | |||
00da062586 | |||
aafbaf6c50 | |||
2894c84a4e | |||
c01084feb0 | |||
c461db5f54 | |||
03fcb73aca | |||
8065e7e51a | |||
2d0ac249df | |||
3d73b82c23 |
35
.github/workflows/build-and-store-wasm.yml
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
name: Build and Store WASM
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build-and-upload:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
- name: Install Playwright Browsers
|
||||
run: yarn playwright install --with-deps
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Cache wasm
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
- name: build wasm
|
||||
run: yarn build:wasm
|
||||
|
||||
|
||||
# Upload the WASM bundle as an artifact
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: wasm-bundle
|
||||
path: src/wasm-lib/pkg
|
17
.github/workflows/cargo-clippy.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
dir: ['src/wasm-lib']
|
||||
dir: ['src/wasm-lib', 'src-tauri']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install latest rust
|
||||
@ -31,9 +31,22 @@ jobs:
|
||||
|
||||
- name: install dependencies
|
||||
if: matrix.dir == 'src-tauri'
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
sudo apt-get install -y \
|
||||
libgtk-3-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
webkit2gtk-driver \
|
||||
libsoup-3.0-dev \
|
||||
libjavascriptcoregtk-4.1-dev \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
at-spi2-core \
|
||||
xvfb
|
||||
yarn install
|
||||
yarn build:wasm
|
||||
yarn build:local
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2.6.1
|
||||
|
||||
|
57
.github/workflows/cargo-test-tauri.yml
vendored
Normal file
@ -0,0 +1,57 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'src-tauri/**.rs'
|
||||
- '**/Cargo.toml'
|
||||
- '**/Cargo.lock'
|
||||
- '**/rust-toolchain.toml'
|
||||
- .github/workflows/cargo-test-tauri.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src-tauri/**.rs'
|
||||
- '**/Cargo.toml'
|
||||
- '**/Cargo.lock'
|
||||
- '**/rust-toolchain.toml'
|
||||
- .github/workflows/cargo-test-tauri.yml
|
||||
workflow_dispatch:
|
||||
permissions: read-all
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
name: cargo test of tauri
|
||||
jobs:
|
||||
cargotest:
|
||||
name: cargo test
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
strategy:
|
||||
matrix:
|
||||
dir: ['src-tauri']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install latest rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: install dependencies
|
||||
if: matrix.dir == 'src-tauri'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libgtk-3-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
webkit2gtk-driver \
|
||||
libsoup-3.0-dev \
|
||||
libjavascriptcoregtk-4.1-dev \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
at-spi2-core \
|
||||
xvfb
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2.6.1
|
||||
- name: cargo test
|
||||
shell: bash
|
||||
run: |-
|
||||
cd "${{ matrix.dir }}"
|
||||
cargo test --all
|
36
.github/workflows/ci.yml
vendored
@ -50,7 +50,7 @@ jobs:
|
||||
- run: yarn tsc
|
||||
|
||||
|
||||
check-typos:
|
||||
check-typos:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@ -98,7 +98,7 @@ jobs:
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
|
||||
- name: Set nightly version
|
||||
if: github.event_name == 'schedule'
|
||||
run: |
|
||||
@ -143,21 +143,21 @@ jobs:
|
||||
ls -l artifact
|
||||
cp artifact/package.json package.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
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
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
|
||||
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: Sync node version and setup cache
|
||||
uses: actions/setup-node@v4
|
||||
@ -383,7 +383,7 @@ jobs:
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: 'artifact/*/Zoo*'
|
||||
|
||||
|
||||
announce_release:
|
||||
needs: [publish-apps-release]
|
||||
runs-on: ubuntu-latest
|
||||
@ -391,17 +391,17 @@ jobs:
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install requests
|
||||
|
||||
|
||||
- name: Announce Release
|
||||
env:
|
||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
|
5
.github/workflows/playwright.yml
vendored
@ -122,3 +122,8 @@ jobs:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: github.ref == 'refs/heads/main'
|
||||
with:
|
||||
name: wasm-bundle
|
||||
path: src/wasm-lib/pkg
|
||||
|
7410
docs/kcl/std.json
@ -104,6 +104,7 @@ test('Basic sketch', async ({ page }) => {
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)
|
||||
|> line([0, ${commonPoints.num1}], %)`)
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
@ -362,7 +363,7 @@ angle: 90
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
u.openDebugPanel()
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
@ -564,7 +565,9 @@ test('Auto complete works', async ({ page }) => {
|
||||
|
||||
await page.keyboard.press('Tab')
|
||||
await page.keyboard.type('12')
|
||||
await page.waitForTimeout(100)
|
||||
await page.keyboard.press('Tab')
|
||||
await page.waitForTimeout(100)
|
||||
await page.keyboard.press('Tab')
|
||||
await page.keyboard.press('Tab')
|
||||
await page.keyboard.press('Enter')
|
||||
@ -593,13 +596,12 @@ test('Auto complete works', async ({ page }) => {
|
||||
|
||||
test('Stored settings are validated and fall back to defaults', async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
const u = getUtils(page)
|
||||
|
||||
// Override beforeEach test setup
|
||||
// with corrupted settings
|
||||
await context.addInitScript(
|
||||
await page.addInitScript(
|
||||
async ({ settingsKey, settings }) => {
|
||||
localStorage.setItem(settingsKey, settings)
|
||||
},
|
||||
@ -616,18 +618,18 @@ test('Stored settings are validated and fall back to defaults', async ({
|
||||
// Check the settings were reset
|
||||
const storedSettings = TOML.parse(
|
||||
await page.evaluate(
|
||||
({ settingsKey }) => localStorage.getItem(settingsKey) || '{}',
|
||||
({ settingsKey }) => localStorage.getItem(settingsKey) || '',
|
||||
{ settingsKey: TEST_SETTINGS_KEY }
|
||||
)
|
||||
) as { settings: SaveSettingsPayload }
|
||||
|
||||
expect(storedSettings.settings.app?.theme).toBe('dark')
|
||||
expect(storedSettings.settings?.app?.theme).toBe(undefined)
|
||||
|
||||
// Check that the invalid settings were removed
|
||||
expect(storedSettings.settings.modeling?.defaultUnit).toBe(undefined)
|
||||
expect(storedSettings.settings.modeling?.mouseControls).toBe(undefined)
|
||||
expect(storedSettings.settings.app?.projectDirectory).toBe(undefined)
|
||||
expect(storedSettings.settings.projects?.defaultProjectName).toBe(undefined)
|
||||
expect(storedSettings.settings?.modeling?.defaultUnit).toBe(undefined)
|
||||
expect(storedSettings.settings?.modeling?.mouseControls).toBe(undefined)
|
||||
expect(storedSettings.settings?.app?.projectDirectory).toBe(undefined)
|
||||
expect(storedSettings.settings?.projects?.defaultProjectName).toBe(undefined)
|
||||
})
|
||||
|
||||
test('Project settings can be set and override user settings', async ({
|
||||
@ -734,7 +736,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
await u.openDebugPanel()
|
||||
|
||||
const xAxisClick = () =>
|
||||
page.mouse.click(700, 250).then(() => page.waitForTimeout(100))
|
||||
page.mouse.click(700, 253).then(() => page.waitForTimeout(100))
|
||||
const emptySpaceClick = () =>
|
||||
page.mouse.click(728, 343).then(() => page.waitForTimeout(100))
|
||||
const topHorzSegmentClick = () =>
|
||||
@ -759,6 +761,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)`)
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
@ -766,12 +769,14 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)`)
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)
|
||||
|> line([0, ${commonPoints.num1}], %)`)
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
@ -784,10 +789,14 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Line' }).click()
|
||||
|
||||
await u.closeDebugPanel()
|
||||
const selectionSequence = async () => {
|
||||
const selectionSequence = async (isSecondTime = false) => {
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
|
||||
await page.mouse.move(startXPx + PUR * 15, 500 - PUR * 10)
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.move(
|
||||
startXPx + PUR * 15,
|
||||
isSecondTime ? 430 : 500 - PUR * 10
|
||||
)
|
||||
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
// bg-yellow-200 is more brittle than hover-highlight, but is closer to the user experience
|
||||
@ -797,7 +806,10 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
// check mousing off, than mousing onto another line
|
||||
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 15) // mouse off
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 20) // mouse onto another line
|
||||
await page.mouse.move(
|
||||
startXPx + PUR * 10,
|
||||
isSecondTime ? 295 : 500 - PUR * 20
|
||||
) // mouse onto another line
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
|
||||
// now check clicking works including axis
|
||||
@ -807,6 +819,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
await page.keyboard.down('Shift')
|
||||
const absYButton = page.getByRole('button', { name: 'ABS Y' })
|
||||
await expect(absYButton).toBeDisabled()
|
||||
await page.waitForTimeout(100)
|
||||
await xAxisClick()
|
||||
await page.keyboard.up('Shift')
|
||||
await absYButton.and(page.locator(':not([disabled])')).waitFor()
|
||||
@ -815,10 +828,12 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
// clear selection by clicking on nothing
|
||||
await emptySpaceClick()
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
// same selection but click the axis first
|
||||
await xAxisClick()
|
||||
await expect(absYButton).toBeDisabled()
|
||||
await page.keyboard.down('Shift')
|
||||
await page.waitForTimeout(100)
|
||||
await topHorzSegmentClick()
|
||||
|
||||
await page.keyboard.up('Shift')
|
||||
@ -831,6 +846,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click()
|
||||
await page.keyboard.down('Shift')
|
||||
await expect(absYButton).toBeDisabled()
|
||||
await page.waitForTimeout(100)
|
||||
await xAxisClick()
|
||||
await page.keyboard.up('Shift')
|
||||
await expect(absYButton).not.toBeDisabled()
|
||||
@ -873,11 +889,16 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// enter sketch again
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.getByRole('button', { name: 'Edit Sketch' }).click(),
|
||||
'default_camera_get_settings'
|
||||
)
|
||||
await page.waitForTimeout(150)
|
||||
|
||||
await page.waitForTimeout(300) // wait for animation
|
||||
|
||||
// hover again and check it works
|
||||
await selectionSequence()
|
||||
await selectionSequence(true)
|
||||
})
|
||||
|
||||
test.describe('Command bar tests', () => {
|
||||
@ -1013,6 +1034,7 @@ const part001 = startSketchOn('-XZ')
|
||||
})
|
||||
|
||||
test('Can add multiple sketches', async ({ page }) => {
|
||||
test.skip(process.platform === 'darwin', 'Can add multiple sketches')
|
||||
const u = getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
@ -1063,6 +1085,7 @@ test('Can add multiple sketches', async ({ page }) => {
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)
|
||||
|> line([0, ${commonPoints.num1}], %)`)
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
const finalCodeFirstSketch = `const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
@ -1078,24 +1101,33 @@ test('Can add multiple sketches', async ({ page }) => {
|
||||
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
|
||||
await u.updateCamPosition([0, 100, 100])
|
||||
await u.updateCamPosition([100, 100, 100])
|
||||
await page.waitForTimeout(250)
|
||||
|
||||
// start a new sketch
|
||||
await u.clearCommandLogs()
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(673, 384)
|
||||
await page.waitForTimeout(400)
|
||||
await page.mouse.click(650, 450)
|
||||
|
||||
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||
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)
|
||||
const startAt2 = '[0.93,-1.25]'
|
||||
const startAt2 =
|
||||
process.platform === 'darwin' ? '[9.75, -13.16]' : '[0.93, -1.25]'
|
||||
await expect(
|
||||
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
||||
).toBe(
|
||||
`${finalCodeFirstSketch}
|
||||
const part002 = startSketchOn('XY')
|
||||
const part002 = startSketchOn('${plane}')
|
||||
|> startProfileAt(${startAt2}, %)`.replace(/\s/g, '')
|
||||
)
|
||||
await page.waitForTimeout(100)
|
||||
@ -1104,12 +1136,12 @@ const part002 = startSketchOn('XY')
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
const num2 = 0.94
|
||||
const num2 = process.platform === 'darwin' ? 9.84 : 0.94
|
||||
await expect(
|
||||
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
||||
).toBe(
|
||||
`${finalCodeFirstSketch}
|
||||
const part002 = startSketchOn('XY')
|
||||
const part002 = startSketchOn('${plane}')
|
||||
|> startProfileAt(${startAt2}, %)
|
||||
|> line([${num2}, 0], %)`.replace(/\s/g, '')
|
||||
)
|
||||
@ -1119,21 +1151,29 @@ const part002 = startSketchOn('XY')
|
||||
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
||||
).toBe(
|
||||
`${finalCodeFirstSketch}
|
||||
const part002 = startSketchOn('XY')
|
||||
const part002 = startSketchOn('${plane}')
|
||||
|> startProfileAt(${startAt2}, %)
|
||||
|> line([${num2}, 0], %)
|
||||
|> line([0, ${roundOff(num2 - 0.01)}], %)`.replace(/\s/g, '')
|
||||
|> line([0, ${roundOff(
|
||||
num2 + (process.platform === 'darwin' ? 0.01 : -0.01)
|
||||
)}], %)`.replace(/\s/g, '')
|
||||
)
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
await expect(
|
||||
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
||||
).toBe(
|
||||
`${finalCodeFirstSketch}
|
||||
const part002 = startSketchOn('XY')
|
||||
const part002 = startSketchOn('${plane}')
|
||||
|> startProfileAt(${startAt2}, %)
|
||||
|> line([${num2}, 0], %)
|
||||
|> line([0, ${roundOff(num2 - 0.01)}], %)
|
||||
|> line([-1.87, 0], %)`.replace(/\s/g, '')
|
||||
|> line([0, ${roundOff(
|
||||
num2 + (process.platform === 'darwin' ? 0.01 : -0.01)
|
||||
)}], %)
|
||||
|> line([-${process.platform === 'darwin' ? 19.59 : 1.87}, 0], %)`.replace(
|
||||
/\s/g,
|
||||
''
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
@ -1337,10 +1377,12 @@ test('Deselecting line tool should mean nothing happens on click', async ({
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(700, 300)
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(750, 300)
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||
@ -1365,16 +1407,16 @@ test('Can edit segments by dragging their handles', async ({ page }) => {
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
|
||||
const startPX = [652, 418]
|
||||
const lineEndPX = [794, 416]
|
||||
const arcEndPX = [893, 318]
|
||||
const startPX = [665, 458]
|
||||
const lineEndPX = [842, 458]
|
||||
const arcEndPX = [971, 342]
|
||||
|
||||
const dragPX = 30
|
||||
|
||||
await page.getByText('startProfileAt([4.61, -14.01], %)').click()
|
||||
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
await page.waitForTimeout(400)
|
||||
let prevContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
const step5 = { steps: 5 }
|
||||
@ -1384,7 +1426,7 @@ test('Can edit segments by dragging their handles', async ({ page }) => {
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
|
||||
await page.mouse.up()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||
prevContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
@ -1412,9 +1454,9 @@ test('Can edit segments by dragging their handles', async ({ page }) => {
|
||||
// expect the code to have changed
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([7.01, -11.79], %)
|
||||
|> line([14.69, 2.73], %)
|
||||
|> tangentialArcTo([27.6, -3.25], %)`)
|
||||
|> startProfileAt([6.44, -12.07], %)
|
||||
|> line([14.04, 2.03], %)
|
||||
|> tangentialArcTo([27.19, -4.2], %)`)
|
||||
})
|
||||
|
||||
const doSnapAtDifferentScales = async (
|
||||
@ -1533,38 +1575,46 @@ test('Sketch on face', async ({ page }) => {
|
||||
).not.toBeDisabled()
|
||||
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
let previousCodeContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
await page.mouse.click(793, 133)
|
||||
await u.openAndClearDebugPanel()
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.mouse.click(793, 133),
|
||||
'default_camera_get_settings',
|
||||
true
|
||||
)
|
||||
await page.waitForTimeout(150)
|
||||
|
||||
const firstClickPosition = [612, 238]
|
||||
const secondClickPosition = [661, 242]
|
||||
const thirdClickPosition = [609, 267]
|
||||
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
await page.mouse.click(firstClickPosition[0], firstClickPosition[1])
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(secondClickPosition[0], secondClickPosition[1])
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(thirdClickPosition[0], thirdClickPosition[1])
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(firstClickPosition[0], firstClickPosition[1])
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|
||||
|> startProfileAt([1.03, 1.03], %)
|
||||
|> line([4.18, -0.35], %)
|
||||
|> line([-4.44, -2.13], %)
|
||||
|> startProfileAt([-12.83, 6.7], %)
|
||||
|> line([2.87, -0.23], %)
|
||||
|> line([-3.05, -1.47], %)
|
||||
|> close(%)`)
|
||||
|
||||
await u.openAndClearDebugPanel()
|
||||
@ -1574,9 +1624,14 @@ test('Sketch on face', async ({ page }) => {
|
||||
await u.updateCamPosition([1049, 239, 686])
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await page.getByText('startProfileAt([1.03, 1.03], %)').click()
|
||||
await page.getByText('startProfileAt([-12.83, 6.7], %)').click()
|
||||
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.getByRole('button', { name: 'Edit Sketch' }).click(),
|
||||
'default_camera_get_settings',
|
||||
true
|
||||
)
|
||||
await page.waitForTimeout(150)
|
||||
await page.setViewportSize({ width: 1200, height: 1200 })
|
||||
await u.openAndClearDebugPanel()
|
||||
await u.updateCamPosition([452, -152, 1166])
|
||||
@ -1596,11 +1651,11 @@ test('Sketch on face', async ({ page }) => {
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|
||||
|> startProfileAt([1.03, 1.03], %)
|
||||
|> line([${process?.env?.CI ? 2.74 : 2.93}, -${
|
||||
process?.env?.CI ? 0.24 : 0.2
|
||||
|> startProfileAt([-12.83, 6.7], %)
|
||||
|> line([${process?.env?.CI ? 2.28 : 2.28}, -${
|
||||
process?.env?.CI ? 0.07 : 0.07
|
||||
}], %)
|
||||
|> line([-4.44, -2.13], %)
|
||||
|> line([-3.05, -1.47], %)
|
||||
|> close(%)`)
|
||||
|
||||
// exit sketch
|
||||
@ -1608,7 +1663,7 @@ test('Sketch on face', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
|
||||
await page.getByText('startProfileAt([1.03, 1.03], %)').click()
|
||||
await page.getByText('startProfileAt([-12.83, 6.7], %)').click()
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Extrude' })).not.toBeDisabled()
|
||||
await page.getByRole('button', { name: 'Extrude' }).click()
|
||||
@ -1622,11 +1677,11 @@ test('Sketch on face', async ({ page }) => {
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|
||||
|> startProfileAt([1.03, 1.03], %)
|
||||
|> line([${process?.env?.CI ? 2.74 : 2.93}, -${
|
||||
process?.env?.CI ? 0.24 : 0.2
|
||||
|> startProfileAt([-12.83, 6.7], %)
|
||||
|> line([${process?.env?.CI ? 2.28 : 2.28}, -${
|
||||
process?.env?.CI ? 0.07 : 0.07
|
||||
}], %)
|
||||
|> line([-4.44, -2.13], %)
|
||||
|> line([-3.05, -1.47], %)
|
||||
|> close(%)
|
||||
|> extrude(5 + 7, %)`)
|
||||
})
|
||||
@ -1659,11 +1714,11 @@ test('Can code mod a line length', async ({ page }) => {
|
||||
|
||||
// enter sketch again
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
await page.waitForTimeout(300) // wait for animation
|
||||
await page.waitForTimeout(350) // wait for animation
|
||||
|
||||
const startXPx = 500
|
||||
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
|
||||
await page.mouse.click(615, 133)
|
||||
await page.mouse.click(615, 102)
|
||||
await page.getByRole('button', { name: 'length', exact: true }).click()
|
||||
await page.getByText('Add constraining value').click()
|
||||
|
||||
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 46 KiB |
@ -1,12 +1,13 @@
|
||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||
import { Themes } from 'lib/theme'
|
||||
|
||||
export const TEST_SETTINGS_KEY = '/user.toml'
|
||||
export const TEST_SETTINGS_KEY = '/settings.toml'
|
||||
export const TEST_SETTINGS = {
|
||||
app: {
|
||||
theme: Themes.Dark,
|
||||
onboardingStatus: 'dismissed',
|
||||
projectDirectory: '',
|
||||
enableSSAO: false,
|
||||
},
|
||||
modeling: {
|
||||
defaultUnit: 'in',
|
||||
@ -23,7 +24,7 @@ export const TEST_SETTINGS = {
|
||||
|
||||
export const TEST_SETTINGS_ONBOARDING = {
|
||||
...TEST_SETTINGS,
|
||||
app: { ...TEST_SETTINGS.app, onboardingStatus: '/export ' },
|
||||
app: { ...TEST_SETTINGS.app, onboardingStatus: '/export' },
|
||||
} satisfies Partial<SaveSettingsPayload>
|
||||
|
||||
export const TEST_SETTINGS_CORRUPTED = {
|
||||
|
@ -71,7 +71,7 @@ describe('ZMA (Tauri, Linux)', () => {
|
||||
|
||||
// Now should be signed in
|
||||
const newFileButton = await $('[data-testid="home-new-file"]')
|
||||
expect(await newFileButton.getText()).toEqual('New file')
|
||||
expect(await newFileButton.getText()).toEqual('New project')
|
||||
})
|
||||
|
||||
it('opens the settings page, checks filesystem settings, and closes the settings page', async () => {
|
||||
|
@ -57,7 +57,7 @@ echo "New version number without 'v': $new_version_number"
|
||||
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" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)" > src-tauri/tauri.conf.json
|
||||
echo "$(jq --arg v "$new_version_number" '.version=$v' src-tauri/tauri.conf.json --indent 2)" > src-tauri/tauri.conf.json
|
||||
|
||||
git add package.json src-tauri/tauri.conf.json
|
||||
git commit -m "Cut release $new_version"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "untitled-app",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.16.0",
|
||||
|
2324
src-tauri/Cargo.lock
generated
@ -8,27 +8,26 @@ repository = "https://github.com/KittyCAD/modeling-app"
|
||||
default-run = "app"
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.0-beta.12", features = [] }
|
||||
tauri-build = { version = "2.0.0-beta.13", features = [] }
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
kittycad = "0.2.67"
|
||||
kcl-lib = { version = "0.1.51", path = "../src/wasm-lib/kcl" }
|
||||
kittycad = "0.3.0"
|
||||
oauth2 = "4.4.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] }
|
||||
tauri-plugin-dialog = { version = "2.0.0-beta.5" }
|
||||
tauri-plugin-fs = { version = "2.0.0-beta.5" }
|
||||
tauri-plugin-http = { version = "2.0.0-beta.5" }
|
||||
tauri-plugin-dialog = { version = "2.0.0-beta.6" }
|
||||
tauri-plugin-fs = { version = "2.0.0-beta.6" }
|
||||
tauri-plugin-http = { version = "2.0.0-beta.6" }
|
||||
tauri-plugin-os = { version = "2.0.0-beta.2" }
|
||||
tauri-plugin-process = { version = "2.0.0-beta.2" }
|
||||
tauri-plugin-shell = { version = "2.0.0-beta.2" }
|
||||
tauri-plugin-updater = { version = "2.0.0-beta.4" }
|
||||
tokio = { version = "1.37.0", features = ["time"] }
|
||||
tokio = { version = "1.37.0", features = ["time", "fs"] }
|
||||
toml = "0.8.2"
|
||||
|
||||
[features]
|
||||
|
6
src-tauri/rustfmt.toml
Normal file
@ -0,0 +1,6 @@
|
||||
max_width = 120
|
||||
edition = "2018"
|
||||
format_code_in_doc_comments = true
|
||||
format_strings = false
|
||||
imports_granularity = "Crate"
|
||||
group_imports = "StdExternalCrate"
|
@ -1,91 +1,189 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::{
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use kcl_lib::settings::types::{
|
||||
file::{FileEntry, Project},
|
||||
project::ProjectConfiguration,
|
||||
Configuration,
|
||||
};
|
||||
use oauth2::TokenResponse;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
use tauri::ipc::InvokeError;
|
||||
use tauri::{ipc::InvokeError, Manager};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
const DEFAULT_HOST: &str = "https://api.kittycad.io";
|
||||
|
||||
/// This command returns the a json string parse from a toml file at the path.
|
||||
const DEFAULT_HOST: &str = "https://api.zoo.dev";
|
||||
const SETTINGS_FILE_NAME: &str = "settings.toml";
|
||||
const PROJECT_SETTINGS_FILE_NAME: &str = "project.toml";
|
||||
const PROJECT_FOLDER: &str = "zoo-modeling-app-projects";
|
||||
|
||||
#[tauri::command]
|
||||
fn read_toml(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()))?;
|
||||
let value =
|
||||
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)
|
||||
fn get_initial_default_dir(app: tauri::AppHandle) -> Result<PathBuf, InvokeError> {
|
||||
let dir = match app.path().document_dir() {
|
||||
Ok(dir) => dir,
|
||||
Err(_) => {
|
||||
// for headless Linux (eg. Github Actions)
|
||||
let home_dir = app.path().home_dir()?;
|
||||
home_dir.join("Documents")
|
||||
}
|
||||
};
|
||||
|
||||
Ok(dir.join(PROJECT_FOLDER))
|
||||
}
|
||||
|
||||
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
|
||||
/// Removed from tauri v2
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct DiskEntry {
|
||||
/// The path to the entry.
|
||||
pub path: PathBuf,
|
||||
/// The name of the entry (file name with extension or directory name).
|
||||
pub name: Option<String>,
|
||||
/// The children of this entry if it's a directory.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub children: Option<Vec<DiskEntry>>,
|
||||
fn get_app_settings_file_path(app: &tauri::AppHandle) -> Result<PathBuf, InvokeError> {
|
||||
let app_config_dir = app.path().app_config_dir()?;
|
||||
Ok(app_config_dir.join(SETTINGS_FILE_NAME))
|
||||
}
|
||||
|
||||
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
|
||||
/// Removed from tauri v2
|
||||
fn is_dir<P: AsRef<Path>>(path: P) -> Result<bool> {
|
||||
std::fs::metadata(path)
|
||||
.map(|md| md.is_dir())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
|
||||
/// Removed from tauri v2
|
||||
#[tauri::command]
|
||||
fn read_dir_recursive(path: &str) -> Result<Vec<DiskEntry>, InvokeError> {
|
||||
let mut files_and_dirs: Vec<DiskEntry> = vec![];
|
||||
// let path = path.as_ref();
|
||||
for entry in fs::read_dir(path).map_err(|e| InvokeError::from_anyhow(e.into()))? {
|
||||
let path = entry
|
||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?
|
||||
.path();
|
||||
async fn read_app_settings_file(app: tauri::AppHandle) -> Result<Configuration, InvokeError> {
|
||||
let mut settings_path = get_app_settings_file_path(&app)?;
|
||||
let mut needs_migration = false;
|
||||
|
||||
if let Ok(flag) = is_dir(&path) {
|
||||
files_and_dirs.push(DiskEntry {
|
||||
path: path.clone(),
|
||||
children: if flag {
|
||||
Some(read_dir_recursive(path.to_str().expect("No path"))?)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
name: path
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy())
|
||||
.map(|name| name.to_string()),
|
||||
});
|
||||
// Check if this file exists.
|
||||
if !settings_path.exists() {
|
||||
// Try the backwards compatible path.
|
||||
// TODO: Remove this after a few releases.
|
||||
let app_config_dir = app.path().app_config_dir()?;
|
||||
settings_path = format!(
|
||||
"{}user.toml",
|
||||
app_config_dir.display().to_string().trim_end_matches('/')
|
||||
)
|
||||
.into();
|
||||
needs_migration = true;
|
||||
// Check if this path exists.
|
||||
if !settings_path.exists() {
|
||||
let mut default = Configuration::default();
|
||||
default.settings.project.directory = get_initial_default_dir(app.clone())?;
|
||||
// Return the default configuration.
|
||||
return Ok(default);
|
||||
}
|
||||
}
|
||||
Ok(files_and_dirs)
|
||||
|
||||
let contents = tokio::fs::read_to_string(&settings_path)
|
||||
.await
|
||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||
let mut parsed = Configuration::backwards_compatible_toml_parse(&contents).map_err(InvokeError::from_anyhow)?;
|
||||
if parsed.settings.project.directory == PathBuf::new() {
|
||||
parsed.settings.project.directory = get_initial_default_dir(app.clone())?;
|
||||
}
|
||||
|
||||
// TODO: Remove this after a few releases.
|
||||
if needs_migration {
|
||||
write_app_settings_file(app, parsed.clone()).await?;
|
||||
// Delete the old file.
|
||||
tokio::fs::remove_file(settings_path)
|
||||
.await
|
||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||
}
|
||||
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
/// This command returns a string that is the contents of a file at the path.
|
||||
#[tauri::command]
|
||||
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)
|
||||
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(contents)
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_project_settings_file_path(app_settings: Configuration, project_name: &str) -> Result<PathBuf, InvokeError> {
|
||||
Ok(app_settings
|
||||
.settings
|
||||
.project
|
||||
.directory
|
||||
.join(project_name)
|
||||
.join(PROJECT_SETTINGS_FILE_NAME))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn read_project_settings_file(
|
||||
app_settings: Configuration,
|
||||
project_name: &str,
|
||||
) -> Result<ProjectConfiguration, InvokeError> {
|
||||
let settings_path = get_project_settings_file_path(app_settings, project_name)?;
|
||||
|
||||
// Check if this file exists.
|
||||
if !settings_path.exists() {
|
||||
// Return the default configuration.
|
||||
return Ok(ProjectConfiguration::default());
|
||||
}
|
||||
|
||||
let contents = tokio::fs::read_to_string(&settings_path)
|
||||
.await
|
||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||
let parsed = ProjectConfiguration::backwards_compatible_toml_parse(&contents).map_err(InvokeError::from_anyhow)?;
|
||||
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn write_project_settings_file(
|
||||
app_settings: Configuration,
|
||||
project_name: &str,
|
||||
configuration: ProjectConfiguration,
|
||||
) -> Result<(), InvokeError> {
|
||||
let settings_path = get_project_settings_file_path(app_settings, project_name)?;
|
||||
let contents = toml::to_string_pretty(&configuration).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||
tokio::fs::write(settings_path, contents.as_bytes())
|
||||
.await
|
||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialize the directory that holds all the projects.
|
||||
#[tauri::command]
|
||||
async fn initialize_project_directory(configuration: Configuration) -> Result<PathBuf, InvokeError> {
|
||||
configuration
|
||||
.ensure_project_directory_exists()
|
||||
.await
|
||||
.map_err(InvokeError::from_anyhow)
|
||||
}
|
||||
|
||||
/// Create a new project directory.
|
||||
#[tauri::command]
|
||||
async fn create_new_project_directory(
|
||||
configuration: Configuration,
|
||||
project_name: &str,
|
||||
initial_code: Option<&str>,
|
||||
) -> Result<Project, InvokeError> {
|
||||
configuration
|
||||
.create_new_project_directory(project_name, initial_code)
|
||||
.await
|
||||
.map_err(InvokeError::from_anyhow)
|
||||
}
|
||||
|
||||
/// List all the projects in the project directory.
|
||||
#[tauri::command]
|
||||
async fn list_projects(configuration: Configuration) -> Result<Vec<Project>, InvokeError> {
|
||||
configuration.list_projects().await.map_err(InvokeError::from_anyhow)
|
||||
}
|
||||
|
||||
/// Get information about a project.
|
||||
#[tauri::command]
|
||||
async fn get_project_info(configuration: Configuration, project_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.
|
||||
@ -103,8 +201,7 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
|
||||
let auth_client = oauth2::basic::BasicClient::new(
|
||||
oauth2::ClientId::new(client_id),
|
||||
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(
|
||||
oauth2::TokenUrl::new(format!("{host}/oauth2/device/token"))
|
||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?,
|
||||
@ -132,12 +229,10 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
|
||||
// and bypass the shell::open call as it fails on GitHub Actions.
|
||||
let e2e_tauri_enabled = env::var("E2E_TAURI_ENABLED").is_ok();
|
||||
if e2e_tauri_enabled {
|
||||
println!(
|
||||
"E2E_TAURI_ENABLED is set, won't open {} externally",
|
||||
auth_uri.secret()
|
||||
);
|
||||
fs::write("/tmp/kittycad_user_code", details.user_code().secret())
|
||||
.expect("Unable to write /tmp/kittycad_user_code file");
|
||||
println!("E2E_TAURI_ENABLED is set, won't open {} externally", auth_uri.secret());
|
||||
tokio::fs::write("/tmp/kittycad_user_code", details.user_code().secret())
|
||||
.await
|
||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||
} else {
|
||||
app.shell()
|
||||
.open(auth_uri.secret(), None)
|
||||
@ -160,10 +255,7 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
|
||||
///This command returns the KittyCAD user info given a token.
|
||||
/// The string returned from this method is the user info as a json string.
|
||||
#[tauri::command]
|
||||
async fn get_user(
|
||||
token: Option<String>,
|
||||
hostname: &str,
|
||||
) -> Result<kittycad::types::User, InvokeError> {
|
||||
async fn get_user(token: &str, hostname: &str) -> Result<kittycad::types::User, InvokeError> {
|
||||
// Use the host passed in if it's set.
|
||||
// Otherwise, use the default host.
|
||||
let host = if hostname.is_empty() {
|
||||
@ -183,7 +275,7 @@ async fn get_user(
|
||||
println!("Getting user info...");
|
||||
|
||||
// use kittycad library to fetch the user info from /user/me
|
||||
let mut client = kittycad::Client::new(token.unwrap());
|
||||
let mut client = kittycad::Client::new(token);
|
||||
|
||||
if baseurl != DEFAULT_HOST {
|
||||
client.set_base_url(&baseurl);
|
||||
@ -202,43 +294,53 @@ async fn get_user(
|
||||
/// 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.
|
||||
#[tauri::command]
|
||||
fn show_in_folder(path: String) {
|
||||
#[cfg(target_os = "windows")]
|
||||
fn show_in_folder(path: &str) -> Result<(), InvokeError> {
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
Command::new("explorer")
|
||||
.args(["/select,", &path]) // The comma after select is not a typo
|
||||
.spawn()
|
||||
.unwrap();
|
||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(unix)]
|
||||
{
|
||||
Command::new("open").args(["-R", &path]).spawn().unwrap();
|
||||
Command::new("open")
|
||||
.args(["-R", &path])
|
||||
.spawn()
|
||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn main() -> Result<()> {
|
||||
tauri::Builder::default()
|
||||
.setup(|_app| {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
use tauri::Manager;
|
||||
_app.get_webview("main").unwrap().open_devtools();
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
_app.handle()
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())?;
|
||||
_app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_initial_default_dir,
|
||||
initialize_project_directory,
|
||||
create_new_project_directory,
|
||||
list_projects,
|
||||
get_project_info,
|
||||
get_user,
|
||||
login,
|
||||
read_toml,
|
||||
read_txt_file,
|
||||
read_dir_recursive,
|
||||
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_fs::init())
|
||||
@ -246,6 +348,7 @@ fn main() {
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
.run(tauri::generate_context!())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -55,5 +55,5 @@
|
||||
}
|
||||
},
|
||||
"productName": "Zoo Modeling App",
|
||||
"version": "0.18.0"
|
||||
"version": "0.19.0"
|
||||
}
|
||||
|
@ -246,13 +246,31 @@ export class CameraControls {
|
||||
camSettings.center.y,
|
||||
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) {
|
||||
this.camera.fov = camSettings.fov_y
|
||||
} else if (
|
||||
this.camera instanceof OrthographicCamera &&
|
||||
camSettings.ortho_scale
|
||||
) {
|
||||
this.camera.zoom = camSettings.ortho_scale
|
||||
const distanceToTarget = new Vector3(
|
||||
camSettings.pos.x,
|
||||
camSettings.pos.y,
|
||||
camSettings.pos.z
|
||||
).distanceTo(
|
||||
new Vector3(
|
||||
camSettings.center.x,
|
||||
camSettings.center.y,
|
||||
camSettings.center.z
|
||||
)
|
||||
)
|
||||
this.camera.zoom = (camSettings.ortho_scale * 40) / distanceToTarget
|
||||
}
|
||||
this.onCameraChange()
|
||||
}
|
||||
@ -965,10 +983,10 @@ export class CameraControls {
|
||||
// Pure function helpers
|
||||
|
||||
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_far = 1000 + nearFarRatio * (100000 - 1000)
|
||||
return { z_near: 0.1, z_far }
|
||||
// const z_far = 1000 + nearFarRatio * (100000 - 1000)
|
||||
return { z_near: 0.1, z_far: 1000 }
|
||||
}
|
||||
|
||||
function convertThreeCamValuesToEngineCam({
|
||||
@ -1043,3 +1061,62 @@ function _getInteractionType(
|
||||
if (enableZoom && interactionGuards.zoom.dragCallback(event)) return 'zoom'
|
||||
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',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -215,8 +215,9 @@ export class SceneEntities {
|
||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||
const baseXColor = 0x000055
|
||||
const baseYColor = 0x550000
|
||||
const xAxisGeometry = new BoxGeometry(100000, 0.3, 0.01)
|
||||
const yAxisGeometry = new BoxGeometry(0.3, 100000, 0.01)
|
||||
const axisPixelWidth = 1.6
|
||||
const xAxisGeometry = new BoxGeometry(100000, axisPixelWidth, 0.01)
|
||||
const yAxisGeometry = new BoxGeometry(axisPixelWidth, 100000, 0.01)
|
||||
const xAxisMaterial = new MeshBasicMaterial({
|
||||
color: baseXColor,
|
||||
depthTest: false,
|
||||
@ -1324,30 +1325,31 @@ export class SceneEntities {
|
||||
selected.material.color = defaultPlaneColor(type)
|
||||
},
|
||||
onClick: async (args) => {
|
||||
const checkExtrudeFaceClick = async (): Promise<boolean> => {
|
||||
const checkExtrudeFaceClick = async (): Promise<
|
||||
['face' | 'plane' | 'other', string]
|
||||
> => {
|
||||
const { streamDimensions } = useStore.getState()
|
||||
const { entity_id } = await sendSelectEventToEngine(
|
||||
args?.mouseEvent,
|
||||
document.getElementById('video-stream') as HTMLVideoElement,
|
||||
streamDimensions
|
||||
)
|
||||
if (!entity_id) return false
|
||||
if (!entity_id) return ['other', '']
|
||||
if (
|
||||
engineCommandManager.defaultPlanes?.xy === entity_id ||
|
||||
engineCommandManager.defaultPlanes?.xz === entity_id ||
|
||||
engineCommandManager.defaultPlanes?.yz === entity_id
|
||||
) {
|
||||
return ['plane', entity_id]
|
||||
}
|
||||
const artifact = this.engineCommandManager.artifactMap[entity_id]
|
||||
if (artifact?.commandType !== 'solid3d_get_extrusion_face_info')
|
||||
return false
|
||||
const faceInfo: Models['FaceIsPlanar_type'] = (
|
||||
await this.engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'face_is_planar',
|
||||
object_id: entity_id,
|
||||
},
|
||||
})
|
||||
)?.data?.data
|
||||
return ['other', entity_id]
|
||||
|
||||
const faceInfo = await getFaceDetails(entity_id)
|
||||
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
|
||||
return false
|
||||
const { z_axis, origin, y_axis } = faceInfo
|
||||
return ['other', entity_id]
|
||||
const { z_axis, y_axis, origin } = faceInfo
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
kclManager.ast,
|
||||
artifact.range
|
||||
@ -1367,12 +1369,15 @@ export class SceneEntities {
|
||||
artifact?.additionalData?.type === 'cap'
|
||||
? artifact.additionalData.info
|
||||
: 'none',
|
||||
faceId: entity_id,
|
||||
},
|
||||
})
|
||||
return true
|
||||
return ['face', entity_id]
|
||||
}
|
||||
|
||||
if (await checkExtrudeFaceClick()) return
|
||||
const faceResult = await checkExtrudeFaceClick()
|
||||
console.log('faceResult', faceResult)
|
||||
if (faceResult[0] === 'face') return
|
||||
|
||||
if (!args || !args.intersects?.[0]) return
|
||||
if (args.mouseEvent.which !== 1) return
|
||||
@ -1398,6 +1403,7 @@ export class SceneEntities {
|
||||
plane: planeString,
|
||||
zAxis,
|
||||
yAxis,
|
||||
planeId: faceResult[1],
|
||||
},
|
||||
})
|
||||
},
|
||||
@ -1681,7 +1687,7 @@ export async function getSketchOrientationDetails(
|
||||
sketchPathToNode: PathToNode
|
||||
): Promise<{
|
||||
quat: Quaternion
|
||||
sketchDetails: SketchDetails
|
||||
sketchDetails: SketchDetails & { faceId?: string }
|
||||
}> {
|
||||
const sketchGroup = sketchGroupFromPathToNode({
|
||||
pathToNode: sketchPathToNode,
|
||||
@ -1697,20 +1703,13 @@ export async function getSketchOrientationDetails(
|
||||
zAxis: [zAxis.x, zAxis.y, zAxis.z],
|
||||
yAxis: [sketchGroup.yAxis.x, sketchGroup.yAxis.y, sketchGroup.yAxis.z],
|
||||
origin: [0, 0, 0],
|
||||
faceId: sketchGroup.on.id,
|
||||
},
|
||||
}
|
||||
}
|
||||
if (sketchGroup.on.type === 'face') {
|
||||
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
|
||||
const faceInfo = await getFaceDetails(sketchGroup.on.faceId)
|
||||
|
||||
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
|
||||
throw new Error('faceInfo')
|
||||
const { z_axis, y_axis, origin } = faceInfo
|
||||
@ -1725,6 +1724,7 @@ export async function getSketchOrientationDetails(
|
||||
zAxis: [z_axis.x, z_axis.y, z_axis.z],
|
||||
yAxis: [y_axis.x, y_axis.y, y_axis.z],
|
||||
origin: [origin.x, origin.y, origin.z],
|
||||
faceId: sketchGroup.on.faceId,
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1733,6 +1733,46 @@ export async function getSketchOrientationDetails(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves orientation details for a given entity representing a face (brep face or default plane).
|
||||
* This function asynchronously fetches and returns the origin, x-axis, y-axis, and z-axis details
|
||||
* for a specified entity ID. It is primarily used to obtain the orientation of a face in the scene,
|
||||
* which is essential for calculating the correct positioning and alignment of the client side sketch.
|
||||
*
|
||||
* @param entityId - The ID of the entity for which orientation details are being fetched.
|
||||
* @returns A promise that resolves with the orientation details of the face.
|
||||
*/
|
||||
async function getFaceDetails(
|
||||
entityId: string
|
||||
): Promise<Models['FaceIsPlanar_type']> {
|
||||
// TODO mode engine connection to allow batching returns and batch the following
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'enable_sketch_mode',
|
||||
adjust_camera: false,
|
||||
animated: false,
|
||||
ortho: false,
|
||||
entity_id: entityId,
|
||||
},
|
||||
})
|
||||
// TODO change typing to get_sketch_mode_plane once lib is updated
|
||||
const faceInfo: Models['FaceIsPlanar_type'] = (
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'get_sketch_mode_plane' },
|
||||
})
|
||||
)?.data?.data
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'sketch_mode_disable' },
|
||||
})
|
||||
return faceInfo
|
||||
}
|
||||
|
||||
export function getQuaternionFromZAxis(zAxis: Vector3): Quaternion {
|
||||
const dummyCam = new PerspectiveCamera()
|
||||
dummyCam.up.set(0, 0, 1)
|
||||
|
@ -3,13 +3,12 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
import { CommandArgument } from 'lib/commandTypes'
|
||||
import {
|
||||
ResolvedSelectionType,
|
||||
canSubmitSelectionArg,
|
||||
getSelectionType,
|
||||
getSelectionTypeDisplayText,
|
||||
} from 'lib/selections'
|
||||
import { modelingMachine } from 'machines/modelingMachine'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { StateFrom } from 'xstate'
|
||||
|
||||
@ -30,13 +29,13 @@ function CommandBarSelectionInput({
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
const [hasSubmitted, setHasSubmitted] = useState(false)
|
||||
const selection = useSelector(arg.machineActor, selectionSelector)
|
||||
const [selectionsByType, setSelectionsByType] = useState<
|
||||
'none' | ResolvedSelectionType[]
|
||||
>(
|
||||
selection.codeBasedSelections[0]?.range[1] === code.length
|
||||
const initSelectionsByType = useCallback(() => {
|
||||
const selectionRangeEnd = selection.codeBasedSelections[0]?.range[1]
|
||||
return !selectionRangeEnd || selectionRangeEnd === code.length
|
||||
? 'none'
|
||||
: getSelectionType(selection)
|
||||
)
|
||||
}, [selection, code])
|
||||
const selectionsByType = initSelectionsByType()
|
||||
const [canSubmitSelection, setCanSubmitSelection] = useState<boolean>(
|
||||
canSubmitSelectionArg(selectionsByType, arg)
|
||||
)
|
||||
@ -51,17 +50,14 @@ function CommandBarSelectionInput({
|
||||
inputRef.current?.focus()
|
||||
}, [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
|
||||
// and we have a valid selection already
|
||||
useEffect(() => {
|
||||
console.log('selection input effect', {
|
||||
selectionsByType,
|
||||
canSubmitSelection,
|
||||
arg,
|
||||
})
|
||||
setCanSubmitSelection(canSubmitSelectionArg(selectionsByType, arg))
|
||||
const argValue = commandBarState.context.argumentsToSubmit[arg.name]
|
||||
if (canSubmitSelection && arg.skip && argValue === undefined) {
|
||||
|
@ -15,10 +15,10 @@ import {
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { fileMachine } from 'machines/fileMachine'
|
||||
import { mkdir, remove, rename, create } from '@tauri-apps/plugin-fs'
|
||||
import { readProject } from 'lib/tauriFS'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { join, sep } from '@tauri-apps/api/path'
|
||||
import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants'
|
||||
import { getProjectInfo } from 'lib/tauri'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -62,7 +62,7 @@ export const FileMachineProvider = ({
|
||||
services: {
|
||||
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
|
||||
const newFiles = isTauri()
|
||||
? await readProject(context.project.path)
|
||||
? (await getProjectInfo(context.project.name)).children
|
||||
: []
|
||||
return {
|
||||
...context.project,
|
||||
|
@ -94,10 +94,7 @@ export function HelpMenu(props: React.PropsWithChildren) {
|
||||
if (isInProject) {
|
||||
navigate('onboarding')
|
||||
} else {
|
||||
createAndOpenNewProject(
|
||||
settings.context.app.projectDirectory.current,
|
||||
navigate
|
||||
)
|
||||
createAndOpenNewProject(navigate)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
@ -54,10 +54,9 @@ import { exportFromEngine } from 'lib/exportFromEngine'
|
||||
import { Models } from '@kittycad/lib/dist/types/src'
|
||||
import toast from 'react-hot-toast'
|
||||
import { EditorSelection } from '@uiw/react-codemirror'
|
||||
import { Vector3 } from 'three'
|
||||
import { quaternionFromUpNForward } from 'clientSideScene/helpers'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -78,7 +77,7 @@ export const ModelingMachineProvider = ({
|
||||
auth,
|
||||
settings: {
|
||||
context: {
|
||||
app: { theme },
|
||||
app: { theme, enableSSAO },
|
||||
modeling: { defaultUnit, highlightEdges },
|
||||
},
|
||||
},
|
||||
@ -88,6 +87,7 @@ export const ModelingMachineProvider = ({
|
||||
useSetupEngineManager(streamRef, token, {
|
||||
theme: theme.current,
|
||||
highlightEdges: highlightEdges.current,
|
||||
enableSSAO: enableSSAO.current,
|
||||
})
|
||||
const { htmlRef } = useStore((s) => ({
|
||||
htmlRef: s.htmlRef,
|
||||
@ -268,10 +268,12 @@ export const ModelingMachineProvider = ({
|
||||
'has valid extrude selection': ({ selectionRanges }) => {
|
||||
// A user can begin extruding if they either have 1+ faces selected or nothing selected
|
||||
// TODO: I believe this guard only allows for extruding a single face at a time
|
||||
if (selectionRanges.codeBasedSelections.length < 1) return false
|
||||
const isPipe = isSketchPipe(selectionRanges)
|
||||
|
||||
if (isSelectionLastLine(selectionRanges, codeManager.code))
|
||||
if (
|
||||
selectionRanges.codeBasedSelections.length === 0 ||
|
||||
isSelectionLastLine(selectionRanges, codeManager.code)
|
||||
)
|
||||
return true
|
||||
if (!isPipe) return false
|
||||
|
||||
@ -319,16 +321,9 @@ export const ModelingMachineProvider = ({
|
||||
)
|
||||
await kclManager.executeAstMock(modifiedAst)
|
||||
|
||||
const forward = new Vector3(...data.zAxis)
|
||||
const up = new Vector3(...data.yAxis)
|
||||
|
||||
let target = new Vector3(...data.position).multiplyScalar(
|
||||
sceneInfra._baseUnitMultiplier
|
||||
)
|
||||
const quaternion = quaternionFromUpNForward(up, forward)
|
||||
await sceneInfra.camControls.tweenCameraToQuaternion(
|
||||
quaternion,
|
||||
target
|
||||
await letEngineAnimateAndSyncCamAfter(
|
||||
engineCommandManager,
|
||||
data.faceId
|
||||
)
|
||||
|
||||
return {
|
||||
@ -343,6 +338,7 @@ export const ModelingMachineProvider = ({
|
||||
data.plane
|
||||
)
|
||||
await kclManager.updateAst(modifiedAst, false)
|
||||
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||||
const quat = await getSketchQuaternion(pathToNode, data.zAxis)
|
||||
await sceneInfra.camControls.tweenCameraToQuaternion(quat)
|
||||
return {
|
||||
@ -359,9 +355,9 @@ export const ModelingMachineProvider = ({
|
||||
sourceRange
|
||||
)
|
||||
const info = await getSketchOrientationDetails(sketchPathToNode || [])
|
||||
await sceneInfra.camControls.tweenCameraToQuaternion(
|
||||
info.quat,
|
||||
new Vector3(...info.sketchDetails.origin)
|
||||
await letEngineAnimateAndSyncCamAfter(
|
||||
engineCommandManager,
|
||||
info?.sketchDetails?.faceId || ''
|
||||
)
|
||||
return {
|
||||
sketchPathToNode: sketchPathToNode || [],
|
||||
|
@ -16,7 +16,6 @@ import {
|
||||
EditorView,
|
||||
dropCursor,
|
||||
drawSelection,
|
||||
ViewUpdate,
|
||||
} from '@codemirror/view'
|
||||
import {
|
||||
indentWithTab,
|
||||
@ -191,9 +190,6 @@ export const KclEditorPane = () => {
|
||||
return extensions
|
||||
}, [kclLSP, copilotLSP, textWrapping.current, cursorBlinking.current])
|
||||
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const updateDelay = 100
|
||||
|
||||
return (
|
||||
<div
|
||||
id="code-mirror-override"
|
||||
@ -206,17 +202,6 @@ export const KclEditorPane = () => {
|
||||
onCreateEditor={(_editorView) =>
|
||||
editorManager.setEditorView(_editorView)
|
||||
}
|
||||
onUpdate={(view: ViewUpdate) => {
|
||||
// debounce the view update.
|
||||
// otherwise it is laggy for typing.
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer)
|
||||
}
|
||||
|
||||
debounceTimer = setTimeout(() => {
|
||||
editorManager.handleOnViewUpdate(view)
|
||||
}, updateDelay)
|
||||
}}
|
||||
indentWithTab={false}
|
||||
basicSetup={false}
|
||||
/>
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { FormEvent, useEffect, useRef, useState } from 'react'
|
||||
import { type ProjectWithEntryPointMetadata } from 'lib/types'
|
||||
import { paths } from 'lib/paths'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ActionButton } from './ActionButton'
|
||||
@ -9,11 +8,11 @@ import {
|
||||
faTrashAlt,
|
||||
faX,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { getPartsCount, readProject } from '../lib/tauriFS'
|
||||
import { FILE_EXT } from 'lib/constants'
|
||||
import { Dialog } from '@headlessui/react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import Tooltip from './Tooltip'
|
||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||
|
||||
function ProjectCard({
|
||||
project,
|
||||
@ -21,17 +20,17 @@ function ProjectCard({
|
||||
handleDeleteProject,
|
||||
...props
|
||||
}: {
|
||||
project: ProjectWithEntryPointMetadata
|
||||
project: Project
|
||||
handleRenameProject: (
|
||||
e: FormEvent<HTMLFormElement>,
|
||||
f: ProjectWithEntryPointMetadata
|
||||
f: Project
|
||||
) => Promise<void>
|
||||
handleDeleteProject: (f: ProjectWithEntryPointMetadata) => Promise<void>
|
||||
handleDeleteProject: (f: Project) => Promise<void>
|
||||
}) {
|
||||
useHotkeys('esc', () => setIsEditing(false))
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
||||
const [numberOfParts, setNumberOfParts] = useState(1)
|
||||
const [numberOfFiles, setNumberOfFiles] = useState(1)
|
||||
const [numberOfFolders, setNumberOfFolders] = useState(0)
|
||||
|
||||
let inputRef = useRef<HTMLInputElement>(null)
|
||||
@ -41,7 +40,8 @@ function ProjectCard({
|
||||
void handleRenameProject(e, project).then(() => setIsEditing(false))
|
||||
}
|
||||
|
||||
function getDisplayedTime(date: Date) {
|
||||
function getDisplayedTime(dateStr: string) {
|
||||
const date = new Date(dateStr)
|
||||
const startOfToday = new Date()
|
||||
startOfToday.setHours(0, 0, 0, 0)
|
||||
return date.getTime() < startOfToday.getTime()
|
||||
@ -50,15 +50,12 @@ function ProjectCard({
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
async function getNumberOfParts() {
|
||||
const { kclFileCount, kclDirCount } = getPartsCount(
|
||||
await readProject(project.path)
|
||||
)
|
||||
setNumberOfParts(kclFileCount)
|
||||
setNumberOfFolders(kclDirCount)
|
||||
async function getNumberOfFiles() {
|
||||
setNumberOfFiles(project.kcl_file_count)
|
||||
setNumberOfFolders(project.directory_count)
|
||||
}
|
||||
void getNumberOfParts()
|
||||
}, [project.path])
|
||||
void getNumberOfFiles()
|
||||
}, [project.kcl_file_count, project.directory_count])
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
@ -129,7 +126,7 @@ function ProjectCard({
|
||||
{project.name?.replace(FILE_EXT, '')}
|
||||
</Link>
|
||||
<span className="text-chalkboard-60 text-xs">
|
||||
{numberOfParts} part{numberOfParts === 1 ? '' : 's'}{' '}
|
||||
{numberOfFiles} file{numberOfFiles === 1 ? '' : 's'}{' '}
|
||||
{numberOfFolders > 0 &&
|
||||
`/ ${numberOfFolders} folder${
|
||||
numberOfFolders === 1 ? '' : 's'
|
||||
@ -137,8 +134,8 @@ function ProjectCard({
|
||||
</span>
|
||||
<span className="text-chalkboard-60 text-xs">
|
||||
Edited{' '}
|
||||
{project.entrypointMetadata.mtime
|
||||
? getDisplayedTime(project.entrypointMetadata.mtime)
|
||||
{project.metadata && project.metadata?.modified
|
||||
? getDisplayedTime(project.metadata.modified)
|
||||
: 'never'}
|
||||
</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">
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||
import { type ProjectWithEntryPointMetadata } from 'lib/types'
|
||||
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
||||
import { APP_NAME } from 'lib/constants'
|
||||
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
|
||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||
|
||||
const now = new Date()
|
||||
const projectWellFormed = {
|
||||
@ -14,29 +14,17 @@ const projectWellFormed = {
|
||||
{
|
||||
name: 'main.kcl',
|
||||
path: '/some/path/Simple Box/main.kcl',
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
entrypointMetadata: {
|
||||
atime: now,
|
||||
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,
|
||||
metadata: {
|
||||
created: now.toISOString(),
|
||||
modified: now.toISOString(),
|
||||
size: 32,
|
||||
uid: 1,
|
||||
fileAttributes: null,
|
||||
},
|
||||
} satisfies ProjectWithEntryPointMetadata
|
||||
kcl_file_count: 1,
|
||||
directory_count: 0,
|
||||
} satisfies Project
|
||||
|
||||
describe('ProjectSidebarMenu tests', () => {
|
||||
test('Renders the project name', () => {
|
||||
|
@ -133,13 +133,13 @@ function ProjectMenuPopover({
|
||||
<p className="m-0 text-mono" data-testid="projectName">
|
||||
{project?.name ? project.name : APP_NAME}
|
||||
</p>
|
||||
{project?.entrypointMetadata && (
|
||||
{project?.metadata && project.metadata.created && (
|
||||
<p
|
||||
className="m-0 text-xs text-chalkboard-80 dark:text-chalkboard-40"
|
||||
data-testid="createdAt"
|
||||
>
|
||||
Created{' '}
|
||||
{project.entrypointMetadata.birthtime?.toLocaleDateString()}
|
||||
{new Date(project.metadata.created).toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
@ -7,7 +7,12 @@ import React, { createContext, useEffect } from 'react'
|
||||
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
||||
import { settingsMachine } from 'machines/settingsMachine'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { getThemeColorForEngine, setThemeClass, Themes } from 'lib/theme'
|
||||
import {
|
||||
getThemeColorForEngine,
|
||||
getOppositeTheme,
|
||||
setThemeClass,
|
||||
Themes,
|
||||
} from 'lib/theme'
|
||||
import decamelize from 'decamelize'
|
||||
import {
|
||||
AnyStateMachine,
|
||||
@ -99,6 +104,9 @@ export const SettingsAuthProviderBase = ({
|
||||
{
|
||||
context: loadedSettings,
|
||||
actions: {
|
||||
//TODO: batch all these and if that's difficult to do from tsx,
|
||||
// make it easy to do
|
||||
|
||||
setClientSideSceneUnits: (context, event) => {
|
||||
const newBaseUnit =
|
||||
event.type === 'set.modeling.defaultUnit'
|
||||
@ -115,6 +123,16 @@ export const SettingsAuthProviderBase = ({
|
||||
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) => {
|
||||
engineCommandManager.sendSceneCommand({
|
||||
@ -150,7 +168,7 @@ export const SettingsAuthProviderBase = ({
|
||||
},
|
||||
'Execute AST': () => kclManager.executeCode(true),
|
||||
persistSettings: (context) =>
|
||||
saveSettings(context, loadedProject?.project?.path),
|
||||
saveSettings(context, loadedProject?.project?.name),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
@ -39,6 +39,8 @@ const CompletionItemKindMap = Object.fromEntries(
|
||||
) as Record<CompletionItemKind, string>
|
||||
|
||||
const changesDelay = 600
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const updateDelay = 100
|
||||
|
||||
export class LanguageServerPlugin implements PluginValue {
|
||||
public client: LanguageServerClient
|
||||
@ -47,6 +49,7 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
public workspaceFolders: LSP.WorkspaceFolder[]
|
||||
private documentVersion: number
|
||||
private foldingRanges: LSP.FoldingRange[] | null = null
|
||||
private viewUpdate: ViewUpdate | null = null
|
||||
private _defferer = deferExecution((code: string) => {
|
||||
try {
|
||||
// Update the state (not the editor) with the new code.
|
||||
@ -57,8 +60,9 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
},
|
||||
contentChanges: [{ text: code }],
|
||||
})
|
||||
if (editorManager.editorView) {
|
||||
//editorManager.handleOnViewUpdate(editorManager.editorView)
|
||||
|
||||
if (this.viewUpdate) {
|
||||
editorManager.handleOnViewUpdate(this.viewUpdate)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
@ -83,14 +87,27 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
})
|
||||
}
|
||||
|
||||
update({ docChanged }: ViewUpdate) {
|
||||
if (!docChanged) return
|
||||
update(viewUpdate: ViewUpdate) {
|
||||
this.viewUpdate = viewUpdate
|
||||
if (!viewUpdate.docChanged) {
|
||||
// debounce the view update.
|
||||
// otherwise it is laggy for typing.
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer)
|
||||
}
|
||||
|
||||
debounceTimer = setTimeout(() => {
|
||||
editorManager.handleOnViewUpdate(viewUpdate)
|
||||
}, updateDelay)
|
||||
return
|
||||
}
|
||||
|
||||
const newCode = this.view.state.doc.toString()
|
||||
|
||||
codeManager.code = newCode
|
||||
codeManager.writeToFile()
|
||||
kclManager.executeCode()
|
||||
|
||||
this.sendChange({
|
||||
documentText: newCode,
|
||||
})
|
||||
|
@ -11,9 +11,11 @@ export function useSetupEngineManager(
|
||||
settings = {
|
||||
theme: Themes.System,
|
||||
highlightEdges: true,
|
||||
enableSSAO: true,
|
||||
} as {
|
||||
theme: Themes
|
||||
highlightEdges: boolean
|
||||
enableSSAO: boolean
|
||||
}
|
||||
) {
|
||||
const {
|
||||
|
@ -347,6 +347,16 @@ export class KclManager {
|
||||
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true)
|
||||
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, true)
|
||||
}
|
||||
enterEditMode() {
|
||||
enterEditMode(this.programMemory, this.engineCommandManager)
|
||||
}
|
||||
exitEditMode() {
|
||||
this.engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'edit_mode_exit' },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function enterEditMode(
|
||||
|
@ -266,9 +266,9 @@ const mySk1 = startSketchAt([0, 0])
|
||||
|
||||
|
||||
|> rx(45, %)
|
||||
/*
|
||||
one more for good measure
|
||||
*/
|
||||
/*
|
||||
one more for good measure
|
||||
*/
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
@ -285,7 +285,7 @@ const mySk1 = startSketchAt([0, 0])
|
||||
// and another with just white space between others below
|
||||
|> ry(45, %)
|
||||
|> rx(45, %)
|
||||
/* one more for good measure */
|
||||
/* one more for good measure */
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
@ -4,7 +4,7 @@ import { Models } from '@kittycad/lib'
|
||||
import { exportSave } from 'lib/exportSave'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { Themes, getThemeColorForEngine } from 'lib/theme'
|
||||
import { Themes, getThemeColorForEngine, getOppositeTheme } from 'lib/theme'
|
||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
|
||||
let lastMessage = ''
|
||||
@ -941,6 +941,7 @@ export class EngineCommandManager {
|
||||
settings = {
|
||||
theme: Themes.Dark,
|
||||
highlightEdges: true,
|
||||
enableSSAO: true,
|
||||
},
|
||||
}: {
|
||||
setMediaStream: (stream: MediaStream) => void
|
||||
@ -953,6 +954,7 @@ export class EngineCommandManager {
|
||||
settings?: {
|
||||
theme: Themes
|
||||
highlightEdges: boolean
|
||||
enableSSAO: boolean
|
||||
}
|
||||
}) {
|
||||
this.makeDefaultPlanes = makeDefaultPlanes
|
||||
@ -969,7 +971,8 @@ export class EngineCommandManager {
|
||||
return
|
||||
}
|
||||
|
||||
const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}`
|
||||
const additionalSettings = settings.enableSSAO ? '&post_effect=ssao' : ''
|
||||
const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}${additionalSettings}`
|
||||
this.engineConnection = new EngineConnection({
|
||||
engineCommandManager: this,
|
||||
url,
|
||||
@ -989,6 +992,18 @@ export class EngineCommandManager {
|
||||
color: getThemeColorForEngine(settings.theme),
|
||||
},
|
||||
})
|
||||
|
||||
// Sets the default line colors
|
||||
const opposingTheme = getOppositeTheme(settings.theme)
|
||||
this.sendSceneCommand({
|
||||
cmd_id: uuidv4(),
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'set_default_system_properties',
|
||||
color: getThemeColorForEngine(opposingTheme),
|
||||
},
|
||||
})
|
||||
|
||||
// Set the edge lines visibility
|
||||
this.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
@ -1171,7 +1186,10 @@ export class EngineCommandManager {
|
||||
type: 'receive-reliable',
|
||||
data: message,
|
||||
id,
|
||||
cmd_type: command?.commandType || this.lastArtifactMap[id]?.commandType,
|
||||
cmd_type:
|
||||
command?.commandType ||
|
||||
this.lastArtifactMap[id]?.commandType ||
|
||||
sceneCommand?.commandType,
|
||||
})
|
||||
Object.values(this.subscriptions[modelingResponse.type] || {}).forEach(
|
||||
(callback) => callback(modelingResponse)
|
||||
@ -1323,6 +1341,17 @@ export class EngineCommandManager {
|
||||
this.lastArtifactMap = this.artifactMap
|
||||
this.artifactMap = {}
|
||||
await this.initPlanes()
|
||||
await this.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'make_axes_gizmo',
|
||||
clobber: false,
|
||||
// If true, axes gizmo will be placed in the corner of the screen.
|
||||
// If false, it will be placed at the origin of the scene.
|
||||
gizmo_mode: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
subscribeTo<T extends ModelTypes>({
|
||||
event,
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { readFile, exists as tauriExists } from '@tauri-apps/plugin-fs'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { join } from '@tauri-apps/api/path'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { FileEntry } from 'lib/types'
|
||||
import { readDirRecursive } from 'lib/tauri'
|
||||
|
||||
/// FileSystemManager is a class that provides a way to read files from the local file system.
|
||||
/// It assumes that you are in a project since it is solely used by the std lib
|
||||
@ -69,9 +68,7 @@ class FileSystemManager {
|
||||
throw new Error(`Error joining dir: ${error}`)
|
||||
})
|
||||
.then((p) => {
|
||||
invoke<FileEntry[]>('read_dir_recursive', {
|
||||
path: p,
|
||||
})
|
||||
readDirRecursive(p)
|
||||
.catch((error) => {
|
||||
throw new Error(`Error reading dir: ${error}`)
|
||||
})
|
||||
|
@ -102,7 +102,7 @@ describe('testing changeSketchArguments', () => {
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> ${line}
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
// |> rx(45, %)
|
||||
// |> rx(45, %)
|
||||
`
|
||||
const code = genCode(lineToChange)
|
||||
const expectedCode = genCode(lineAfterChange)
|
||||
|
@ -10,7 +10,10 @@ import init, {
|
||||
make_default_planes,
|
||||
coredump,
|
||||
toml_stringify,
|
||||
toml_parse,
|
||||
default_app_settings,
|
||||
parse_app_settings,
|
||||
parse_project_settings,
|
||||
default_project_settings,
|
||||
} from '../wasm-lib/pkg/wasm_lib'
|
||||
import { KCLError } from './errors'
|
||||
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
||||
@ -26,6 +29,8 @@ import { CoreDumpManager } from 'lib/coredump'
|
||||
import openWindow from 'lib/openWindow'
|
||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
import { TEST } from 'env'
|
||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
||||
|
||||
export type { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||
export type { Value } from '../wasm-lib/kcl/bindings/Value'
|
||||
@ -349,11 +354,38 @@ export function tomlStringify(toml: any): string {
|
||||
}
|
||||
}
|
||||
|
||||
export function tomlParse(toml: string): any {
|
||||
export function defaultAppSettings(): Configuration {
|
||||
try {
|
||||
const parsed: any = toml_parse(toml)
|
||||
return parsed
|
||||
const settings: Configuration = default_app_settings()
|
||||
return settings
|
||||
} catch (e: any) {
|
||||
throw new Error(`Error parsing toml: ${e}`)
|
||||
throw new Error(`Error getting default app settings: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function parseAppSettings(toml: string): Configuration {
|
||||
try {
|
||||
const settings: Configuration = parse_app_settings(toml)
|
||||
return settings
|
||||
} catch (e: any) {
|
||||
throw new Error(`Error parsing app settings: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function defaultProjectSettings(): ProjectConfiguration {
|
||||
try {
|
||||
const settings: ProjectConfiguration = default_project_settings()
|
||||
return settings
|
||||
} catch (e: any) {
|
||||
throw new Error(`Error getting default project settings: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function parseProjectSettings(toml: string): ProjectConfiguration {
|
||||
try {
|
||||
const settings: ProjectConfiguration = parse_project_settings(toml)
|
||||
return settings
|
||||
} catch (e: any) {
|
||||
throw new Error(`Error parsing project settings: ${e}`)
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { MouseControlType } from 'wasm-lib/kcl/bindings/MouseControlType'
|
||||
|
||||
const noModifiersPressed = (e: React.MouseEvent) =>
|
||||
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
|
||||
|
||||
@ -20,6 +22,29 @@ export const cameraSystems: CameraSystem[] = [
|
||||
'AutoCAD',
|
||||
]
|
||||
|
||||
export function mouseControlsToCameraSystem(
|
||||
mouseControl: MouseControlType | undefined
|
||||
): CameraSystem | undefined {
|
||||
switch (mouseControl) {
|
||||
case 'kitty_cad':
|
||||
return 'KittyCAD'
|
||||
case 'on_shape':
|
||||
return 'OnShape'
|
||||
case 'trackpad_friendly':
|
||||
return 'Trackpad Friendly'
|
||||
case 'solidworks':
|
||||
return 'Solidworks'
|
||||
case 'nx':
|
||||
return 'NX'
|
||||
case 'creo':
|
||||
return 'Creo'
|
||||
case 'auto_cad':
|
||||
return 'AutoCAD'
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
interface MouseGuardHandler {
|
||||
description: string
|
||||
callback: (e: React.MouseEvent) => boolean
|
||||
|
@ -8,8 +8,6 @@ export const MAX_PADDING = 7
|
||||
* This is available for users to edit as a setting.
|
||||
*/
|
||||
export const DEFAULT_PROJECT_NAME = 'project-$nnn'
|
||||
/** The file name for settings files, both at the user and project level */
|
||||
export const SETTINGS_FILE_EXT = '.toml'
|
||||
/** Name given the temporary "project" in the browser version of the app */
|
||||
export const BROWSER_PROJECT_NAME = 'browser'
|
||||
/** Name given the temporary file in the browser version of the app */
|
||||
|
@ -1,10 +1,5 @@
|
||||
import { ActionFunction, LoaderFunction, redirect } from 'react-router-dom'
|
||||
import {
|
||||
FileEntry,
|
||||
FileLoaderData,
|
||||
HomeLoaderData,
|
||||
IndexLoaderData,
|
||||
} from './types'
|
||||
import { FileLoaderData, HomeLoaderData, IndexLoaderData } from './types'
|
||||
import { isTauri } from './isTauri'
|
||||
import { getProjectMetaByRouteId, paths } from './paths'
|
||||
import { BROWSER_PATH } from 'lib/paths'
|
||||
@ -14,24 +9,24 @@ import {
|
||||
PROJECT_ENTRYPOINT,
|
||||
} from 'lib/constants'
|
||||
import { loadAndValidateSettings } from './settings/settingsUtils'
|
||||
import {
|
||||
getInitialDefaultDir,
|
||||
getProjectsInDir,
|
||||
initializeProjectDirectory,
|
||||
} from './tauriFS'
|
||||
import makeUrlPathRelative from './makeUrlPathRelative'
|
||||
import { join, sep } from '@tauri-apps/api/path'
|
||||
import { readTextFile, stat } from '@tauri-apps/plugin-fs'
|
||||
import { sep } from '@tauri-apps/api/path'
|
||||
import { readTextFile } from '@tauri-apps/plugin-fs'
|
||||
import { codeManager, kclManager } from 'lib/singletons'
|
||||
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import {
|
||||
getProjectInfo,
|
||||
initializeProjectDirectory,
|
||||
listProjects,
|
||||
} from './tauri'
|
||||
import { createSettings } from './settings/initialSettings'
|
||||
|
||||
// The root loader simply resolves the settings and any errors that
|
||||
// occurred during the settings load
|
||||
export const settingsLoader: LoaderFunction = async ({
|
||||
params,
|
||||
}): ReturnType<typeof loadAndValidateSettings> => {
|
||||
let settings = await loadAndValidateSettings()
|
||||
}): Promise<ReturnType<typeof createSettings>> => {
|
||||
let { settings } = await loadAndValidateSettings()
|
||||
|
||||
// I don't love that we have to read the settings again here,
|
||||
// but we need to get the project path to load the project settings
|
||||
@ -39,8 +34,9 @@ export const settingsLoader: LoaderFunction = async ({
|
||||
const defaultDir = settings.app.projectDirectory.current || ''
|
||||
const projectPathData = getProjectMetaByRouteId(params.id, defaultDir)
|
||||
if (projectPathData) {
|
||||
const { projectPath } = projectPathData
|
||||
settings = await loadAndValidateSettings(projectPath)
|
||||
const { projectName } = projectPathData
|
||||
const { settings: s } = await loadAndValidateSettings(projectName)
|
||||
settings = s
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,7 +45,7 @@ export const settingsLoader: LoaderFunction = async ({
|
||||
|
||||
// Redirect users to the appropriate onboarding page if they haven't completed it
|
||||
export const onboardingRedirectLoader: ActionFunction = async (args) => {
|
||||
const settings = await loadAndValidateSettings()
|
||||
const { settings } = await loadAndValidateSettings()
|
||||
const onboardingStatus = settings.app.onboardingStatus.current || ''
|
||||
const notEnRouteToOnboarding = !args.request.url.includes(
|
||||
paths.ONBOARDING.INDEX
|
||||
@ -73,7 +69,7 @@ export const onboardingRedirectLoader: ActionFunction = async (args) => {
|
||||
export const fileLoader: LoaderFunction = async ({
|
||||
params,
|
||||
}): Promise<FileLoaderData | Response> => {
|
||||
let settings = await loadAndValidateSettings()
|
||||
let { settings } = await loadAndValidateSettings()
|
||||
|
||||
const defaultDir = settings.app.projectDirectory.current || '/'
|
||||
const projectPathData = getProjectMetaByRouteId(params.id, defaultDir)
|
||||
@ -94,12 +90,7 @@ export const fileLoader: LoaderFunction = async ({
|
||||
// TODO: PROJECT_ENTRYPOINT is hardcoded
|
||||
// until we support setting a project's entrypoint file
|
||||
const code = await readTextFile(currentFilePath)
|
||||
const entrypointMetadata = await stat(
|
||||
await join(projectPath, PROJECT_ENTRYPOINT)
|
||||
)
|
||||
const children = await invoke<FileEntry[]>('read_dir_recursive', {
|
||||
path: projectPath,
|
||||
})
|
||||
|
||||
// Update both the state and the editor's code.
|
||||
// We explicitly do not write to the file here since we are loading from
|
||||
// the file system and not the editor.
|
||||
@ -113,15 +104,19 @@ export const fileLoader: LoaderFunction = async ({
|
||||
|
||||
const projectData: IndexLoaderData = {
|
||||
code,
|
||||
project: {
|
||||
name: projectName,
|
||||
path: projectPath,
|
||||
children,
|
||||
entrypointMetadata,
|
||||
},
|
||||
project: isTauri()
|
||||
? await getProjectInfo(projectName)
|
||||
: {
|
||||
name: projectName,
|
||||
path: projectPath,
|
||||
children: [],
|
||||
kcl_file_count: 0,
|
||||
directory_count: 0,
|
||||
},
|
||||
file: {
|
||||
name: currentFileName,
|
||||
path: currentFilePath,
|
||||
children: [],
|
||||
},
|
||||
}
|
||||
|
||||
@ -140,6 +135,7 @@ export const fileLoader: LoaderFunction = async ({
|
||||
file: {
|
||||
name: BROWSER_FILE_NAME,
|
||||
path: decodeURIComponent(BROWSER_PATH),
|
||||
children: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -152,14 +148,12 @@ export const homeLoader: LoaderFunction = async (): Promise<
|
||||
if (!isTauri()) {
|
||||
return redirect(paths.FILE + '/%2F' + BROWSER_PROJECT_NAME)
|
||||
}
|
||||
const settings = await loadAndValidateSettings()
|
||||
const { configuration } = await loadAndValidateSettings()
|
||||
|
||||
const projectDir = await initializeProjectDirectory(
|
||||
settings.app.projectDirectory.current || (await getInitialDefaultDir())
|
||||
)
|
||||
const projectDir = await initializeProjectDirectory(configuration)
|
||||
|
||||
if (projectDir.path) {
|
||||
const projects = await getProjectsInDir(projectDir.path)
|
||||
if (projectDir) {
|
||||
const projects = await listProjects(configuration)
|
||||
|
||||
return {
|
||||
projects,
|
||||
|
@ -420,7 +420,13 @@ export function getSelectionTypeDisplayText(
|
||||
const selectionsByType = getSelectionType(selection)
|
||||
|
||||
return (selectionsByType as Exclude<typeof selectionsByType, 'none'>)
|
||||
.map(([type, count]) => `${count} ${type}${count > 1 ? 's' : ''}`)
|
||||
.map(
|
||||
// Hack for showing "face" instead of "extrude-wall" in command bar text
|
||||
([type, count]) =>
|
||||
`${count} ${type.replace('extrude-wall', 'face')}${
|
||||
count > 1 ? 's' : ''
|
||||
}`
|
||||
)
|
||||
.join(', ')
|
||||
}
|
||||
|
||||
|
@ -156,6 +156,13 @@ export function createSettings() {
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
enableSSAO: new Setting<boolean>({
|
||||
defaultValue: true,
|
||||
description:
|
||||
'Whether or not Screen Space Ambient Occlusion (SSAO) is enabled',
|
||||
validate: (v) => typeof v === 'boolean',
|
||||
hideOnPlatform: 'both', //for now
|
||||
}),
|
||||
onboardingStatus: new Setting<string>({
|
||||
defaultValue: '',
|
||||
validate: (v) => typeof v === 'string',
|
||||
|
@ -1,100 +1,228 @@
|
||||
import {
|
||||
getInitialDefaultDir,
|
||||
getSettingsFilePaths,
|
||||
readSettingsFile,
|
||||
} from '../tauriFS'
|
||||
import { Setting, createSettings, settings } from 'lib/settings/initialSettings'
|
||||
import { SaveSettingsPayload, SettingsLevel } from './settingsTypes'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { remove, writeTextFile, exists } from '@tauri-apps/plugin-fs'
|
||||
import { initPromise, tomlParse, tomlStringify } from 'lang/wasm'
|
||||
import {
|
||||
defaultAppSettings,
|
||||
defaultProjectSettings,
|
||||
initPromise,
|
||||
parseAppSettings,
|
||||
parseProjectSettings,
|
||||
tomlStringify,
|
||||
} from 'lang/wasm'
|
||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||
import { mouseControlsToCameraSystem } from 'lib/cameraControls'
|
||||
import { appThemeToTheme } from 'lib/theme'
|
||||
import {
|
||||
readAppSettingsFile,
|
||||
readProjectSettingsFile,
|
||||
writeAppSettingsFile,
|
||||
writeProjectSettingsFile,
|
||||
} from 'lib/tauri'
|
||||
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
||||
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||
|
||||
/**
|
||||
* We expect the settings to be stored in a TOML file
|
||||
* or TOML-formatted string in localStorage
|
||||
* under a top-level [settings] key.
|
||||
* @param path
|
||||
* @returns
|
||||
*/
|
||||
function getSettingsFromStorage(path: string) {
|
||||
return isTauri()
|
||||
? readSettingsFile(path)
|
||||
: (tomlParse(localStorage.getItem(path) ?? '')
|
||||
.settings as Partial<SaveSettingsPayload>)
|
||||
* Convert from a rust settings struct into the JS settings struct.
|
||||
* We do this because the JS settings type has all the fancy shit
|
||||
* for hiding and showing settings.
|
||||
**/
|
||||
function configurationToSettingsPayload(
|
||||
configuration: Configuration
|
||||
): Partial<SaveSettingsPayload> {
|
||||
return {
|
||||
app: {
|
||||
theme: appThemeToTheme(configuration?.settings?.app?.appearance?.theme),
|
||||
themeColor: configuration?.settings?.app?.appearance?.color
|
||||
? configuration?.settings?.app?.appearance?.color.toString()
|
||||
: undefined,
|
||||
onboardingStatus: configuration?.settings?.app?.onboarding_status,
|
||||
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
|
||||
projectDirectory: configuration?.settings?.project?.directory,
|
||||
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
|
||||
},
|
||||
modeling: {
|
||||
defaultUnit: configuration?.settings?.modeling?.base_unit,
|
||||
mouseControls: mouseControlsToCameraSystem(
|
||||
configuration?.settings?.modeling?.mouse_controls
|
||||
),
|
||||
highlightEdges: configuration?.settings?.modeling?.highlight_edges,
|
||||
showDebugPanel: configuration?.settings?.modeling?.show_debug_panel,
|
||||
},
|
||||
textEditor: {
|
||||
textWrapping: configuration?.settings?.text_editor?.text_wrapping,
|
||||
blinkingCursor: configuration?.settings?.text_editor?.blinking_cursor,
|
||||
},
|
||||
projects: {
|
||||
defaultProjectName:
|
||||
configuration?.settings?.project?.default_project_name,
|
||||
},
|
||||
commandBar: {
|
||||
includeSettings: configuration?.settings?.command_bar?.include_settings,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadAndValidateSettings(projectPath?: string) {
|
||||
const settings = createSettings()
|
||||
settings.app.projectDirectory.default = await getInitialDefaultDir()
|
||||
// First, get the settings data at the user and project level
|
||||
const settingsFilePaths = await getSettingsFilePaths(projectPath)
|
||||
function projectConfigurationToSettingsPayload(
|
||||
configuration: ProjectConfiguration
|
||||
): Partial<SaveSettingsPayload> {
|
||||
return {
|
||||
app: {
|
||||
theme: appThemeToTheme(configuration?.settings?.app?.appearance?.theme),
|
||||
themeColor: configuration?.settings?.app?.appearance?.color
|
||||
? configuration?.settings?.app?.appearance?.color.toString()
|
||||
: undefined,
|
||||
onboardingStatus: configuration?.settings?.app?.onboarding_status,
|
||||
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
|
||||
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
|
||||
},
|
||||
modeling: {
|
||||
defaultUnit: configuration?.settings?.modeling?.base_unit,
|
||||
mouseControls: mouseControlsToCameraSystem(
|
||||
configuration?.settings?.modeling?.mouse_controls
|
||||
),
|
||||
highlightEdges: configuration?.settings?.modeling?.highlight_edges,
|
||||
showDebugPanel: configuration?.settings?.modeling?.show_debug_panel,
|
||||
},
|
||||
textEditor: {
|
||||
textWrapping: configuration?.settings?.text_editor?.text_wrapping,
|
||||
blinkingCursor: configuration?.settings?.text_editor?.blinking_cursor,
|
||||
},
|
||||
commandBar: {
|
||||
includeSettings: configuration?.settings?.command_bar?.include_settings,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Load the settings from the files
|
||||
if (settingsFilePaths.user) {
|
||||
await initPromise
|
||||
const userSettings = await getSettingsFromStorage(settingsFilePaths.user)
|
||||
if (userSettings) {
|
||||
setSettingsAtLevel(settings, 'user', userSettings)
|
||||
}
|
||||
function localStorageAppSettingsPath() {
|
||||
return '/settings.toml'
|
||||
}
|
||||
|
||||
function localStorageProjectSettingsPath() {
|
||||
return '/' + BROWSER_PROJECT_NAME + '/project.toml'
|
||||
}
|
||||
|
||||
function readLocalStorageAppSettingsFile(): Configuration {
|
||||
// TODO: Remove backwards compatibility after a few releases.
|
||||
let stored =
|
||||
localStorage.getItem(localStorageAppSettingsPath()) ??
|
||||
localStorage.getItem('/user.toml') ??
|
||||
''
|
||||
|
||||
if (stored === '') {
|
||||
return defaultAppSettings()
|
||||
}
|
||||
|
||||
// Load the project settings if they exist
|
||||
if (settingsFilePaths.project) {
|
||||
const projectSettings = await getSettingsFromStorage(
|
||||
settingsFilePaths.project
|
||||
try {
|
||||
return parseAppSettings(stored)
|
||||
} catch (e) {
|
||||
const settings = defaultAppSettings()
|
||||
localStorage.setItem(localStorageAppSettingsPath(), tomlStringify(settings))
|
||||
return settings
|
||||
}
|
||||
}
|
||||
|
||||
function readLocalStorageProjectSettingsFile(): ProjectConfiguration {
|
||||
// TODO: Remove backwards compatibility after a few releases.
|
||||
let stored = localStorage.getItem(localStorageProjectSettingsPath()) ?? ''
|
||||
|
||||
if (stored === '') {
|
||||
return defaultProjectSettings()
|
||||
}
|
||||
|
||||
try {
|
||||
return parseProjectSettings(stored)
|
||||
} catch (e) {
|
||||
const settings = defaultProjectSettings()
|
||||
localStorage.setItem(
|
||||
localStorageProjectSettingsPath(),
|
||||
tomlStringify(settings)
|
||||
)
|
||||
if (projectSettings) {
|
||||
setSettingsAtLevel(settings, 'project', projectSettings)
|
||||
}
|
||||
return settings
|
||||
}
|
||||
}
|
||||
|
||||
export interface AppSettings {
|
||||
settings: ReturnType<typeof createSettings>
|
||||
configuration: Configuration
|
||||
}
|
||||
|
||||
export async function loadAndValidateSettings(
|
||||
projectName?: string
|
||||
): Promise<AppSettings> {
|
||||
const settings = createSettings()
|
||||
const inTauri = isTauri()
|
||||
|
||||
if (!inTauri) {
|
||||
// Make sure we have wasm initialized.
|
||||
await initPromise
|
||||
}
|
||||
|
||||
// Load the app settings from the file system or localStorage.
|
||||
const appSettings = inTauri
|
||||
? await readAppSettingsFile()
|
||||
: readLocalStorageAppSettingsFile()
|
||||
// Convert the app settings to the JS settings format.
|
||||
const appSettingsPayload = configurationToSettingsPayload(appSettings)
|
||||
setSettingsAtLevel(settings, 'user', appSettingsPayload)
|
||||
|
||||
// Load the project settings if they exist
|
||||
if (projectName) {
|
||||
const projectSettings = inTauri
|
||||
? await readProjectSettingsFile(appSettings, projectName)
|
||||
: readLocalStorageProjectSettingsFile()
|
||||
|
||||
const projectSettingsPayload =
|
||||
projectConfigurationToSettingsPayload(projectSettings)
|
||||
setSettingsAtLevel(settings, 'project', projectSettingsPayload)
|
||||
}
|
||||
|
||||
// Return the settings object
|
||||
return settings
|
||||
return { settings, configuration: appSettings }
|
||||
}
|
||||
|
||||
export async function saveSettings(
|
||||
allSettings: typeof settings,
|
||||
projectPath?: string
|
||||
) {
|
||||
const settingsFilePaths = await getSettingsFilePaths(projectPath)
|
||||
|
||||
if (settingsFilePaths.user) {
|
||||
const changedSettings = getChangedSettingsAtLevel(allSettings, 'user')
|
||||
|
||||
await writeOrClearPersistedSettings(settingsFilePaths.user, changedSettings)
|
||||
}
|
||||
|
||||
if (settingsFilePaths.project) {
|
||||
const changedSettings = getChangedSettingsAtLevel(allSettings, 'project')
|
||||
|
||||
await writeOrClearPersistedSettings(
|
||||
settingsFilePaths.project,
|
||||
changedSettings
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function writeOrClearPersistedSettings(
|
||||
settingsFilePath: string,
|
||||
changedSettings: Partial<SaveSettingsPayload>
|
||||
projectName?: string
|
||||
) {
|
||||
// Make sure we have wasm initialized.
|
||||
await initPromise
|
||||
if (changedSettings && Object.keys(changedSettings).length) {
|
||||
if (isTauri()) {
|
||||
await writeTextFile(
|
||||
settingsFilePath,
|
||||
tomlStringify({ settings: changedSettings })
|
||||
)
|
||||
}
|
||||
localStorage.setItem(
|
||||
settingsFilePath,
|
||||
tomlStringify({ settings: changedSettings })
|
||||
)
|
||||
const inTauri = isTauri()
|
||||
|
||||
// Get the user settings.
|
||||
const jsAppSettings = getChangedSettingsAtLevel(allSettings, 'user')
|
||||
const tomlString = tomlStringify({ settings: jsAppSettings })
|
||||
// Parse this as a Configuration.
|
||||
const appSettings = parseAppSettings(tomlString)
|
||||
|
||||
// Write the app settings.
|
||||
if (inTauri) {
|
||||
await writeAppSettingsFile(appSettings)
|
||||
} else {
|
||||
if (isTauri() && (await exists(settingsFilePath))) {
|
||||
await remove(settingsFilePath)
|
||||
}
|
||||
localStorage.removeItem(settingsFilePath)
|
||||
localStorage.setItem(
|
||||
localStorageAppSettingsPath(),
|
||||
tomlStringify(appSettings)
|
||||
)
|
||||
}
|
||||
|
||||
if (!projectName) {
|
||||
// If we're not saving project settings, we're done.
|
||||
return
|
||||
}
|
||||
|
||||
// Get the project settings.
|
||||
const jsProjectSettings = getChangedSettingsAtLevel(allSettings, 'project')
|
||||
const projectTomlString = tomlStringify({ settings: jsProjectSettings })
|
||||
// Parse this as a Configuration.
|
||||
const projectSettings = parseProjectSettings(projectTomlString)
|
||||
|
||||
// Write the project settings.
|
||||
if (inTauri) {
|
||||
await writeProjectSettingsFile(appSettings, projectName, projectSettings)
|
||||
} else {
|
||||
localStorage.setItem(
|
||||
localStorageProjectSettingsPath(),
|
||||
tomlStringify(projectSettings)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,3 +20,15 @@ export const sceneEntitiesManager = new SceneEntities(engineCommandManager)
|
||||
|
||||
// This needs to be after sceneInfra and engineCommandManager are is created.
|
||||
export const editorManager = new EditorManager()
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
;(window as any).engineCommandManager = engineCommandManager
|
||||
;(window as any).kclManager = kclManager
|
||||
;(window as any).sceneInfra = sceneInfra
|
||||
;(window as any).sceneEntitiesManager = sceneEntitiesManager
|
||||
;(window as any).editorManager = editorManager
|
||||
;(window as any).enableMousePositionLogs = () =>
|
||||
document.addEventListener('mousemove', (e) =>
|
||||
console.log(`await page.mouse.click(${e.clientX}, ${e.clientY})`)
|
||||
)
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import {
|
||||
faArrowUp,
|
||||
faCircle,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { type ProjectWithEntryPointMetadata } from 'lib/types'
|
||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||
|
||||
const DESC = ':desc'
|
||||
|
||||
@ -27,10 +27,7 @@ export function getNextSearchParams(currentSort: string, newSort: string) {
|
||||
}
|
||||
|
||||
export function getSortFunction(sortBy: string) {
|
||||
const sortByName = (
|
||||
a: ProjectWithEntryPointMetadata,
|
||||
b: ProjectWithEntryPointMetadata
|
||||
) => {
|
||||
const sortByName = (a: Project, b: Project) => {
|
||||
if (a.name && b.name) {
|
||||
return sortBy.includes('desc')
|
||||
? a.name.localeCompare(b.name)
|
||||
@ -39,16 +36,13 @@ export function getSortFunction(sortBy: string) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const sortByModified = (
|
||||
a: ProjectWithEntryPointMetadata,
|
||||
b: ProjectWithEntryPointMetadata
|
||||
) => {
|
||||
if (a.entrypointMetadata?.mtime && b.entrypointMetadata?.mtime) {
|
||||
const sortByModified = (a: Project, b: Project) => {
|
||||
if (a.metadata?.modified && b.metadata?.modified) {
|
||||
const aDate = new Date(a.metadata.modified)
|
||||
const bDate = new Date(b.metadata.modified)
|
||||
return !sortBy || sortBy.includes('desc')
|
||||
? b.entrypointMetadata.mtime.getTime() -
|
||||
a.entrypointMetadata.mtime.getTime()
|
||||
: a.entrypointMetadata.mtime.getTime() -
|
||||
b.entrypointMetadata.mtime.getTime()
|
||||
? bDate.getTime() - aDate.getTime()
|
||||
: aDate.getTime() - bDate.getTime()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|