Compare commits

..

1 Commits

Author SHA1 Message Date
2458c9f6fe Test a parser bug 2024-03-27 16:05:51 -05:00
378 changed files with 4534 additions and 8288 deletions

View File

@ -1,3 +1,3 @@
[codespell]
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,./src-tauri/gen/schemas
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md

View File

@ -7,23 +7,23 @@ on:
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- .github/workflows/cargo-bench.yml
- .github/workflows/cargo-criterion.yml
pull_request:
paths:
- '**.rs'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- .github/workflows/cargo-bench.yml
- .github/workflows/cargo-criterion.yml
workflow_dispatch:
permissions: read-all
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
name: cargo bench
name: cargo criterion
jobs:
cargo-bench:
name: Benchmark with iai
cargocriterion:
name: cargo criterion
runs-on: ubuntu-latest-8-cores
steps:
- uses: actions/checkout@v4
@ -31,12 +31,10 @@ jobs:
- name: Install dependencies
run: |
cargo install cargo-criterion
sudo apt update
sudo apt install -y valgrind
- name: Rust Cache
uses: Swatinem/rust-cache@v2.6.1
- name: Benchmark kcl library
shell: bash
run: |-
cd src/wasm-lib/kcl; cargo bench -- iai
cd src/wasm-lib/kcl; cargo criterion

View File

@ -125,9 +125,6 @@ jobs:
fail-fast: false
matrix:
os: [macos-14, ubuntu-latest, windows-latest]
env:
TAURI_ARGS_MACOS: ${{ matrix.os == 'macos-14' && '--target universal-apple-darwin' || '' }}
TAURI_ARGS_UBUNTU: ${{ matrix.os == 'ubuntu-latest' && '--bundles' || '' }}
steps:
- uses: actions/checkout@v4
@ -147,12 +144,10 @@ jobs:
sudo apt-get update &&
sudo apt-get install -y
libgtk-3-dev
libayatana-appindicator3-dev
libgtksourceview-3.0-dev
webkit2gtk-4.0
libappindicator3-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
@ -166,9 +161,7 @@ jobs:
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
# TODO: re-enable for Windows builds, see https://github.com/tauri-apps/tauri/issues/9045
- name: Setup Rust cache
if: matrix.os != 'windows-latest'
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'
@ -231,14 +224,14 @@ jobs:
with:
includeRelease: false
includeDebug: true
args: "${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
args: ${{ matrix.os == 'macos-14' && '--target universal-apple-darwin' || '' }}
- name: Build the app (release) and sign
uses: tauri-apps/tauri-action@v0
if: ${{ env.BUILD_RELEASE == 'true' }}
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
@ -247,7 +240,7 @@ jobs:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
TAURI_CONF_ARGS: "--config ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}"
with:
args: "${{ env.TAURI_CONF_ARGS }} ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
args: "${{ matrix.os == 'macos-14' && '--target universal-apple-darwin' || '' }} ${{ env.TAURI_CONF_ARGS }}"
- uses: actions/upload-artifact@v3
if: matrix.os != 'ubuntu-latest'
@ -257,11 +250,10 @@ jobs:
with:
path: "${{ env.PREFIX }}/${{ env.MODE }}/bundle/*/*"
# TODO: re-enable linux e2e tests when possible
- name: Run e2e tests (linux only)
if: false
if: matrix.os == 'ubuntu-latest'
run: |
cargo install tauri-driver
cargo install tauri-driver@0.1.3
source .env.${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
export VITE_KC_API_BASE_URL
xvfb-run yarn test:e2e:tauri

1
.gitignore vendored
View File

@ -51,6 +51,5 @@ e2e/playwright/export-snapshots/*
## generated files
src/**/*.typegen.ts
src-tauri/gen
src/wasm-lib/grackle/stdlib_cube_partial.json

View File

@ -281,7 +281,7 @@ https://github.com/KittyCAD/modeling-app/assets/29681384/6f5e8e85-1003-4fd9-be7f
<details>
<summary>
PS: for the debug panel, the following JSON is useful for snapping the camera
Ps for the debug panel, the following JSON is useful for snapping the camera
</summary>
```JSON

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -51191,7 +51191,6 @@
"const part001 = startSketchOn('XY')\n |> startProfileAt([4, 12], %)\n |> line([2, 0], %)\n |> line([0, -6], %)\n |> line([4, -6], %)\n |> line([0, -6], %)\n |> line([-3.75, -4.5], %)\n |> line([0, -5.5], %)\n |> line([-2, 0], %)\n |> close(%)\n |> revolve({ axis: 'y' }, %) // default angle is 360",
"// A donut shape.\nconst sketch001 = startSketchOn('XY')\n |> circle([15, 0], 5, %)\n |> revolve({ angle: 360, axis: 'y' }, %)",
"const part001 = startSketchOn('XY')\n |> startProfileAt([4, 12], %)\n |> line([2, 0], %)\n |> line([0, -6], %)\n |> line([4, -6], %)\n |> line([0, -6], %)\n |> line([-3.75, -4.5], %)\n |> line([0, -5.5], %)\n |> line([-2, 0], %)\n |> close(%)\n |> revolve({ axis: 'y', angle: 180 }, %)",
"const part001 = startSketchOn('XY')\n |> startProfileAt([4, 12], %)\n |> line([2, 0], %)\n |> line([0, -6], %)\n |> line([4, -6], %)\n |> line([0, -6], %)\n |> line([-3.75, -4.5], %)\n |> line([0, -5.5], %)\n |> line([-2, 0], %)\n |> close(%)\n |> revolve({ axis: 'y', angle: 180 }, %)\nconst part002 = startSketchOn(part001, 'end')\n |> startProfileAt([4.5, -5], %)\n |> line([0, 5], %)\n |> line([5, 0], %)\n |> line([0, -5], %)\n |> close(%)\n |> extrude(5, %)",
"const box = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([0, 20], %)\n |> line([20, 0], %)\n |> line([0, -20], %)\n |> close(%)\n |> extrude(20, %)\n\nconst sketch001 = startSketchOn(box, \"END\")\n |> circle([10, 10], 4, %)\n |> revolve({ angle: -90, axis: 'y' }, %)",
"const box = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([0, 20], %)\n |> line([20, 0], %)\n |> line([0, -20], %, 'revolveAxis')\n |> close(%)\n |> extrude(20, %)\n\nconst sketch001 = startSketchOn(box, \"END\")\n |> circle([10, 10], 4, %)\n |> revolve({\n angle: 90,\n axis: getOppositeEdge('revolveAxis', box)\n }, %)"
]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 163 KiB

View File

@ -1,11 +1,10 @@
import { test, expect } from '@playwright/test'
import { secrets } from './secrets'
import { getUtils } from './test-utils'
import waitOn from 'wait-on'
import { Themes } from '../../src/lib/theme'
import { initialSettings } from '../../src/lib/settings/initialSettings'
import { roundOff } from 'lib/utils'
import { basicStorageState } from './storageStates'
import * as TOML from '@iarna/toml'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { Themes } from 'lib/theme'
/*
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
@ -31,14 +30,31 @@ test.beforeEach(async ({ context, page }) => {
resources: ['tcp:3000'],
timeout: 5000,
})
await context.addInitScript(async (token) => {
localStorage.setItem('TOKEN_PERSIST_KEY', token)
localStorage.setItem('persistCode', ``)
localStorage.setItem(
'SETTINGS_PERSIST_KEY',
JSON.stringify({
baseUnit: 'in',
cameraControls: 'KittyCAD',
defaultDirectory: '',
defaultProjectName: 'project-$nnn',
onboardingStatus: 'dismissed',
showDebugPanel: true,
textWrapping: 'On',
theme: 'system',
unitSystem: 'imperial',
})
)
}, secrets.token)
// kill animations, speeds up tests and reduced flakiness
await page.emulateMedia({ reducedMotion: 'reduce' })
})
test.setTimeout(60000)
test('Basic sketch', async ({ page, context }) => {
test('Basic sketch', async ({ page }) => {
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
@ -513,133 +529,96 @@ test('Auto complete works', async ({ page }) => {
})
// Stored settings validation test
test.describe('Settings persistence and validation tests', () => {
// Override test setup
test('Stored settings are validated and fall back to defaults', async ({
page,
context,
}) => {
// Override beforeEach test setup
// with corrupted settings
const storageState = structuredClone(basicStorageState)
const s = TOML.parse(storageState.origins[0].localStorage[2].value) as {
settings: SaveSettingsPayload
}
s.settings.app.theme = Themes.Dark
s.settings.app.projectDirectory = 123 as any
s.settings.modeling.defaultUnit = 'invalid' as any
s.settings.modeling.mouseControls = `() => alert('hack the planet')` as any
s.settings.projects.defaultProjectName = false as any
storageState.origins[0].localStorage[2].value = TOML.stringify(s)
await context.addInitScript(async () => {
const storedSettings = JSON.parse(
localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}'
)
test.use({ storageState })
// Corrupt the settings
storedSettings.baseUnit = 'invalid'
storedSettings.cameraControls = `() => alert('hack the planet')`
storedSettings.defaultDirectory = 123
storedSettings.defaultProjectName = false
test('Stored settings are validated and fall back to defaults', async ({
page,
}) => {
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
// Check the settings were reset
const storedSettings = TOML.parse(
await page.evaluate(() => localStorage.getItem('/user.toml') || '{}')
) as { settings: SaveSettingsPayload }
expect(storedSettings.settings.app?.theme).toBe('dark')
// 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)
localStorage.setItem('SETTINGS_PERSIST_KEY', JSON.stringify(storedSettings))
})
test('Project settings can be set and override user settings', async ({
page,
}) => {
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/', { waitUntil: 'domcontentloaded' })
// Open the settings modal with the browser keyboard shortcut
await page.keyboard.press('Meta+Shift+,')
// Check the toast appeared
await expect(
page.getByText(`Error validating persisted settings:`, {
exact: false,
})
).toBeVisible()
await expect(
page.getByRole('heading', { name: 'Settings', exact: true })
).toBeVisible()
await page
.locator('select[name="app-theme"]')
.selectOption({ value: 'light' })
// Verify the toast appeared
await expect(
page.getByText(`Set theme to "light" for this project`)
).toBeVisible()
// Check that the theme changed
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
// Check that the user setting was not changed
await page.getByRole('radio', { name: 'User' }).click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('dark')
// Roll back to default "system" theme
await page
.getByText(
'themeRoll back themeRoll back to match defaultThe overall appearance of the appl'
)
.hover()
await page
.getByRole('button', {
name: 'Roll back theme ; Has tooltip: Roll back to match default',
})
.click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('system')
// Check that the project setting did not change
await page.getByRole('radio', { name: 'Project' }).click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('light')
})
// Check the settings were reset
const storedSettings = JSON.parse(
await page.evaluate(
() => localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}'
)
)
await expect(storedSettings.baseUnit).toBe(initialSettings.baseUnit)
await expect(storedSettings.cameraControls).toBe(
initialSettings.cameraControls
)
await expect(storedSettings.defaultDirectory).toBe(
initialSettings.defaultDirectory
)
await expect(storedSettings.defaultProjectName).toBe(
initialSettings.defaultProjectName
)
})
// Onboarding tests
test.describe('Onboarding tests', () => {
// Override test setup
const storageState = structuredClone(basicStorageState)
const s = TOML.parse(storageState.origins[0].localStorage[2].value) as {
settings: SaveSettingsPayload
}
s.settings.app.onboardingStatus = '/export'
storageState.origins[0].localStorage[2].value = TOML.stringify(s)
test.use({ storageState })
test('Onboarding redirects and code updating', async ({ page, context }) => {
const u = getUtils(page)
test('Onboarding redirects and code updating', async ({ page, context }) => {
const u = getUtils(page)
// Override beforeEach test setup
await context.addInitScript(async () => {
// Give some initial code, so we can test that it's cleared
localStorage.setItem('persistCode', 'const sigmaAllow = 15000')
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
// Test that the redirect happened
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
`/file/%2Fbrowser%2Fmain.kcl/onboarding/export`
const storedSettings = JSON.parse(
localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}'
)
// Test that you come back to this page when you refresh
await page.reload()
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
`/file/%2Fbrowser%2Fmain.kcl/onboarding/export`
)
// Test that the onboarding pane loaded
const title = page.locator('[data-testid="onboarding-content"]')
await expect(title).toBeAttached()
// Test that the code changes when you advance to the next step
await page.locator('[data-testid="onboarding-next"]').click()
await expect(page.locator('.cm-content')).toHaveText('')
// Test that the code is not empty when you click on the next step
await page.locator('[data-testid="onboarding-next"]').click()
await expect(page.locator('.cm-content')).toHaveText(/.+/)
storedSettings.onboardingStatus = '/export'
localStorage.setItem('SETTINGS_PERSIST_KEY', JSON.stringify(storedSettings))
})
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
// Test that the redirect happened
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
`/file/new/onboarding/export`
)
// Test that you come back to this page when you refresh
await page.reload()
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
`/file/new/onboarding/export`
)
// Test that the onboarding pane loaded
const title = page.locator('[data-testid="onboarding-content"]')
await expect(title).toBeAttached()
// Test that the code changes when you advance to the next step
await page.locator('[data-testid="onboarding-next"]').click()
await expect(page.locator('.cm-content')).toHaveText('')
// Test that the code is not empty when you click on the next step
await page.locator('[data-testid="onboarding-next"]').click()
await expect(page.locator('.cm-content')).toHaveText(/.+/)
})
test('Selections work on fresh and edited sketch', async ({ page }) => {
@ -800,134 +779,129 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await selectionSequence()
})
test.describe('Command bar tests', () => {
test('Command bar works and can change a setting', async ({ page }) => {
// Brief boilerplate
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/', { waitUntil: 'domcontentloaded' })
test('Command bar works and can change a setting', async ({ page }) => {
// Brief boilerplate
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
let cmdSearchBar = page.getByPlaceholder('Search commands')
let cmdSearchBar = page.getByPlaceholder('Search commands')
// First try opening the command bar and closing it
// It has a different label on mac and windows/linux, "Meta+K" and "Ctrl+/" respectively
await page
.getByRole('button', { name: 'Ctrl+/' })
.or(page.getByRole('button', { name: '⌘K' }))
.click()
await expect(cmdSearchBar).toBeVisible()
await page.keyboard.press('Escape')
await expect(cmdSearchBar).not.toBeVisible()
// First try opening the command bar and closing it
// It has a different label on mac and windows/linux, "Meta+K" and "Ctrl+/" respectively
await page
.getByRole('button', { name: 'Ctrl+/' })
.or(page.getByRole('button', { name: '⌘K' }))
.click()
await expect(cmdSearchBar).toBeVisible()
await page.keyboard.press('Escape')
await expect(cmdSearchBar).not.toBeVisible()
// Now try the same, but with the keyboard shortcut, check focus
await page.keyboard.press('Meta+K')
await expect(cmdSearchBar).toBeVisible()
await expect(cmdSearchBar).toBeFocused()
// Now try the same, but with the keyboard shortcut, check focus
await page.keyboard.press('Meta+K')
await expect(cmdSearchBar).toBeVisible()
await expect(cmdSearchBar).toBeFocused()
// Try typing in the command bar
await page.keyboard.type('theme')
const themeOption = page.getByRole('option', {
name: 'Settings · app · theme',
})
await expect(themeOption).toBeVisible()
await themeOption.click()
const themeInput = page.getByPlaceholder('Select an option')
await expect(themeInput).toBeVisible()
await expect(themeInput).toBeFocused()
// Select dark theme
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await expect(page.getByRole('option', { name: 'system' })).toHaveAttribute(
'data-headlessui-state',
'active'
// Try typing in the command bar
await page.keyboard.type('theme')
const themeOption = page.getByRole('option', { name: 'Set Theme' })
await expect(themeOption).toBeVisible()
await themeOption.click()
const themeInput = page.getByPlaceholder('system')
await expect(themeInput).toBeVisible()
await expect(themeInput).toBeFocused()
// Select dark theme
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowUp')
await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute(
'data-headlessui-state',
'active'
)
await page.keyboard.press('Enter')
// Check the toast appeared
await expect(page.getByText(`Set Theme to "${Themes.Dark}"`)).toBeVisible()
// Check that the theme changed
await expect(page.locator('body')).toHaveClass(`body-bg ${Themes.Dark}`)
})
test('Can extrude from the command bar', async ({ page, context }) => {
await context.addInitScript(async (token) => {
localStorage.setItem(
'persistCode',
`
const distance = sqrt(20)
const part001 = startSketchOn('-XZ')
|> startProfileAt([-6.95, 4.98], %)
|> line([25.1, 0.41], %)
|> line([0.73, -14.93], %)
|> line([-23.44, 0.52], %)
|> close(%)
`
)
await page.keyboard.press('Enter')
// Check the toast appeared
await expect(
page.getByText(`Set theme to "system" for this project`)
).toBeVisible()
// Check that the theme changed
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
})
// Override test setup code
const storageState = structuredClone(basicStorageState)
storageState.origins[0].localStorage[1].value = `const distance = sqrt(20)
const part001 = startSketchOn('-XZ')
|> startProfileAt([-6.95, 4.98], %)
|> line([25.1, 0.41], %)
|> line([0.73, -14.93], %)
|> line([-23.44, 0.52], %)
|> close(%)
`
test.use({ storageState })
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
test('Can extrude from the command bar', async ({ page, context }) => {
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
let cmdSearchBar = page.getByPlaceholder('Search commands')
await page.keyboard.press('Meta+K')
await expect(cmdSearchBar).toBeVisible()
// Make sure the stream is up
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// Search for extrude command and choose it
await page.getByRole('option', { name: 'Extrude' }).click()
await expect(page.locator('#arg-form > label')).toContainText(
'Please select one face'
)
await expect(page.getByRole('button', { name: 'selection' })).toBeDisabled()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click()
await expect(
page.getByRole('button', { name: 'Extrude' })
).not.toBeDisabled()
// Click to select face and set distance
await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click()
await page.getByRole('button', { name: 'Continue' }).click()
let cmdSearchBar = page.getByPlaceholder('Search commands')
await page.keyboard.press('Meta+K')
await expect(cmdSearchBar).toBeVisible()
// Assert that we're on the distance step
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled()
// Search for extrude command and choose it
await page.getByRole('option', { name: 'Extrude' }).click()
// Assert that the an alternative variable name is chosen,
// since the default variable name is already in use (distance)
await page.getByRole('button', { name: 'Create new variable' }).click()
await expect(page.getByPlaceholder('Variable name')).toHaveValue(
'distance001'
)
await expect(page.getByRole('button', { name: 'Continue' })).toBeEnabled()
await page.getByRole('button', { name: 'Continue' }).click()
// Assert that we're on the distance step
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled()
// Review step and argument hotkeys
await expect(
page.getByRole('button', { name: 'Submit command' })
).toBeEnabled()
await page.keyboard.press('Backspace')
await expect(
page.getByRole('button', { name: 'Distance 12', exact: false })
).toBeDisabled()
await page.keyboard.press('Enter')
// Assert that the an alternative variable name is chosen,
// since the default variable name is already in use (distance)
await page.getByRole('button', { name: 'Create new variable' }).click()
await expect(page.getByPlaceholder('Variable name')).toHaveValue(
'distance001'
)
await expect(page.getByRole('button', { name: 'Continue' })).toBeEnabled()
await page.getByRole('button', { name: 'Continue' }).click()
await expect(page.getByText('Confirm Extrude')).toBeVisible()
// Review step and argument hotkeys
await expect(
page.getByRole('button', { name: 'Submit command' })
).toBeEnabled()
await page.keyboard.press('Backspace')
await expect(
page.getByRole('button', { name: 'Distance 12', exact: false })
).toBeDisabled()
await page.keyboard.press('Enter')
await expect(page.getByText('Confirm Extrude')).toBeVisible()
// Check that the code was updated
await page.keyboard.press('Enter')
// Unfortunately this indentation seems to matter for the test
await expect(page.locator('.cm-content')).toHaveText(
`const distance = sqrt(20)
// Check that the code was updated
await page.keyboard.press('Enter')
// Unfortunately this indentation seems to matter for the test
await expect(page.locator('.cm-content')).toHaveText(
`const distance = sqrt(20)
const distance001 = 5 + 7
const part001 = startSketchOn('-XZ')
|> startProfileAt([-6.95, 4.98], %)
|> line([25.1, 0.41], %)
|> line([0.73, -14.93], %)
|> line([-23.44, 0.52], %)
|> close(%)
|> extrude(distance001, %)`.replace(/(\r\n|\n|\r)/gm, '') // remove newlines
)
})
|> startProfileAt([-6.95, 4.98], %)
|> line([25.1, 0.41], %)
|> line([0.73, -14.93], %)
|> line([-23.44, 0.52], %)
|> close(%)
|> extrude(distance001, %)`.replace(/(\r\n|\n|\r)/gm, '') // remove newlines
)
})
test('Can add multiple sketches', async ({ page }) => {
@ -1496,13 +1470,9 @@ test('Sketch on face', async ({ page, context }) => {
await page.getByText('startProfileAt([1.03, 1.03], %)').click()
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.setViewportSize({ width: 1200, height: 1200 })
await u.openAndClearDebugPanel()
await u.updateCamPosition([452, -152, 1166])
await u.closeDebugPanel()
await page.waitForTimeout(200)
const pointToDragFirst = [787, 565]
const pointToDragFirst = [691, 237]
await page.mouse.move(pointToDragFirst[0], pointToDragFirst[1])
await page.mouse.down()
await page.mouse.move(pointToDragFirst[0] - 20, pointToDragFirst[1], {
@ -1516,9 +1486,7 @@ test('Sketch on face', async ({ page, context }) => {
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
}], %)
|> line([2.81, -0.33], %)
|> line([-4.44, -2.13], %)
|> close(%)`)
@ -1533,7 +1501,6 @@ test('Sketch on face', async ({ page, context }) => {
await page.getByRole('button', { name: 'Extrude' }).click()
await expect(page.getByTestId('command-bar')).toBeVisible()
await page.waitForTimeout(100)
await page.keyboard.press('Enter')
await expect(page.getByText('Confirm Extrude')).toBeVisible()
@ -1542,9 +1509,7 @@ test('Sketch on face', async ({ page, context }) => {
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
}], %)
|> line([2.81, -0.33], %)
|> line([-4.44, -2.13], %)
|> close(%)
|> extrude(5 + 7, %)`)

View File

@ -7,18 +7,30 @@ import { spawn } from 'child_process'
import { APP_NAME } from 'lib/constants'
import JSZip from 'jszip'
import path from 'path'
import { basicSettings, basicStorageState } from './storageStates'
import * as TOML from '@iarna/toml'
test.beforeEach(async ({ page }) => {
test.beforeEach(async ({ context, page }) => {
await context.addInitScript(async (token) => {
localStorage.setItem('TOKEN_PERSIST_KEY', token)
localStorage.setItem('persistCode', ``)
localStorage.setItem(
'SETTINGS_PERSIST_KEY',
JSON.stringify({
baseUnit: 'in',
cameraControls: 'KittyCAD',
defaultDirectory: '',
defaultProjectName: 'project-$nnn',
onboardingStatus: 'dismissed',
showDebugPanel: true,
textWrapping: 'On',
theme: 'dark',
unitSystem: 'imperial',
})
)
}, secrets.token)
// reducedMotion kills animations, which speeds up tests and reduces flakiness
await page.emulateMedia({ reducedMotion: 'reduce' })
})
test.use({
storageState: structuredClone(basicStorageState),
})
test.setTimeout(60_000)
test('exports of each format should work', async ({ page, context }) => {
@ -320,22 +332,6 @@ test('extrude on each default plane should be stable', async ({
page,
context,
}) => {
await context.addInitScript(async () => {
localStorage.setItem(
'SETTINGS_PERSIST_KEY',
JSON.stringify({
baseUnit: 'in',
cameraControls: 'KittyCAD',
defaultDirectory: '',
defaultProjectName: 'project-$nnn',
onboardingStatus: 'dismissed',
showDebugPanel: true,
textWrapping: 'On',
theme: 'dark',
unitSystem: 'imperial',
})
)
})
const u = getUtils(page)
const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}')
|> startProfileAt([7.00, 4.40], %)
@ -357,26 +353,29 @@ test('extrude on each default plane should be stable', async ({
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel()
await page.waitForTimeout(200)
await page.getByText('Code').click()
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
await page.getByText('Code').click()
const runSnapshotsForOtherPlanes = async (plane = 'XY') => {
// clear code
await u.removeCurrentCode()
// add makeCode('XZ')
await u.openAndClearDebugPanel()
await page.locator('.cm-content').fill(makeCode(plane))
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel()
await page.getByText('Code').click()
await page.waitForTimeout(150)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
await page.getByText('Code').click()
}
await runSnapshotsForOtherPlanes('XY')
await runSnapshotsForOtherPlanes('-XY')
await runSnapshotsForOtherPlanes('XZ')
@ -387,6 +386,22 @@ test('extrude on each default plane should be stable', async ({
})
test('Draft segments should look right', async ({ page, context }) => {
await context.addInitScript(async () => {
localStorage.setItem(
'SETTINGS_PERSIST_KEY',
JSON.stringify({
baseUnit: 'in',
cameraControls: 'KittyCAD',
defaultDirectory: '',
defaultProjectName: 'project-$nnn',
onboardingStatus: 'dismissed',
showDebugPanel: true,
textWrapping: 'On',
theme: 'dark',
unitSystem: 'imperial',
})
)
})
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
@ -445,9 +460,26 @@ test('Draft segments should look right', async ({ page, context }) => {
})
})
test('Client side scene scale should match engine scale - Inch', async ({
test('Client side scene scale should match engine scale inch', async ({
page,
context,
}) => {
await context.addInitScript(async () => {
localStorage.setItem(
'SETTINGS_PERSIST_KEY',
JSON.stringify({
baseUnit: 'in',
cameraControls: 'KittyCAD',
defaultDirectory: '',
defaultProjectName: 'project-$nnn',
onboardingStatus: 'dismissed',
showDebugPanel: true,
textWrapping: 'On',
theme: 'dark',
unitSystem: 'imperial',
})
)
})
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
@ -480,7 +512,7 @@ test('Client side scene scale should match engine scale - Inch', async ({
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([9.06, -12.22], %)`)
|> startProfileAt([9.06, -12.22], %)`)
await page.waitForTimeout(100)
await u.closeDebugPanel()
@ -490,8 +522,8 @@ test('Client side scene scale should match engine scale - Inch', async ({
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([9.06, -12.22], %)
|> line([9.14, 0], %)`)
|> startProfileAt([9.06, -12.22], %)
|> line([9.14, 0], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)
@ -500,9 +532,9 @@ test('Client side scene scale should match engine scale - Inch', async ({
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([9.06, -12.22], %)
|> line([9.14, 0], %)
|> tangentialArcTo([27.34, -3.08], %)`)
|> startProfileAt([9.06, -12.22], %)
|> line([9.14, 0], %)
|> tangentialArcTo([27.34, -3.08], %)`)
// click tangential arc tool again to unequip it
await page.getByRole('button', { name: 'Tangential Arc' }).click()
@ -528,101 +560,102 @@ test('Client side scene scale should match engine scale - Inch', async ({
})
})
test.describe('Client side scene scale should match engine scale - Millimeters', () => {
const storageState = structuredClone(basicStorageState)
storageState.origins[0].localStorage[2].value = TOML.stringify({
settings: {
...basicSettings,
modeling: {
...basicSettings.modeling,
defaultUnit: 'mm',
},
},
test('Client side scene scale should match engine scale mm', async ({
page,
context,
}) => {
await context.addInitScript(async () => {
localStorage.setItem(
'SETTINGS_PERSIST_KEY',
JSON.stringify({
baseUnit: 'mm',
cameraControls: 'KittyCAD',
defaultDirectory: '',
defaultProjectName: 'project-$nnn',
onboardingStatus: 'dismissed',
showDebugPanel: true,
textWrapping: 'On',
theme: 'dark',
unitSystem: 'metric',
})
)
})
test.use({
storageState,
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
// click on "Start Sketch" button
await u.clearCommandLogs()
await u.doAndWaitForImageDiff(
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
200
)
// select a plane
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText(
`const part001 = startSketchOn('-XZ')`
)
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([230.03, -310.32], %)`)
await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([230.03, -310.32], %)
|> line([232.2, 0], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([230.03, -310.32], %)
|> line([232.2, 0], %)
|> tangentialArcTo([694.43, -78.12], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)
// screen shot should show the sketch
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
test('Millimeters', async ({ page }) => {
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
// exit sketch
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
// wait for execution done
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel()
await page.waitForTimeout(200)
// click on "Start Sketch" button
await u.clearCommandLogs()
await u.doAndWaitForImageDiff(
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
200
)
// select a plane
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText(
`const part001 = startSketchOn('-XZ')`
)
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([230.03, -310.32], %)`)
await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([230.03, -310.32], %)
|> line([232.2, 0], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([230.03, -310.32], %)
|> line([232.2, 0], %)
|> tangentialArcTo([694.43, -78.12], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)
// screen shot should show the sketch
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
// exit sketch
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
// wait for execution done
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel()
await page.waitForTimeout(200)
// second screen shot should look almost identical, i.e. scale should be the same.
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
// second screen shot should look almost identical, i.e. scale should be the same.
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
})
@ -633,14 +666,14 @@ test('Sketch on face with none z-up', async ({ page, context }) => {
'persistCode',
`const part001 = startSketchOn('-XZ')
|> startProfileAt([1.4, 2.47], %)
|> line([9.31, 10.55], %, 'seg01')
|> line({ to: [9.31, 10.55], tag: 'seg01' }, %)
|> line([11.91, -10.42], %)
|> close(%)
|> extrude(5 + 7, %)
const part002 = startSketchOn(part001, 'seg01')
|> startProfileAt([-2.89, 1.82], %)
|> line([4.68, 3.05], %)
|> line([0, -7.79], %, 'seg02')
|> line({ to: [0, -7.79], tag: 'seg02' }, %)
|> close(%)
|> extrude(5 + 7, %)
`
@ -667,4 +700,6 @@ const part002 = startSketchOn(part001, 'seg01')
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
await page.waitForTimeout(200)
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -1,40 +0,0 @@
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { secrets } from './secrets'
import * as TOML from '@iarna/toml'
import { Themes } from 'lib/theme'
export const basicSettings = {
app: {
theme: Themes.Dark,
onboardingStatus: 'dismissed',
projectDirectory: '',
},
modeling: {
defaultUnit: 'in',
mouseControls: 'KittyCAD',
showDebugPanel: true,
},
projects: {
defaultProjectName: 'project-$nnn',
},
textEditor: {
textWrapping: true,
},
} satisfies Partial<SaveSettingsPayload>
export const basicStorageState = {
cookies: [],
origins: [
{
origin: 'http://localhost:3000',
localStorage: [
{ name: 'TOKEN_PERSIST_KEY', value: secrets.token },
{ name: 'persistCode', value: '' },
{
name: '/user.toml',
value: TOML.stringify({ settings: basicSettings }),
},
],
},
],
}

View File

@ -33,7 +33,7 @@ async function clearCommandLogs(page: Page) {
}
async function expectCmdLog(page: Page, locatorStr: string) {
await expect(page.locator(locatorStr).last()).toBeVisible()
await expect(page.locator(locatorStr)).toBeVisible()
}
async function waitForDefaultPlanesToBeVisible(page: Page) {

View File

@ -1,10 +1,7 @@
import { browser, $, expect } from '@wdio/globals'
import fs from 'fs/promises'
const documentsDir = `${process.env.HOME}/Documents`
const userSettingsFile = `${process.env.HOME}/.config/dev.zoo.modeling-app/user.toml`
const defaultProjectDir = `${documentsDir}/zoo-modeling-app-projects`
const newProjectDir = `${documentsDir}/a-different-directory`
const defaultDir = `${process.env.HOME}/Documents/zoo-modeling-app-projects`
const userCodeDir = '/tmp/kittycad_user_code'
async function click(element: WebdriverIO.Element): Promise<void> {
@ -13,25 +10,12 @@ async function click(element: WebdriverIO.Element): Promise<void> {
await browser.execute('arguments[0].click();', element)
}
/* Shoutout to @Sheap on Github for a great workaround utility:
* https://github.com/tauri-apps/tauri/issues/6541#issue-1638944060
*/
async function setDatasetValue(
field: WebdriverIO.Element,
property: string,
value: string
) {
await browser.execute(`arguments[0].dataset.${property} = "${value}"`, field)
}
describe('ZMA (Tauri, Linux)', () => {
it('opens the auth page and signs in', async () => {
// Clean up filesystem from previous tests
await new Promise((resolve) => setTimeout(resolve, 100))
await fs.rm(defaultProjectDir, { force: true, recursive: true })
await fs.rm(defaultDir, { force: true, recursive: true })
await fs.rm(userCodeDir, { force: true })
await fs.rm(userSettingsFile, { force: true })
await fs.mkdir(newProjectDir, { recursive: true })
const signInButton = await $('[data-testid="sign-in-button"]')
expect(await signInButton.getText()).toEqual('Sign in')
@ -81,25 +65,13 @@ describe('ZMA (Tauri, Linux)', () => {
const settingsButton = await $('[data-testid="settings-button"]')
await click(settingsButton)
const projectDirInput = await $('[data-testid="project-directory-input"]')
expect(await projectDirInput.getValue()).toEqual(defaultProjectDir)
const defaultDirInput = await $('[data-testid="default-directory-input"]')
expect(await defaultDirInput.getValue()).toEqual(defaultDir)
/*
* We've set up the project directory input (in initialSettings.tsx)
* to be able to skip the folder selection dialog if data-testValue
* has a value, allowing us to test the input otherwise works.
*/
await setDatasetValue(projectDirInput, 'testValue', newProjectDir)
const projectDirButton = await $('[data-testid="project-directory-button"]')
await click(projectDirButton)
await new Promise((resolve) => setTimeout(resolve, 500))
// This line is broken. I need a different way to grab the toast
await expect(await $('div*=Set project directory to')).toBeDisplayed()
const nameInput = await $('[data-testid="projects-defaultProjectName"]')
const nameInput = await $('[data-testid="name-input"]')
expect(await nameInput.getValue()).toEqual('project-$nnn')
const closeButton = await $('[data-testid="settings-close-button"]')
const closeButton = await $('[data-testid="close-button"]')
await click(closeButton)
})

View File

@ -1,6 +1,6 @@
{
"name": "untitled-app",
"version": "0.17.3",
"version": "0.17.0",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.15.0",
@ -10,32 +10,25 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.18",
"@headlessui/tailwindcss": "^0.2.0",
"@iarna/toml": "^2.2.5",
"@kittycad/lib": "^0.0.56",
"@lezer/javascript": "^1.4.9",
"@open-rpc/client-js": "^1.8.1",
"@react-hook/resize-observer": "^1.2.6",
"@replit/codemirror-interact": "^6.3.0",
"@tauri-apps/api": "^2.0.0-beta.7",
"@tauri-apps/plugin-dialog": "^2.0.0-beta.2",
"@tauri-apps/plugin-fs": "^2.0.0-beta.2",
"@tauri-apps/plugin-http": "^2.0.0-beta.2",
"@tauri-apps/plugin-os": "^2.0.0-beta.2",
"@tauri-apps/plugin-shell": "^2.0.0-beta.2",
"@tauri-apps/api": "^1.5.3",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.5.2",
"@ts-stack/markdown": "^1.5.0",
"@tweenjs/tween.js": "^23.1.1",
"@types/node": "^18.19.26",
"@types/react": "^18.2.73",
"@types/react": "^18.2.67",
"@types/react-dom": "^18.2.22",
"@uiw/react-codemirror": "^4.21.25",
"@uiw/react-codemirror": "^4.21.24",
"@xstate/inspect": "^0.8.0",
"@xstate/react": "^3.2.2",
"crypto-js": "^4.2.0",
"debounce-promise": "^3.1.2",
"decamelize": "^6.0.0",
"formik": "^2.4.3",
"fuse.js": "^7.0.0",
"http-server": "^14.1.1",
@ -53,6 +46,7 @@
"react-router-dom": "^6.22.3",
"sketch-helpers": "^0.0.4",
"swr": "^2.2.2",
"tauri-plugin-fs-extra-api": "https://github.com/tauri-apps/tauri-plugin-fs-extra#v1",
"three": "^0.160.0",
"toml": "^3.0.0",
"ts-node": "^10.9.2",
@ -90,7 +84,7 @@
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
"lint": "eslint --fix src",
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json",
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json",
"postinstall": "yarn xstate:typegen",
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\""
},
@ -114,9 +108,9 @@
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.24.3",
"@babel/preset-env": "^7.23.3",
"@playwright/test": "^1.39.0",
"@tauri-apps/cli": "^2.0.0-beta.12",
"@tauri-apps/cli": "^1.5.11",
"@types/crypto-js": "^4.2.2",
"@types/debounce-promise": "^3.1.9",
"@types/pixelmatch": "^5.2.6",
@ -138,7 +132,7 @@
"eslint": "^8.57.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0",
"happy-dom": "^14.3.10",
"happy-dom": "^14.3.1",
"husky": "^9.0.11",
"pixelmatch": "^5.3.0",
"pngjs": "^7.0.0",
@ -147,7 +141,7 @@
"prettier": "^2.8.0",
"setimmediate": "^1.0.5",
"tailwindcss": "^3.4.1",
"vite": "^5.2.6",
"vite": "^5.2.2",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2",

View File

@ -1,5 +1,4 @@
import { defineConfig, devices } from '@playwright/test'
import { basicStorageState } from './e2e/playwright/storageStates'
/**
* Read environment variables from file.
@ -29,9 +28,6 @@ export default defineConfig({
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
/* Use a common shared localStorage */
storageState: basicStorageState,
},
/* Configure projects for major browsers */

Binary file not shown.

Before

Width:  |  Height:  |  Size: 327 B

2527
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -7,12 +7,12 @@ license = ""
repository = "https://github.com/KittyCAD/modeling-app"
default-run = "app"
edition = "2021"
rust-version = "1.70"
rust-version = "1.60"
# 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", features = [] }
tauri-build = { version = "1.5.1", features = [] }
[dependencies]
anyhow = "1"
@ -20,14 +20,9 @@ kittycad = "0.2.63"
oauth2 = "4.4.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tauri = { version = "2.0.0-beta", features = [ "devtools", "unstable"] }
tauri-plugin-dialog = { version = "2.0.0-beta.0" }
tauri-plugin-fs = { version = "2.0.0-beta.0" }
tauri-plugin-http = { version = "2.0.0-beta.0" }
tauri-plugin-os = { version = "2.0.0-beta.0" }
tauri-plugin-shell = { version = "2.0.0-beta.0" }
tauri-plugin-updater = { version = "2.0.0-beta.0" }
tokio = { version = "1.37.0", features = ["time"] }
tauri = { version = "1.6.1", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "devtools"] }
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tokio = { version = "1.36.0", features = ["time"] }
toml = "0.8.2"
[features]

View File

@ -1,87 +0,0 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"description": "Capability for the main window",
"context": "local",
"windows": [
"main"
],
"permissions": [
"path:default",
"event:default",
"window:default",
"app:default",
"resources:default",
"menu:default",
"tray:default",
"fs:allow-create",
"fs:allow-read-file",
"fs:allow-read-text-file",
"fs:allow-write-file",
"fs:allow-write-text-file",
"fs:allow-read-dir",
"fs:allow-copy-file",
"fs:allow-mkdir",
"fs:allow-remove",
"fs:allow-remove",
"fs:allow-rename",
"fs:allow-exists",
"fs:allow-stat",
{
"identifier": "fs:scope",
"allow": [
{
"path": "$HOME/**/*"
},
{
"path": "$HOME/.config"
},
{
"path": "$HOME/.config/**/*"
},
{
"path": "$APPCONFIG"
},
{
"path": "$APPCONFIG/**/*"
},
{
"path": "$DOCUMENT"
},
{
"path": "$DOCUMENT/**/*"
}
]
},
"shell:allow-open",
"dialog:allow-open",
"dialog:allow-save",
"dialog:allow-message",
"dialog:allow-ask",
"dialog:allow-confirm",
{
"identifier": "http:default",
"allow": [
"https://dev.kittycad.io/*",
"https://dev.zoo.dev/*",
"https://kittycad.io/*",
"https://zoo.dev/*",
"https://api.dev.kittycad.io/*",
"https://api.dev.zoo.dev/*"
]
},
"os:allow-platform",
"os:allow-version",
"os:allow-os-type",
"os:allow-family",
"os:allow-arch",
"os:allow-exe-extension",
"os:allow-locale",
"os:allow-hostname"
],
"platforms": [
"linux",
"macOS",
"windows"
]
}

View File

@ -4,15 +4,11 @@
use std::env;
use std::fs;
use std::io::Read;
use std::path::Path;
use std::path::PathBuf;
use anyhow::Result;
use oauth2::TokenResponse;
use serde::Serialize;
use std::process::Command;
use tauri::ipc::InvokeError;
use tauri_plugin_shell::ShellExt;
use tauri::{InvokeError, Manager};
const DEFAULT_HOST: &str = "https://api.kittycad.io";
/// This command returns the a json string parse from a toml file at the path.
@ -28,56 +24,6 @@ fn read_toml(path: &str) -> Result<String, InvokeError> {
Ok(value)
}
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
/// Removed from tauri v2
#[derive(Debug, Serialize)]
pub struct DiskEntry {
/// The path to the entry.
pub path: PathBuf,
/// The name of the entry (file name with extension or directory name).
pub name: Option<String>,
/// The children of this entry if it's a directory.
#[serde(skip_serializing_if = "Option::is_none")]
pub children: Option<Vec<DiskEntry>>,
}
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
/// Removed from tauri v2
fn is_dir<P: AsRef<Path>>(path: P) -> Result<bool> {
std::fs::metadata(path)
.map(|md| md.is_dir())
.map_err(Into::into)
}
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
/// Removed from tauri v2
#[tauri::command]
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();
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()),
});
}
}
Ok(files_and_dirs)
}
/// 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> {
@ -139,8 +85,7 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
fs::write("/tmp/kittycad_user_code", details.user_code().secret())
.expect("Unable to write /tmp/kittycad_user_code file");
} else {
app.shell()
.open(auth_uri.secret(), None)
tauri::api::shell::open(&app.shell_scope(), auth_uri.secret(), None)
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
}
@ -220,15 +165,12 @@ fn show_in_folder(path: String) {
fn main() {
tauri::Builder::default()
.setup(|_app| {
#[cfg(debug_assertions)]
#[cfg(debug_assertions)] // only include this code on debug builds
{
use tauri::Manager;
_app.get_webview("main").unwrap().open_devtools();
}
#[cfg(not(debug_assertions))]
{
_app.handle()
.plugin(tauri_plugin_updater::Builder::new().build())?;
let window = _app.get_window("main").unwrap();
// comment out the below if you don't devtools to open everytime.
// it's useful because otherwise devtools shuts everytime rust code changes.
window.open_devtools();
}
Ok(())
})
@ -237,14 +179,9 @@ fn main() {
login,
read_toml,
read_txt_file,
read_dir_recursive,
show_in_folder,
])
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_fs_extra::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@ -1,9 +1,90 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"app": {
"build": {
"beforeDevCommand": "yarn start",
"devPath": "http://localhost:3000",
"distDir": "../build"
},
"package": {
"productName": "zoo-modeling-app",
"version": "0.17.0"
},
"tauri": {
"allowlist": {
"all": false,
"dialog": {
"all": true,
"ask": true,
"confirm": true,
"message": true,
"open": true,
"save": true
},
"fs": {
"scope": [
"$HOME/**/*",
"$APPCONFIG",
"$APPCONFIG/**/*",
"$DOCUMENT",
"$DOCUMENT/**/*"
],
"all": true
},
"http": {
"request": true,
"scope": [
"https://dev.kittycad.io/*",
"https://dev.zoo.dev/*",
"https://kittycad.io/*",
"https://zoo.dev/*",
"https://api.dev.kittycad.io/*",
"https://api.dev.zoo.dev/*"
]
},
"os": {
"all": true
},
"shell": {
"open": true
},
"path": {
"all": true
}
},
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "",
"deb": {
"depends": []
},
"externalBin": [],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "dev.zoo.modeling-app",
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"targets": "all"
},
"security": {
"csp": null
},
"updater": {
"active": false
},
"windows": [
{
"fullscreen": false,
@ -13,47 +94,5 @@
"width": 1800
}
]
},
"build": {
"beforeDevCommand": "yarn start",
"devUrl": "http://localhost:3000",
"frontendDist": "../build"
},
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "",
"externalBin": [],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"linux": {
"deb": {
"depends": []
}
},
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"targets": "all"
},
"identifier": "dev.zoo.modeling-app",
"plugins": {
"shell": {
"open": true
}
},
"productName": "Zoo Modeling App",
"version": "0.17.3"
}
}

View File

@ -0,0 +1,6 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": {
"productName": "Zoo Modeling App"
}
}

View File

@ -1,13 +1,6 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"bundle": {
"windows": {
"certificateThumbprint": "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D",
"digestAlgorithm": "sha256",
"timestampUrl": "http://timestamp.digicert.com"
}
},
"plugins": {
"tauri": {
"updater": {
"active": true,
"endpoints": [
@ -15,6 +8,14 @@
],
"dialog": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K"
},
"bundle": {
"identifier": "io.kittycad.modeling-app",
"windows": {
"certificateThumbprint": "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D",
"digestAlgorithm": "sha256",
"timestampUrl": "http://timestamp.digicert.com"
}
}
}
}

View File

@ -0,0 +1,6 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": {
"productName": "Zoo Modeling App"
}
}

View File

@ -1,6 +1,6 @@
import { useCallback, MouseEventHandler, useEffect } from 'react'
import { DebugPanel } from './components/DebugPanel'
import { uuidv4 } from 'lib/utils'
import { v4 as uuidv4 } from 'uuid'
import { PaneType, useStore } from './useStore'
import { Logs, KCLErrors } from './components/Logs'
import { CollapsiblePanel } from './components/CollapsiblePanel'
@ -33,10 +33,10 @@ import { useModelingContext } from 'hooks/useModelingContext'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import { isTauri } from 'lib/isTauri'
import { useLspContext } from 'components/LspProvider'
import { useRefreshSettings } from 'hooks/useRefreshSettings'
import { useValidateSettings } from 'hooks/useValidateSettings'
export function App() {
useRefreshSettings(paths.FILE + 'SETTINGS')
useValidateSettings()
const { project, file } = useLoaderData() as IndexLoaderData
const navigate = useNavigate()
const filePath = useAbsoluteFilePath()
@ -64,14 +64,10 @@ export function App() {
}))
const { settings } = useSettingsAuthContext()
const {
modeling: { showDebugPanel },
app: { theme, onboardingStatus },
} = settings.context
const { showDebugPanel, onboardingStatus, theme } = settings?.context || {}
const { state, send } = useModelingContext()
const editorTheme =
theme.current === Themes.System ? getSystemTheme() : theme.current
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
// Pane toggling keyboard shortcuts
const togglePane = useCallback(
@ -99,7 +95,7 @@ export function App() {
)
const paneOpacity = [onboardingPaths.CAMERA, onboardingPaths.STREAMING].some(
(p) => p === onboardingStatus.current
(p) => p === onboardingStatus
)
? 'opacity-20'
: didDragInStream
@ -153,7 +149,7 @@ export function App() {
<ModalContainer />
<Resizable
className={
'pointer-events-none h-full flex flex-col flex-1 z-10 my-2 ml-2 pr-1 transition-opacity transition-duration-75 ' +
'pointer-events-none h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' +
+paneOpacity
}
defaultSize={{
@ -166,8 +162,8 @@ export function App() {
maxHeight={'auto'}
handleClasses={{
right:
'hover:bg-chalkboard-10 hover:dark:bg-chalkboard-110 bg-transparent transition-colors duration-75 transition-ease-out delay-100 ' +
(buttonDownInStream || onboardingStatus.current === 'camera'
'hover:bg-chalkboard-10/50 bg-transparent transition-colors duration-75 transition-ease-out delay-100 ' +
(buttonDownInStream || onboardingStatus === 'camera'
? 'pointer-events-none '
: 'pointer-events-auto'),
}}
@ -202,13 +198,13 @@ export function App() {
theme={editorTheme}
open={openPanes.includes('kclErrors')}
title="KCL Errors"
iconClassNames={{ bg: 'group-open:bg-destroy-70' }}
iconClassNames={{ icon: 'group-open:text-destroy-30' }}
/>
</section>
</div>
</Resizable>
<Stream className="absolute inset-0 z-0" />
{showDebugPanel.current && (
{showDebugPanel && (
<DebugPanel
title="Debug"
className={

View File

@ -22,18 +22,19 @@ import { paths } from 'lib/paths'
import {
fileLoader,
homeLoader,
indexLoader,
onboardingRedirectLoader,
settingsLoader,
} from 'lib/routeLoaders'
import { CommandBarProvider } from 'components/CommandBar/CommandBarProvider'
import SettingsAuthProvider from 'components/SettingsAuthProvider'
import LspProvider from 'components/LspProvider'
import { KclContextProvider } from 'lang/KclProvider'
import { BROWSER_PROJECT_NAME } from 'lib/constants'
export const BROWSER_FILE_NAME = 'new'
const router = createBrowserRouter([
{
loader: settingsLoader,
loader: indexLoader,
id: paths.INDEX,
element: (
<CommandBarProvider>
@ -46,14 +47,14 @@ const router = createBrowserRouter([
</KclContextProvider>
</CommandBarProvider>
),
errorElement: <ErrorPage />,
children: [
{
path: paths.INDEX,
loader: () =>
isTauri()
? redirect(paths.HOME)
: redirect(paths.FILE + '/%2F' + BROWSER_PROJECT_NAME),
: redirect(paths.FILE + '/' + BROWSER_FILE_NAME),
errorElement: <ErrorPage />,
},
{
loader: fileLoader,
@ -66,29 +67,29 @@ const router = createBrowserRouter([
<Outlet />
<App />
<CommandBar />
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
</ModelingMachineProvider>
<WasmErrBanner />
</FileMachineProvider>
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
</Auth>
),
children: [
{
id: paths.FILE + 'SETTINGS',
loader: settingsLoader,
loader: onboardingRedirectLoader,
index: true,
element: <></>,
},
{
children: [
{
loader: onboardingRedirectLoader,
index: true,
element: <></>,
},
{
path: makeUrlPathRelative(paths.SETTINGS),
loader: indexLoader, // very rare someone will load into settings first, but it's possible in the browser
element: <Settings />,
},
{
path: makeUrlPathRelative(paths.ONBOARDING.INDEX),
element: <Onboarding />,
loader: indexLoader, // very rare someone will load into settings first, but it's possible in the browser
children: onboardingRoutes,
},
],
@ -107,15 +108,8 @@ const router = createBrowserRouter([
id: paths.HOME,
loader: homeLoader,
children: [
{
index: true,
element: <></>,
id: paths.HOME + 'SETTINGS',
loader: settingsLoader,
},
{
path: makeUrlPathRelative(paths.SETTINGS),
loader: settingsLoader,
element: <Settings />,
},
],

View File

@ -18,12 +18,8 @@ export const Toolbar = () => {
const { commandBarSend } = useCommandsContext()
const { state, send, context } = useModelingContext()
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
const iconClassName =
'group-disabled:text-chalkboard-50 group-enabled:group-hover:!text-chalkboard-10 group-pressed:!text-chalkboard-10'
const bgClassName =
'group-disabled:!bg-transparent group-enabled:group-hover:bg-primary group-pressed:bg-primary'
const buttonClassName =
'bg-chalkboard-10 dark:bg-chalkboard-100 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100'
'group-enabled:group-hover:bg-energy-10 group-pressed:bg-energy-10 dark:group-enabled:group-hover:bg-chalkboard-80 dark:group-pressed:bg-chalkboard-80'
const pathId = useMemo(() => {
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) {
return false
@ -68,14 +64,12 @@ export const Toolbar = () => {
{state.nextEvents.includes('Enter sketch') && (
<li className="contents">
<ActionButton
className={buttonClassName}
Element="button"
onClick={() =>
send({ type: 'Enter sketch', data: { forceNewSketch: true } })
}
icon={{
icon: 'sketch',
iconClassName,
bgClassName,
}}
disabled={disableAllButtons}
@ -87,12 +81,10 @@ export const Toolbar = () => {
{state.nextEvents.includes('Enter sketch') && pathId && (
<li className="contents">
<ActionButton
className={buttonClassName}
Element="button"
onClick={() => send({ type: 'Enter sketch' })}
icon={{
icon: 'sketch',
iconClassName,
bgClassName,
}}
disabled={disableAllButtons}
@ -104,12 +96,10 @@ export const Toolbar = () => {
{state.nextEvents.includes('Cancel') && !state.matches('idle') && (
<li className="contents">
<ActionButton
className={buttonClassName}
Element="button"
onClick={() => send({ type: 'Cancel' })}
icon={{
icon: 'arrowLeft',
iconClassName,
bgClassName,
}}
disabled={disableAllButtons}
@ -122,7 +112,6 @@ export const Toolbar = () => {
<>
<li className="contents" key="line-button">
<ActionButton
className={buttonClassName}
Element="button"
onClick={() =>
state?.matches('Sketch.Line tool')
@ -130,9 +119,9 @@ export const Toolbar = () => {
: send('Equip Line tool')
}
aria-pressed={state?.matches('Sketch.Line tool')}
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
icon={{
icon: 'line',
iconClassName,
bgClassName,
}}
disabled={disableAllButtons}
@ -142,7 +131,6 @@ export const Toolbar = () => {
</li>
<li className="contents" key="tangential-arc-button">
<ActionButton
className={buttonClassName}
Element="button"
onClick={() =>
state.matches('Sketch.Tangential arc to')
@ -150,9 +138,9 @@ export const Toolbar = () => {
: send('Equip tangential arc to')
}
aria-pressed={state.matches('Sketch.Tangential arc to')}
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
icon={{
icon: 'arc',
iconClassName,
bgClassName,
}}
disabled={
@ -191,8 +179,8 @@ export const Toolbar = () => {
.map((eventName) => (
<li className="contents" key={eventName}>
<ActionButton
className={buttonClassName}
Element="button"
className="text-sm"
key={eventName}
onClick={() => send(eventName)}
disabled={
@ -203,7 +191,6 @@ export const Toolbar = () => {
title={eventName}
icon={{
icon: 'line',
iconClassName,
bgClassName,
}}
>
@ -216,8 +203,8 @@ export const Toolbar = () => {
{state.matches('idle') && (
<li className="contents">
<ActionButton
className={buttonClassName}
Element="button"
className="text-sm"
onClick={() =>
commandBarSend({
type: 'Find and select command',
@ -232,7 +219,6 @@ export const Toolbar = () => {
}
icon={{
icon: 'extrude',
iconClassName,
bgClassName,
}}
>
@ -245,16 +231,16 @@ export const Toolbar = () => {
}
return (
<div className="max-w-full flex items-stretch rounded-l-sm rounded-r-full bg-chalkboard-10/80 dark:bg-chalkboard-110/70 relative">
<menu className="flex-1 pl-1 pr-2 py-0 overflow-hidden rounded-l-sm whitespace-nowrap border-solid border border-primary/30 dark:border-chalkboard-90 border-r-0">
<div className="max-w-full flex items-stretch rounded-l-sm rounded-r-full bg-chalkboard-10 dark:bg-chalkboard-100 relative">
<menu className="flex-1 pl-1 pr-2 py-0 overflow-hidden rounded-l-sm whitespace-nowrap bg-chalkboard-10 dark:bg-chalkboard-100 border-solid border border-energy-10 dark:border-chalkboard-90 border-r-0">
<ToolbarButtons />
</menu>
<ActionButton
Element="button"
onClick={() => commandBarSend({ type: 'Open' })}
className="rounded-r-full pr-4 self-stretch border-primary/30 hover:border-primary dark:border-chalkboard-80 dark:bg-chalkboard-80 text-primary"
className="rounded-r-full pr-4 self-stretch border-energy-10 hover:border-energy-10 dark:border-chalkboard-80 bg-energy-10/50 hover:bg-energy-10 dark:bg-chalkboard-80 dark:text-energy-10"
>
{platform === 'macos' ? '⌘K' : 'Ctrl+/'}
{platform === 'darwin' ? '⌘K' : 'Ctrl+/'}
</ActionButton>
</div>
)

View File

@ -21,7 +21,7 @@ import {
Subscription,
EngineCommandManager,
} from 'lang/std/engineConnection'
import { uuidv4 } from 'lib/utils'
import { v4 as uuidv4 } from 'uuid'
import { deg2Rad } from 'lib/utils2d'
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
import * as TWEEN from '@tweenjs/tween.js'

View File

@ -4,15 +4,10 @@ import { useModelingContext } from 'hooks/useModelingContext'
import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useStore } from 'useStore'
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
import { DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
import { ReactCameraProperties } from './CameraControls'
import { throttle } from 'lib/utils'
import { sceneInfra } from 'lib/singletons'
import {
EXTRA_SEGMENT_HANDLE,
PROFILE_START,
getParentGroup,
} from './sceneEntities'
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
const [isCamMoving, setIsCamMoving] = useState(false)
@ -42,10 +37,10 @@ export const ClientSideScene = ({
}: {
cameraControls: ReturnType<
typeof useSettingsAuthContext
>['settings']['context']['modeling']['mouseControls']['current']
>['settings']['context']['cameraControls']
}) => {
const canvasRef = useRef<HTMLDivElement>(null)
const { state, send, context } = useModelingContext()
const { state, send } = useModelingContext()
const { hideClient, hideServer } = useShouldHideScene()
const { setHighlightRange } = useStore((s) => ({
setHighlightRange: s.setHighlightRange,
@ -81,33 +76,9 @@ export const ClientSideScene = ({
}
}, [])
let cursor = 'default'
if (state.matches('Sketch')) {
if (
context.mouseState.type === 'isHovering' &&
getParentGroup(context.mouseState.on, [
ARROWHEAD,
EXTRA_SEGMENT_HANDLE,
PROFILE_START,
])
) {
cursor = 'move'
} else if (context.mouseState.type === 'isDragging') {
cursor = 'grabbing'
} else if (
state.matches('Sketch.Line tool') ||
state.matches('Sketch.Tangential arc to')
) {
cursor = 'crosshair'
} else {
cursor = 'default'
}
}
return (
<div
ref={canvasRef}
style={{ cursor: cursor }}
className={`absolute inset-0 h-full w-full transition-all duration-300 ${
hideClient ? 'opacity-0' : 'opacity-100'
} ${hideServer ? 'bg-black' : ''} ${

View File

@ -28,15 +28,12 @@ export function createGridHelper({
gridHelper.rotation.x = Math.PI / 2
return gridHelper
}
const fudgeFactor = 72.66985970437086
export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) =>
(0.55 * fudgeFactor) / cam.zoom / window.innerHeight
0.55 / cam.zoom
export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) =>
(group.position.distanceTo(cam.position) * cam.fov * fudgeFactor) /
4000 /
window.innerHeight
(group.position.distanceTo(cam.position) * cam.fov) / 4000
export function isQuaternionVertical(q: Quaternion) {
const v = new Vector3(0, 0, 1).applyQuaternion(q)

View File

@ -12,7 +12,6 @@ import {
OrthographicCamera,
PerspectiveCamera,
PlaneGeometry,
Points,
Quaternion,
Scene,
Shape,
@ -82,25 +81,20 @@ import {
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
import { createGridHelper, orthoScale, perspScale } from './helpers'
import { Models } from '@kittycad/lib'
import { uuidv4 } from 'lib/utils'
import { v4 as uuidv4 } from 'uuid'
import { SketchDetails } from 'machines/modelingMachine'
import { EngineCommandManager } from 'lang/std/engineConnection'
type DraftSegment = 'line' | 'tangentialArcTo'
export const EXTRA_SEGMENT_HANDLE = 'extraSegmentHandle'
export const EXTRA_SEGMENT_OFFSET_PX = 8
export const PROFILE_START = 'profile-start'
export const STRAIGHT_SEGMENT = 'straight-segment'
export const STRAIGHT_SEGMENT_BODY = 'straight-segment-body'
export const STRAIGHT_SEGMENT_DASH = 'straight-segment-body-dashed'
export const TANGENTIAL_ARC_TO__SEGMENT_DASH =
'tangential-arc-to-segment-body-dashed'
export const TANGENTIAL_ARC_TO_SEGMENT = 'tangential-arc-to-segment'
export const TANGENTIAL_ARC_TO_SEGMENT_BODY = 'tangential-arc-to-segment-body'
export const SEGMENT_WIDTH_PX = 1.6
export const HIDE_SEGMENT_LENGTH = 75 // in pixels
export const HIDE_HOVER_SEGMENT_LENGTH = 60 // in pixels
export const TANGENTIAL_ARC_TO__SEGMENT_DASH =
'tangential-arc-to-segment-body-dashed'
export const PROFILE_START = 'profile-start'
// This singleton Class is responsible for all of the things the user sees and interacts with.
// That mostly mean sketch elements.
@ -117,12 +111,8 @@ export class SceneEntities {
this.engineCommandManager = engineCommandManager
this.scene = sceneInfra?.scene
sceneInfra?.camControls.subscribeToCamChange(this.onCamChange)
window.addEventListener('resize', this.onWindowResize)
}
onWindowResize = () => {
this.onCamChange()
}
onCamChange = () => {
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
@ -292,6 +282,7 @@ export class SceneEntities {
sketchGroup: SketchGroup
variableDeclarationName: string
}> {
sceneInfra.resetMouseListeners()
this.createIntersectionPlane()
const { truncatedAst, programMemoryOverride, variableDeclarationName } =
@ -304,7 +295,7 @@ export class SceneEntities {
})
const sketchGroup = sketchGroupFromPathToNode({
pathToNode: sketchPathToNode,
ast: maybeModdedAst,
ast: kclManager.ast,
programMemory,
})
if (!Array.isArray(sketchGroup?.value))
@ -392,7 +383,6 @@ export class SceneEntities {
pathToNode: segPathToNode,
isDraftSegment,
scale: factor,
texture: sceneInfra.extraSegmentTexture,
})
} else {
seg = straightSegment({
@ -403,7 +393,6 @@ export class SceneEntities {
isDraftSegment,
scale: factor,
callExpName,
texture: sceneInfra.extraSegmentTexture,
})
}
seg.layers.set(SKETCH_LAYER)
@ -446,7 +435,6 @@ export class SceneEntities {
) => {
await kclManager.updateAst(modifiedAst, false)
await this.tearDownSketch({ removeAxis: false })
sceneInfra.resetMouseListeners()
await this.setupSketch({
sketchPathToNode,
forward,
@ -454,12 +442,7 @@ export class SceneEntities {
position: origin,
maybeModdedAst: kclManager.ast,
})
this.setupSketchIdleCallbacks({
forward,
up,
position: origin,
pathToNode: sketchPathToNode,
})
this.setupSketchIdleCallbacks(sketchPathToNode)
}
setUpDraftSegment = async (
sketchPathToNode: PathToNode,
@ -484,20 +467,19 @@ export class SceneEntities {
const index = sg.value.length // because we've added a new segment that's not in the memory yet, no need for `-1`
const mod = addNewSketchLn({
node: _ast,
let modifiedAst = addNewSketchLn({
node: kclManager.ast,
programMemory: kclManager.programMemory,
to: [lastSeg.to[0], lastSeg.to[1]],
from: [lastSeg.to[0], lastSeg.to[1]],
fnName: segmentName,
pathToNode: sketchPathToNode,
})
const modifiedAst = parse(recast(mod.modifiedAst))
}).modifiedAst
modifiedAst = parse(recast(modifiedAst))
const draftExpressionsIndices = { start: index, end: index }
if (shouldTearDown) await this.tearDownSketch({ removeAxis: false })
sceneInfra.resetMouseListeners()
const { truncatedAst, programMemoryOverride, sketchGroup } =
await this.setupSketch({
sketchPathToNode,
@ -564,104 +546,13 @@ export class SceneEntities {
},
})
},
...this.mouseEnterLeaveCallbacks(),
...mouseEnterLeaveCallbacks(),
})
}
setupSketchIdleCallbacks = ({
pathToNode,
up,
forward,
position,
}: {
pathToNode: PathToNode
forward: [number, number, number]
up: [number, number, number]
position?: [number, number, number]
}) => {
let addingNewSegmentStatus: 'nothing' | 'pending' | 'added' = 'nothing'
setupSketchIdleCallbacks = (pathToNode: PathToNode) => {
sceneInfra.setCallbacks({
onDragEnd: async () => {
if (addingNewSegmentStatus !== 'nothing') {
await this.tearDownSketch({ removeAxis: false })
this.setupSketch({
sketchPathToNode: pathToNode,
maybeModdedAst: kclManager.ast,
up,
forward,
position,
})
// setting up the callbacks again resets value in closures
this.setupSketchIdleCallbacks({
pathToNode,
up,
forward,
position,
})
}
},
onDrag: async ({
selected,
intersectionPoint,
mouseEvent,
intersects,
}) => {
onDrag: ({ selected, intersectionPoint, mouseEvent, intersects }) => {
if (mouseEvent.which !== 1) return
const group = getParentGroup(selected, [EXTRA_SEGMENT_HANDLE])
if (group?.name === EXTRA_SEGMENT_HANDLE) {
const segGroup = getParentGroup(selected)
const pathToNode: PathToNode = segGroup?.userData?.pathToNode
const pathToNodeIndex = pathToNode.findIndex(
(x) => x[1] === 'PipeExpression'
)
const sketchGroup = sketchGroupFromPathToNode({
pathToNode,
ast: kclManager.ast,
programMemory: kclManager.programMemory,
})
const pipeIndex = pathToNode[pathToNodeIndex + 1][0] as number
if (addingNewSegmentStatus === 'nothing') {
const prevSegment = sketchGroup.value[pipeIndex - 2]
const mod = addNewSketchLn({
node: kclManager.ast,
programMemory: kclManager.programMemory,
to: [intersectionPoint.twoD.x, intersectionPoint.twoD.y],
from: [prevSegment.from[0], prevSegment.from[1]],
// TODO assuming it's always a straight segments being added
// as this is easiest, and we'll need to add "tabbing" behavior
// to support other segment types
fnName: 'line',
pathToNode: pathToNode,
spliceBetween: true,
})
addingNewSegmentStatus = 'pending'
await kclManager.executeAstMock(mod.modifiedAst, {
updates: 'code',
})
await this.tearDownSketch({ removeAxis: false })
this.setupSketch({
sketchPathToNode: pathToNode,
maybeModdedAst: kclManager.ast,
up,
forward,
position,
})
addingNewSegmentStatus = 'added'
} else if (addingNewSegmentStatus === 'added') {
const pathToNodeForNewSegment = pathToNode.slice(0, pathToNodeIndex)
pathToNodeForNewSegment.push([pipeIndex - 2, 'index'])
this.onDragSegment({
sketchPathToNode: pathToNodeForNewSegment,
object: selected,
intersection2d: intersectionPoint.twoD,
intersects,
})
}
return
}
this.onDragSegment({
object: selected,
intersection2d: intersectionPoint.twoD,
@ -686,7 +577,7 @@ export class SceneEntities {
if (!event) return
sceneInfra.modelingSend(event)
},
...this.mouseEnterLeaveCallbacks(),
...mouseEnterLeaveCallbacks(),
})
}
prepareTruncatedMemoryAndAst = (
@ -864,7 +755,8 @@ export class SceneEntities {
group.userData.to = to
group.userData.prevSegment = prevSegment
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE)
arrowGroup.position.set(to[0], to[1], 0)
const previousPoint =
prevSegment?.type === 'TangentialArcTo'
@ -882,49 +774,13 @@ export class SceneEntities {
obtuse: true,
})
const pxLength = arcInfo.arcLength / scale
const shouldHideIdle = pxLength < HIDE_SEGMENT_LENGTH
const shouldHideHover = pxLength < HIDE_HOVER_SEGMENT_LENGTH
const hoveredParent =
sceneInfra.hoveredObject &&
getParentGroup(sceneInfra.hoveredObject, [TANGENTIAL_ARC_TO_SEGMENT])
let isHandlesVisible = !shouldHideIdle
if (hoveredParent && hoveredParent?.uuid === group?.uuid) {
isHandlesVisible = !shouldHideHover
}
if (arrowGroup) {
arrowGroup.position.set(to[0], to[1], 0)
const arrowheadAngle =
arcInfo.endAngle + (Math.PI / 2) * (arcInfo.ccw ? 1 : -1)
arrowGroup.quaternion.setFromUnitVectors(
new Vector3(0, 1, 0),
new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0)
)
arrowGroup.scale.set(scale, scale, scale)
arrowGroup.visible = isHandlesVisible
}
if (extraSegmentGroup) {
const circumferenceInPx = (2 * Math.PI * arcInfo.radius) / scale
const extraSegmentAngleDelta =
(EXTRA_SEGMENT_OFFSET_PX / circumferenceInPx) * Math.PI * 2
const extraSegmentAngle =
arcInfo.startAngle + (arcInfo.ccw ? 1 : -1) * extraSegmentAngleDelta
const extraSegmentOffset = new Vector2(
Math.cos(extraSegmentAngle) * arcInfo.radius,
Math.sin(extraSegmentAngle) * arcInfo.radius
)
extraSegmentGroup.position.set(
arcInfo.center[0] + extraSegmentOffset.x,
arcInfo.center[1] + extraSegmentOffset.y,
0
)
extraSegmentGroup.scale.set(scale, scale, scale)
extraSegmentGroup.visible = isHandlesVisible
}
const arrowheadAngle =
arcInfo.endAngle + (Math.PI / 2) * (arcInfo.ccw ? 1 : -1)
arrowGroup.quaternion.setFromUnitVectors(
new Vector3(0, 1, 0),
new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0)
)
arrowGroup.scale.set(scale, scale, scale)
const tangentialArcToSegmentBody = group.children.find(
(child) => child.userData.type === TANGENTIAL_ARC_TO_SEGMENT_BODY
@ -971,26 +827,10 @@ export class SceneEntities {
group.userData.from = from
group.userData.to = to
const shape = new Shape()
shape.moveTo(0, (-SEGMENT_WIDTH_PX / 2) * scale) // The width of the line in px (2.4px in this case)
shape.lineTo(0, (SEGMENT_WIDTH_PX / 2) * scale)
shape.moveTo(0, -0.08 * scale)
shape.lineTo(0, 0.08 * scale) // The width of the line
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
const length = Math.sqrt(
Math.pow(to[0] - from[0], 2) + Math.pow(to[1] - from[1], 2)
)
const pxLength = length / scale
const shouldHideIdle = pxLength < HIDE_SEGMENT_LENGTH
const shouldHideHover = pxLength < HIDE_HOVER_SEGMENT_LENGTH
const hoveredParent =
sceneInfra.hoveredObject &&
getParentGroup(sceneInfra.hoveredObject, [STRAIGHT_SEGMENT])
let isHandlesVisible = !shouldHideIdle
if (hoveredParent && hoveredParent?.uuid === group?.uuid) {
isHandlesVisible = !shouldHideHover
}
if (arrowGroup) {
arrowGroup.position.set(to[0], to[1], 0)
@ -1002,21 +842,6 @@ export class SceneEntities {
.normalize()
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
arrowGroup.scale.set(scale, scale, scale)
arrowGroup.visible = isHandlesVisible
}
const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE)
if (extraSegmentGroup) {
const offsetFromBase = new Vector2(to[0] - from[0], to[1] - from[1])
.normalize()
.multiplyScalar(EXTRA_SEGMENT_OFFSET_PX * scale)
extraSegmentGroup.position.set(
from[0] + offsetFromBase.x,
from[1] + offsetFromBase.y,
0
)
extraSegmentGroup.scale.set(scale, scale, scale)
extraSegmentGroup.visible = isHandlesVisible
}
const straightSegmentBody = group.children.find(
@ -1194,119 +1019,6 @@ export class SceneEntities {
},
})
}
mouseEnterLeaveCallbacks() {
return {
onMouseEnter: ({ selected, dragSelected }: OnMouseEnterLeaveArgs) => {
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
const obj = selected as Mesh
const mat = obj.material as MeshBasicMaterial
mat.color.set(obj.userData.baseColor)
mat.color.offsetHSL(0, 0, 0.5)
}
const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
if (parent?.userData?.pathToNode) {
const updatedAst = parse(recast(kclManager.ast))
const node = getNodeFromPath<CallExpression>(
updatedAst,
parent.userData.pathToNode,
'CallExpression'
).node
sceneInfra.highlightCallback([node.start, node.end])
const yellow = 0xffff00
colorSegment(selected, yellow)
const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE)
if (extraSegmentGroup) {
extraSegmentGroup.traverse((child) => {
if (child instanceof Points || child instanceof Mesh) {
child.material.opacity = dragSelected ? 0 : 1
}
})
}
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
const factor =
(sceneInfra.camControls.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(sceneInfra.camControls.camera, parent)) /
sceneInfra._baseUnitMultiplier
if (parent.name === STRAIGHT_SEGMENT) {
this.updateStraightSegment({
from: parent.userData.from,
to: parent.userData.to,
group: parent,
scale: factor,
})
} else if (parent.name === TANGENTIAL_ARC_TO_SEGMENT) {
this.updateTangentialArcToSegment({
prevSegment: parent.userData.prevSegment,
from: parent.userData.from,
to: parent.userData.to,
group: parent,
scale: factor,
})
}
return
}
sceneInfra.highlightCallback([0, 0])
},
onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => {
sceneInfra.highlightCallback([0, 0])
const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
if (parent) {
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
const factor =
(sceneInfra.camControls.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(sceneInfra.camControls.camera, parent)) /
sceneInfra._baseUnitMultiplier
if (parent.name === STRAIGHT_SEGMENT) {
this.updateStraightSegment({
from: parent.userData.from,
to: parent.userData.to,
group: parent,
scale: factor,
})
} else if (parent.name === TANGENTIAL_ARC_TO_SEGMENT) {
this.updateTangentialArcToSegment({
prevSegment: parent.userData.prevSegment,
from: parent.userData.from,
to: parent.userData.to,
group: parent,
scale: factor,
})
}
}
const isSelected = parent?.userData?.isSelected
colorSegment(
selected,
isSelected ? 0x0000ff : parent?.userData?.baseColor || 0xffffff
)
const extraSegmentGroup = parent?.getObjectByName(EXTRA_SEGMENT_HANDLE)
if (extraSegmentGroup) {
extraSegmentGroup.traverse((child) => {
if (child instanceof Points || child instanceof Mesh) {
child.material.opacity = 0
}
})
}
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
const obj = selected as Mesh
const mat = obj.material as MeshBasicMaterial
mat.color.set(obj.userData.baseColor)
if (obj.userData.isSelected) mat.color.offsetHSL(0, 0, 0.2)
}
},
}
}
}
export type DefaultPlaneStr = 'XY' | 'XZ' | 'YZ' | '-XY' | '-XZ' | '-YZ'
@ -1448,7 +1160,7 @@ function colorSegment(object: any, color: number) {
])
if (straightSegmentBody) {
straightSegmentBody.traverse((child) => {
if (child instanceof Mesh && !child.userData.ignoreColorChange) {
if (child instanceof Mesh) {
child.material.color.set(color)
}
})
@ -1549,3 +1261,53 @@ function massageFormats(a: any): Vector3 {
? new Vector3(a[0], a[1], a[2])
: new Vector3(a.x, a.y, a.z)
}
function mouseEnterLeaveCallbacks() {
return {
onMouseEnter: ({ selected }: OnMouseEnterLeaveArgs) => {
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
const obj = selected as Mesh
const mat = obj.material as MeshBasicMaterial
mat.color.set(obj.userData.baseColor)
mat.color.offsetHSL(0, 0, 0.5)
}
const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
if (parent?.userData?.pathToNode) {
const updatedAst = parse(recast(kclManager.ast))
const node = getNodeFromPath<CallExpression>(
updatedAst,
parent.userData.pathToNode,
'CallExpression'
).node
sceneInfra.highlightCallback([node.start, node.end])
const yellow = 0xffff00
colorSegment(selected, yellow)
return
}
sceneInfra.highlightCallback([0, 0])
},
onMouseLeave: ({ selected }: OnMouseEnterLeaveArgs) => {
sceneInfra.highlightCallback([0, 0])
const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
PROFILE_START,
])
const isSelected = parent?.userData?.isSelected
colorSegment(
selected,
isSelected ? 0x0000ff : parent?.userData?.baseColor || 0xffffff
)
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
const obj = selected as Mesh
const mat = obj.material as MeshBasicMaterial
mat.color.set(obj.userData.baseColor)
if (obj.userData.isSelected) mat.color.offsetHSL(0, 0, 0.2)
}
},
}
}

View File

@ -18,8 +18,6 @@ import {
Intersection,
Object3D,
Object3DEventMap,
TextureLoader,
Texture,
} from 'three'
import { compareVec2Epsilon2 } from 'lang/std/sketch'
import { useModelingContext } from 'hooks/useModelingContext'
@ -27,10 +25,9 @@ import * as TWEEN from '@tweenjs/tween.js'
import { SourceRange } from 'lang/wasm'
import { Axis } from 'lib/selections'
import { type BaseUnit } from 'lib/settings/settingsTypes'
import { SETTINGS_PERSIST_KEY } from 'lib/constants'
import { CameraControls } from './CameraControls'
import { EngineCommandManager } from 'lang/std/engineConnection'
import { settings } from 'lib/settings/initialSettings'
import { MouseState } from 'machines/modelingMachine'
type SendType = ReturnType<typeof useModelingContext>['send']
@ -57,7 +54,6 @@ export const ARROWHEAD = 'arrowhead'
export interface OnMouseEnterLeaveArgs {
selected: Object3D<Object3DEventMap>
dragSelected?: Object3D<Object3DEventMap>
mouseEvent: MouseEvent
}
@ -102,26 +98,18 @@ export class SceneInfra {
isFovAnimationInProgress = false
_baseUnit: BaseUnit = 'mm'
_baseUnitMultiplier = 1
extraSegmentTexture: Texture
lastMouseState: MouseState = { type: 'idle' }
onDragStartCallback: (arg: OnDragCallbackArgs) => void = () => {}
onDragEndCallback: (arg: OnDragCallbackArgs) => void = () => {}
onDragCallback: (arg: OnDragCallbackArgs) => void = () => {}
onMoveCallback: (arg: OnMoveCallbackArgs) => void = () => {}
onClickCallback: (arg: OnClickCallbackArgs) => void = () => {}
onMouseEnter: (arg: OnMouseEnterLeaveArgs) => void = () => {}
onMouseLeave: (arg: OnMouseEnterLeaveArgs) => void = () => {}
setCallbacks = (callbacks: {
onDragStart?: (arg: OnDragCallbackArgs) => void
onDragEnd?: (arg: OnDragCallbackArgs) => void
onDrag?: (arg: OnDragCallbackArgs) => void
onMove?: (arg: OnMoveCallbackArgs) => void
onClick?: (arg: OnClickCallbackArgs) => void
onMouseEnter?: (arg: OnMouseEnterLeaveArgs) => void
onMouseLeave?: (arg: OnMouseEnterLeaveArgs) => void
}) => {
this.onDragStartCallback = callbacks.onDragStart || this.onDragStartCallback
this.onDragEndCallback = callbacks.onDragEnd || this.onDragEndCallback
this.onDragCallback = callbacks.onDrag || this.onDragCallback
this.onMoveCallback = callbacks.onMove || this.onMoveCallback
this.onClickCallback = callbacks.onClick || this.onClickCallback
@ -140,8 +128,6 @@ export class SceneInfra {
}
resetMouseListeners = () => {
this.setCallbacks({
onDragStart: () => {},
onDragEnd: () => {},
onDrag: () => {},
onMove: () => {},
onClick: () => {},
@ -184,7 +170,9 @@ export class SceneInfra {
// CAMERA
const camHeightDistanceRatio = 0.5
const baseUnit: BaseUnit = settings.modeling.defaultUnit.current
const baseUnit: BaseUnit =
JSON.parse(localStorage?.getItem(SETTINGS_PERSIST_KEY) || ('{}' as any))
.baseUnit || 'mm'
const baseRadius = 5.6
const length = baseUnitTomm(baseUnit) * baseRadius
const ang = Math.atan(camHeightDistanceRatio)
@ -224,13 +212,6 @@ export class SceneInfra {
const light = new AmbientLight(0x505050) // soft white light
this.scene.add(light)
const textureLoader = new TextureLoader()
this.extraSegmentTexture = textureLoader.load(
'/clientSideSceneAssets/extra-segment-texture.png'
)
this.extraSegmentTexture.anisotropy =
this.renderer?.capabilities?.getMaxAnisotropy?.()
SceneInfra.instance = this
}
@ -340,6 +321,8 @@ export class SceneInfra {
planeIntersectPoint.twoD &&
planeIntersectPoint.threeD
) {
// // console.log('onDrag', this.selected)
this.onDragCallback({
mouseEvent,
intersectionPoint: {
@ -349,10 +332,6 @@ export class SceneInfra {
intersects,
selected: this.selected.object,
})
this.updateMouseState({
type: 'isDragging',
on: this.selected.object,
})
}
} else if (
planeIntersectPoint &&
@ -372,34 +351,25 @@ export class SceneInfra {
if (intersects[0]) {
const firstIntersectObject = intersects[0].object
if (this.hoveredObject !== firstIntersectObject) {
const hoveredObj = this.hoveredObject
this.hoveredObject = null
this.onMouseLeave({
selected: hoveredObj,
mouseEvent: mouseEvent,
})
if (this.hoveredObject) {
this.onMouseLeave({
selected: this.hoveredObject,
mouseEvent: mouseEvent,
})
}
this.hoveredObject = firstIntersectObject
this.onMouseEnter({
selected: this.hoveredObject,
dragSelected: this.selected?.object,
mouseEvent: mouseEvent,
})
if (!this.selected)
this.updateMouseState({
type: 'isHovering',
on: this.hoveredObject,
})
}
} else {
if (this.hoveredObject) {
const hoveredObj = this.hoveredObject
this.hoveredObject = null
this.onMouseLeave({
selected: hoveredObj,
dragSelected: this.selected?.object,
selected: this.hoveredObject,
mouseEvent: mouseEvent,
})
if (!this.selected) this.updateMouseState({ type: 'idle' })
this.hoveredObject = null
}
}
}
@ -456,11 +426,6 @@ export class SceneInfra {
(a, b) => a.distance - b.distance
)
}
updateMouseState(mouseState: MouseState) {
if (this.lastMouseState.type === mouseState.type) return
this.lastMouseState = mouseState
this.modelingSend({ type: 'Set mouse state', data: mouseState })
}
onMouseDown = (event: MouseEvent) => {
this.currentMouseVector.x = (event.clientX / window.innerWidth) * 2 - 1
@ -490,26 +455,8 @@ export class SceneInfra {
if (this.selected) {
if (this.selected.hasBeenDragged) {
// TODO do the types properly here
this.onDragEndCallback({
intersectionPoint: {
twoD: planeIntersectPoint?.twoD as any,
threeD: planeIntersectPoint?.threeD as any,
},
intersects,
mouseEvent,
selected: this.selected as any,
})
if (intersects.length) {
this.updateMouseState({
type: 'isHovering',
on: intersects[0].object,
})
} else {
this.updateMouseState({
type: 'idle',
})
}
// this is where we could fire a onDragEnd event
// console.log('onDragEnd', this.selected)
} else if (planeIntersectPoint?.twoD && planeIntersectPoint?.threeD) {
// fire onClick event as there was no drags
this.onClickCallback({

View File

@ -12,22 +12,15 @@ import {
Mesh,
MeshBasicMaterial,
NormalBufferAttributes,
Points,
PointsMaterial,
Shape,
SphereGeometry,
Texture,
Vector2,
Vector3,
} from 'three'
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
import { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm'
import {
EXTRA_SEGMENT_HANDLE,
EXTRA_SEGMENT_OFFSET_PX,
HIDE_SEGMENT_LENGTH,
PROFILE_START,
SEGMENT_WIDTH_PX,
STRAIGHT_SEGMENT,
STRAIGHT_SEGMENT_BODY,
STRAIGHT_SEGMENT_DASH,
@ -51,7 +44,7 @@ export function profileStart({
}) {
const group = new Group()
const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later
const geometry = new BoxGeometry(0.8, 0.8, 0.8)
const body = new MeshBasicMaterial({ color: 0xffffff })
const mesh = new Mesh(geometry, body)
@ -78,7 +71,6 @@ export function straightSegment({
isDraftSegment,
scale = 1,
callExpName,
texture,
}: {
from: Coords2d
to: Coords2d
@ -87,13 +79,12 @@ export function straightSegment({
isDraftSegment?: boolean
scale?: number
callExpName: string
texture: Texture
}): Group {
const group = new Group()
const shape = new Shape()
shape.moveTo(0, (-SEGMENT_WIDTH_PX / 2) * scale)
shape.lineTo(0, (SEGMENT_WIDTH_PX / 2) * scale)
shape.moveTo(0, -0.08 * scale)
shape.lineTo(0, 0.08 * scale) // The width of the line
let geometry
if (isDraftSegment) {
@ -131,44 +122,24 @@ export function straightSegment({
}
group.name = STRAIGHT_SEGMENT
const length = Math.sqrt(
Math.pow(to[0] - from[0], 2) + Math.pow(to[1] - from[1], 2)
)
const arrowGroup = createArrowhead(scale)
arrowGroup.position.set(to[0], to[1], 0)
const dir = new Vector3()
.subVectors(new Vector3(to[0], to[1], 0), new Vector3(from[0], from[1], 0))
.normalize()
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
const pxLength = length / scale
const shouldHide = pxLength < HIDE_SEGMENT_LENGTH
arrowGroup.visible = !shouldHide
group.add(mesh)
if (callExpName !== 'close') group.add(arrowGroup)
const extraSegmentGroup = createExtraSegmentHandle(scale, texture)
const offsetFromBase = new Vector2(to[0] - from[0], to[1] - from[1])
.normalize()
.multiplyScalar(EXTRA_SEGMENT_OFFSET_PX * scale)
extraSegmentGroup.position.set(
from[0] + offsetFromBase.x,
from[1] + offsetFromBase.y,
0
)
extraSegmentGroup.visible = !shouldHide
group.add(extraSegmentGroup)
return group
}
function createArrowhead(scale = 1): Group {
const arrowMaterial = new MeshBasicMaterial({ color: 0xffffff })
// specify the size of the geometry in pixels (i.e. cone height = 20px, cone radius = 4.5px)
// we'll scale the group to the correct size later to match these sizes in screen space
const arrowheadMesh = new Mesh(new ConeGeometry(4.5, 20, 12), arrowMaterial)
arrowheadMesh.position.set(0, -9, 0)
const sphereMesh = new Mesh(new SphereGeometry(4, 12, 12), arrowMaterial)
const arrowheadMesh = new Mesh(new ConeGeometry(0.31, 1.5, 12), arrowMaterial)
arrowheadMesh.position.set(0, -0.6, 0)
const sphereMesh = new Mesh(new SphereGeometry(0.27, 12, 12), arrowMaterial)
const arrowGroup = new Group()
arrowGroup.userData.type = ARROWHEAD
@ -179,36 +150,6 @@ function createArrowhead(scale = 1): Group {
return arrowGroup
}
function createExtraSegmentHandle(scale: number, texture: Texture): Group {
const particleMaterial = new PointsMaterial({
size: 12, // in pixels
map: texture,
transparent: true,
opacity: 0,
depthTest: false,
})
const mat = new MeshBasicMaterial({
transparent: true,
color: 0xffffff,
opacity: 0,
})
const particleGeometry = new BufferGeometry().setFromPoints([
new Vector3(0, 0, 0),
])
const sphereMesh = new Mesh(new SphereGeometry(6, 12, 12), mat) // sphere radius in pixels
const particle = new Points(particleGeometry, particleMaterial)
particle.userData.ignoreColorChange = true
particle.userData.type = EXTRA_SEGMENT_HANDLE
const extraSegmentGroup = new Group()
extraSegmentGroup.userData.type = EXTRA_SEGMENT_HANDLE
extraSegmentGroup.name = EXTRA_SEGMENT_HANDLE
extraSegmentGroup.add(sphereMesh)
extraSegmentGroup.add(particle)
extraSegmentGroup.scale.set(scale, scale, scale)
return extraSegmentGroup
}
export function tangentialArcToSegment({
prevSegment,
from,
@ -217,7 +158,6 @@ export function tangentialArcToSegment({
pathToNode,
isDraftSegment,
scale = 1,
texture,
}: {
prevSegment: SketchGroup['value'][number]
from: Coords2d
@ -226,7 +166,6 @@ export function tangentialArcToSegment({
pathToNode: PathToNode
isDraftSegment?: boolean
scale?: number
texture: Texture
}): Group {
const group = new Group()
@ -239,13 +178,12 @@ export function tangentialArcToSegment({
)
: prevSegment.from
const { center, radius, startAngle, endAngle, ccw, arcLength } =
getTangentialArcToInfo({
arcStartPoint: from,
arcEndPoint: to,
tanPreviousPoint: previousPoint,
obtuse: true,
})
const { center, radius, startAngle, endAngle, ccw } = getTangentialArcToInfo({
arcStartPoint: from,
arcEndPoint: to,
tanPreviousPoint: previousPoint,
obtuse: true,
})
const geometry = createArcGeometry({
center,
@ -281,28 +219,8 @@ export function tangentialArcToSegment({
new Vector3(0, 1, 0),
new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0)
)
const pxLength = arcLength / scale
const shouldHide = pxLength < HIDE_SEGMENT_LENGTH
arrowGroup.visible = !shouldHide
const extraSegmentGroup = createExtraSegmentHandle(scale, texture)
const circumferenceInPx = (2 * Math.PI * radius) / scale
const extraSegmentAngleDelta =
(EXTRA_SEGMENT_OFFSET_PX / circumferenceInPx) * Math.PI * 2
const extraSegmentAngle = startAngle + (ccw ? 1 : -1) * extraSegmentAngleDelta
const extraSegmentOffset = new Vector2(
Math.cos(extraSegmentAngle) * radius,
Math.sin(extraSegmentAngle) * radius
)
extraSegmentGroup.position.set(
center[0] + extraSegmentOffset.x,
center[1] + extraSegmentOffset.y,
0
)
extraSegmentGroup.visible = !shouldHide
group.add(mesh, arrowGroup, extraSegmentGroup)
group.add(mesh, arrowGroup)
return group
}
@ -324,8 +242,8 @@ export function createArcGeometry({
isDashed?: boolean
scale?: number
}): BufferGeometry {
const dashSizePx = 18 * scale
const gapSizePx = 18 * scale
const dashSize = 1.2 * scale
const gapSize = 1.2 * scale
const arcStart = new EllipseCurve(
center[0],
center[1],
@ -347,8 +265,8 @@ export function createArcGeometry({
0
)
const shape = new Shape()
shape.moveTo(0, (-SEGMENT_WIDTH_PX / 2) * scale)
shape.lineTo(0, (SEGMENT_WIDTH_PX / 2) * scale) // The width of the line
shape.moveTo(0, -0.08 * scale)
shape.lineTo(0, 0.08 * scale) // The width of the line
if (!isDashed) {
const points = arcStart.getPoints(50)
@ -363,7 +281,7 @@ export function createArcGeometry({
}
const length = arcStart.getLength()
const totalDashes = length / (dashSizePx + gapSizePx) // rounding makes the dashes jittery since the new dash is suddenly appears instead of growing into place
const totalDashes = length / (dashSize + gapSize) // rounding makes the dashes jittery since the new dash is suddenly appears instead of growing into place
const dashesAtEachEnd = Math.min(100, totalDashes / 2) // Assuming we want 50 dashes total, 25 at each end
const dashGeometries = []
@ -371,8 +289,8 @@ export function createArcGeometry({
// Function to create a dash at a specific t value (0 to 1 along the curve)
const createDashAt = (t: number, curve: EllipseCurve) => {
const startVec = curve.getPoint(t)
const endVec = curve.getPoint(Math.min(0.5, t + dashSizePx / length))
const midVec = curve.getPoint(Math.min(0.5, t + dashSizePx / length / 2))
const endVec = curve.getPoint(Math.min(0.5, t + dashSize / length))
const midVec = curve.getPoint(Math.min(0.5, t + dashSize / length / 2))
const dashCurve = new CurvePath<Vector3>()
dashCurve.add(
new CatmullRomCurve3([
@ -396,8 +314,7 @@ export function createArcGeometry({
}
// fill in the remaining arc
const remainingArcLength =
length - dashesAtEachEnd * 2 * (dashSizePx + gapSizePx)
const remainingArcLength = length - dashesAtEachEnd * 2 * (dashSize + gapSize)
if (remainingArcLength > 0) {
const remainingArcStartT = dashesAtEachEnd / totalDashes
const remainingArcEndT = 1 - remainingArcStartT
@ -442,8 +359,8 @@ export function dashedStraight(
shape: Shape,
scale = 1
): BufferGeometry<NormalBufferAttributes> {
const dashSize = 18 * scale
const gapSize = 18 * scale // TODO: gapSize is not respected
const dashSize = 1.2 * scale
const gapSize = 1.2 * scale // todo: gabSize is not respected
const dashLine = new LineCurve3(
new Vector3(from[0], from[1], 0),
new Vector3(to[0], to[1], 0)

View File

@ -1,21 +1,11 @@
:root {
--primary-hue: 264.48;
--primary-chroma: 0.2167;
--primary-lightness: 60%;
--_primary: var(--primary-lightness) var(--primary-chroma)
var(--primary-hue, 264.48);
--primary: oklch(
var(--primary-lightness) var(--primary-chroma) var(--primary-hue, 264.48) /
var(--opacity, 1)
);
/*
Generated using Catmosphere Theme Builder
by KittyCAD
https://catmosphere-theme-builder.vercel.app/?colors=%5B%7B%22from%22:%7B%22l%22:1,%22c%22:0.01,%22h%22:78%7D,%22to%22:%7B%22l%22:0.065,%22c%22:0.05,%22h%22:182.6%7D,%22stops%22:10,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.45,%22h%22:122.4%7D,%22to%22:%7B%22l%22:0.13,%22c%22:0.031,%22h%22:137.2%7D,%22stops%22:10,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.13,%22h%22:176%7D,%22to%22:%7B%22l%22:0.116,%22c%22:0.097,%22h%22:213.1%7D,%22stops%22:10,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.169,%22h%22:144.4%7D,%22to%22:%7B%22l%22:0.12,%22c%22:0.45,%22h%22:132.7%7D,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.087,%22h%22:261.6%7D,%22to%22:%7B%22l%22:0.22,%22c%22:0.084,%22h%22:275.5%7D,%22steps%22:12,%22uuid%22:%227tpx9pf1zd6%22%7D,%7B%22from%22:%7B%22l%22:0.954,%22c%22:0.108,%22h%22:280.6%7D,%22to%22:%7B%22l%22:0.166,%22c%22:0.188,%22h%22:263.8%7D,%22steps%22:12,%22uuid%22:%22vu652mebd3%22%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.115,%22h%22:0%7D,%22to%22:%7B%22l%22:0.096,%22c%22:0.261,%22h%22:302%7D,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.185,%22h%22:19.8%7D,%22to%22:%7B%22l%22:0.368,%22c%22:0.45,%22h%22:9.4%7D,%22steps%22:8,%22uuid%22:%22g05inkd34l%22%7D,%7B%22from%22:%7B%22l%22:0.912,%22c%22:0.139,%22h%22:87%7D,%22to%22:%7B%22l%22:0.502,%22c%22:0.45,%22h%22:97.7%7D,%22steps%22:8,%22uuid%22:%22l892hcw4ef%22%7D,%7B%22from%22:%7B%22l%22:0.89,%22c%22:0.16,%22h%22:143.4%7D,%22to%22:%7B%22l%22:0.466,%22c%22:0.208,%22h%22:147.7%7D,%22steps%22:8,%22uuid%22:%22hkd09y9ov4h%22%7D%5D
*/
/* Chalkboard */
--chalkboard-10: oklch(99.9% 0.003766 102.8deg);
--chalkboard-10: oklch(99.7% 0.008766 102.8deg);
--chalkboard-20: oklch(91.34% 0.009353 109deg);
--chalkboard-30: oklch(82.99% 0.00994 115.2deg);
--chalkboard-40: oklch(74.63% 0.01053 121.4deg);

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