Compare commits
44 Commits
v0.17.1
...
derive-doc
Author | SHA1 | Date | |
---|---|---|---|
f5ee346408 | |||
544a7565e3 | |||
979046f7e6 | |||
07ae5106b9 | |||
e9ae484332 | |||
2a86ffc09a | |||
93903a8a47 | |||
45e85a1f81 | |||
c187989d18 | |||
47b5fa1459 | |||
d85781ef99 | |||
233f81a879 | |||
8ac0bf4953 | |||
24caeece65 | |||
f493cf11a0 | |||
594e888c12 | |||
b32295e1d9 | |||
e0838c1198 | |||
f03f34d8be | |||
108bb4ee90 | |||
092d459026 | |||
c4f7296e32 | |||
1cbd422d7f | |||
849685a986 | |||
359b3c1f35 | |||
f4ff5e43f2 | |||
daf7350c9e | |||
a829cdb006 | |||
1a7a19ee85 | |||
b045a89854 | |||
221f037eaa | |||
a93b72f7e1 | |||
63f36cbcbf | |||
79b50ef7d4 | |||
3d16dcd30d | |||
d605d4a029 | |||
77f51530f9 | |||
76480f1a43 | |||
f850f80de1 | |||
15ebbe6947 | |||
01beba42da | |||
509e372ed2 | |||
b0417114af | |||
0360a4021b |
@ -1,3 +1,3 @@
|
|||||||
[codespell]
|
[codespell]
|
||||||
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey
|
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast
|
||||||
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md
|
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,./src-tauri/gen/schemas
|
||||||
|
@ -7,23 +7,23 @@ on:
|
|||||||
- '**/Cargo.toml'
|
- '**/Cargo.toml'
|
||||||
- '**/Cargo.lock'
|
- '**/Cargo.lock'
|
||||||
- '**/rust-toolchain.toml'
|
- '**/rust-toolchain.toml'
|
||||||
- .github/workflows/cargo-criterion.yml
|
- .github/workflows/cargo-bench.yml
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- '**.rs'
|
- '**.rs'
|
||||||
- '**/Cargo.toml'
|
- '**/Cargo.toml'
|
||||||
- '**/Cargo.lock'
|
- '**/Cargo.lock'
|
||||||
- '**/rust-toolchain.toml'
|
- '**/rust-toolchain.toml'
|
||||||
- .github/workflows/cargo-criterion.yml
|
- .github/workflows/cargo-bench.yml
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
permissions: read-all
|
permissions: read-all
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
name: cargo criterion
|
name: cargo bench
|
||||||
jobs:
|
jobs:
|
||||||
cargocriterion:
|
cargo-bench:
|
||||||
name: cargo criterion
|
name: Benchmark with iai
|
||||||
runs-on: ubuntu-latest-8-cores
|
runs-on: ubuntu-latest-8-cores
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@ -31,10 +31,12 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
cargo install cargo-criterion
|
cargo install cargo-criterion
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y valgrind
|
||||||
- name: Rust Cache
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2.6.1
|
uses: Swatinem/rust-cache@v2.6.1
|
||||||
- name: Benchmark kcl library
|
- name: Benchmark kcl library
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |-
|
run: |-
|
||||||
cd src/wasm-lib/kcl; cargo criterion
|
cd src/wasm-lib/kcl; cargo bench -- iai
|
||||||
|
|
26
.github/workflows/ci.yml
vendored
@ -125,6 +125,9 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-14, ubuntu-latest, windows-latest]
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
@ -144,10 +147,12 @@ jobs:
|
|||||||
sudo apt-get update &&
|
sudo apt-get update &&
|
||||||
sudo apt-get install -y
|
sudo apt-get install -y
|
||||||
libgtk-3-dev
|
libgtk-3-dev
|
||||||
libgtksourceview-3.0-dev
|
libayatana-appindicator3-dev
|
||||||
webkit2gtk-4.0
|
|
||||||
libappindicator3-dev
|
|
||||||
webkit2gtk-driver
|
webkit2gtk-driver
|
||||||
|
libsoup-3.0-dev
|
||||||
|
libjavascriptcoregtk-4.1-dev
|
||||||
|
libwebkit2gtk-4.1-dev
|
||||||
|
at-spi2-core
|
||||||
xvfb
|
xvfb
|
||||||
|
|
||||||
- name: Sync node version and setup cache
|
- name: Sync node version and setup cache
|
||||||
@ -161,7 +166,9 @@ jobs:
|
|||||||
- name: Setup Rust
|
- name: Setup Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
# TODO: re-enable for Windows builds, see https://github.com/tauri-apps/tauri/issues/9045
|
||||||
- name: Setup Rust cache
|
- name: Setup Rust cache
|
||||||
|
if: matrix.os != 'windows-latest'
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: './src-tauri -> target'
|
workspaces: './src-tauri -> target'
|
||||||
@ -224,14 +231,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
includeRelease: false
|
includeRelease: false
|
||||||
includeDebug: true
|
includeDebug: true
|
||||||
args: ${{ matrix.os == 'macos-14' && '--target universal-apple-darwin' || '' }}
|
args: "${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
|
||||||
|
|
||||||
- name: Build the app (release) and sign
|
- name: Build the app (release) and sign
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
if: ${{ env.BUILD_RELEASE == 'true' }}
|
if: ${{ env.BUILD_RELEASE == 'true' }}
|
||||||
env:
|
env:
|
||||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||||
@ -240,7 +247,7 @@ jobs:
|
|||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
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' }}"
|
TAURI_CONF_ARGS: "--config ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}"
|
||||||
with:
|
with:
|
||||||
args: "${{ matrix.os == 'macos-14' && '--target universal-apple-darwin' || '' }} ${{ env.TAURI_CONF_ARGS }}"
|
args: "${{ env.TAURI_CONF_ARGS }} ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
if: matrix.os != 'ubuntu-latest'
|
if: matrix.os != 'ubuntu-latest'
|
||||||
@ -250,10 +257,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: "${{ env.PREFIX }}/${{ env.MODE }}/bundle/*/*"
|
path: "${{ env.PREFIX }}/${{ env.MODE }}/bundle/*/*"
|
||||||
|
|
||||||
|
# TODO: re-enable linux e2e tests when possible
|
||||||
- name: Run e2e tests (linux only)
|
- name: Run e2e tests (linux only)
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: false
|
||||||
run: |
|
run: |
|
||||||
cargo install tauri-driver@0.1.3
|
cargo install tauri-driver
|
||||||
source .env.${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
|
source .env.${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
|
||||||
export VITE_KC_API_BASE_URL
|
export VITE_KC_API_BASE_URL
|
||||||
xvfb-run yarn test:e2e:tauri
|
xvfb-run yarn test:e2e:tauri
|
||||||
|
1
.gitignore
vendored
@ -51,5 +51,6 @@ e2e/playwright/export-snapshots/*
|
|||||||
|
|
||||||
## generated files
|
## generated files
|
||||||
src/**/*.typegen.ts
|
src/**/*.typegen.ts
|
||||||
|
src-tauri/gen
|
||||||
|
|
||||||
src/wasm-lib/grackle/stdlib_cube_partial.json
|
src/wasm-lib/grackle/stdlib_cube_partial.json
|
||||||
|
@ -281,7 +281,7 @@ https://github.com/KittyCAD/modeling-app/assets/29681384/6f5e8e85-1003-4fd9-be7f
|
|||||||
<details>
|
<details>
|
||||||
|
|
||||||
<summary>
|
<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>
|
</summary>
|
||||||
|
|
||||||
```JSON
|
```JSON
|
||||||
|
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 224 KiB |
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 224 KiB |
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 224 KiB |
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 165 KiB |
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 165 KiB |
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 165 KiB |
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 165 KiB |
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 224 KiB |
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 165 KiB |
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 165 KiB |
@ -1,10 +1,11 @@
|
|||||||
import { test, expect } from '@playwright/test'
|
import { test, expect } from '@playwright/test'
|
||||||
import { secrets } from './secrets'
|
|
||||||
import { getUtils } from './test-utils'
|
import { getUtils } from './test-utils'
|
||||||
import waitOn from 'wait-on'
|
import waitOn from 'wait-on'
|
||||||
import { Themes } from '../../src/lib/theme'
|
|
||||||
import { initialSettings } from '../../src/lib/settings/initialSettings'
|
|
||||||
import { roundOff } from 'lib/utils'
|
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
|
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
|
||||||
@ -30,31 +31,14 @@ test.beforeEach(async ({ context, page }) => {
|
|||||||
resources: ['tcp:3000'],
|
resources: ['tcp:3000'],
|
||||||
timeout: 5000,
|
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
|
// kill animations, speeds up tests and reduced flakiness
|
||||||
await page.emulateMedia({ reducedMotion: 'reduce' })
|
await page.emulateMedia({ reducedMotion: 'reduce' })
|
||||||
})
|
})
|
||||||
|
|
||||||
test.setTimeout(60000)
|
test.setTimeout(60000)
|
||||||
|
|
||||||
test('Basic sketch', async ({ page }) => {
|
test('Basic sketch', async ({ page, context }) => {
|
||||||
const u = getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
@ -529,96 +513,133 @@ test('Auto complete works', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Stored settings validation test
|
// Stored settings validation test
|
||||||
test('Stored settings are validated and fall back to defaults', async ({
|
test.describe('Settings persistence and validation tests', () => {
|
||||||
page,
|
// Override test setup
|
||||||
context,
|
|
||||||
}) => {
|
|
||||||
// Override beforeEach test setup
|
|
||||||
// with corrupted settings
|
// with corrupted settings
|
||||||
await context.addInitScript(async () => {
|
const storageState = structuredClone(basicStorageState)
|
||||||
const storedSettings = JSON.parse(
|
const s = TOML.parse(storageState.origins[0].localStorage[2].value) as {
|
||||||
localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}'
|
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)
|
||||||
|
|
||||||
// Corrupt the settings
|
test.use({ storageState })
|
||||||
storedSettings.baseUnit = 'invalid'
|
|
||||||
storedSettings.cameraControls = `() => alert('hack the planet')`
|
|
||||||
storedSettings.defaultDirectory = 123
|
|
||||||
storedSettings.defaultProjectName = false
|
|
||||||
|
|
||||||
localStorage.setItem('SETTINGS_PERSIST_KEY', JSON.stringify(storedSettings))
|
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)
|
||||||
})
|
})
|
||||||
|
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
test('Project settings can be set and override user settings', async ({
|
||||||
await page.goto('/', { waitUntil: 'domcontentloaded' })
|
page,
|
||||||
|
}) => {
|
||||||
|
const u = getUtils(page)
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
// Check the toast appeared
|
// Open the settings modal with the browser keyboard shortcut
|
||||||
await expect(
|
await page.keyboard.press('Meta+Shift+,')
|
||||||
page.getByText(`Error validating persisted settings:`, {
|
|
||||||
exact: false,
|
|
||||||
})
|
|
||||||
).toBeVisible()
|
|
||||||
|
|
||||||
// Check the settings were reset
|
await expect(
|
||||||
const storedSettings = JSON.parse(
|
page.getByRole('heading', { name: 'Settings', exact: true })
|
||||||
await page.evaluate(
|
).toBeVisible()
|
||||||
() => localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}'
|
await page
|
||||||
)
|
.locator('select[name="app-theme"]')
|
||||||
)
|
.selectOption({ value: 'light' })
|
||||||
await expect(storedSettings.baseUnit).toBe(initialSettings.baseUnit)
|
|
||||||
await expect(storedSettings.cameraControls).toBe(
|
// Verify the toast appeared
|
||||||
initialSettings.cameraControls
|
await expect(
|
||||||
)
|
page.getByText(`Set theme to "light" for this project`)
|
||||||
await expect(storedSettings.defaultDirectory).toBe(
|
).toBeVisible()
|
||||||
initialSettings.defaultDirectory
|
// Check that the theme changed
|
||||||
)
|
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
|
||||||
await expect(storedSettings.defaultProjectName).toBe(
|
|
||||||
initialSettings.defaultProjectName
|
// 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')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Onboarding tests
|
// Onboarding tests
|
||||||
test('Onboarding redirects and code updating', async ({ page, context }) => {
|
test.describe('Onboarding tests', () => {
|
||||||
const u = getUtils(page)
|
// 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 })
|
||||||
|
|
||||||
// Override beforeEach test setup
|
test('Onboarding redirects and code updating', async ({ page, context }) => {
|
||||||
await context.addInitScript(async () => {
|
const u = getUtils(page)
|
||||||
// Give some initial code, so we can test that it's cleared
|
|
||||||
localStorage.setItem('persistCode', 'const sigmaAllow = 15000')
|
|
||||||
|
|
||||||
const storedSettings = JSON.parse(
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}'
|
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`
|
||||||
)
|
)
|
||||||
storedSettings.onboardingStatus = '/export'
|
|
||||||
localStorage.setItem('SETTINGS_PERSIST_KEY', JSON.stringify(storedSettings))
|
// 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(/.+/)
|
||||||
})
|
})
|
||||||
|
|
||||||
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 }) => {
|
test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||||
@ -779,129 +800,134 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
|||||||
await selectionSequence()
|
await selectionSequence()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Command bar works and can change a setting', async ({ page }) => {
|
test.describe('Command bar tests', () => {
|
||||||
// Brief boilerplate
|
test('Command bar works and can change a setting', async ({ page }) => {
|
||||||
const u = getUtils(page)
|
// Brief boilerplate
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page.goto('/')
|
await page.goto('/', { waitUntil: 'domcontentloaded' })
|
||||||
await u.waitForAuthSkipAppStart()
|
|
||||||
|
|
||||||
let cmdSearchBar = page.getByPlaceholder('Search commands')
|
let cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||||
|
|
||||||
// First try opening the command bar and closing it
|
// First try opening the command bar and closing it
|
||||||
// It has a different label on mac and windows/linux, "Meta+K" and "Ctrl+/" respectively
|
// It has a different label on mac and windows/linux, "Meta+K" and "Ctrl+/" respectively
|
||||||
await page
|
await page
|
||||||
.getByRole('button', { name: 'Ctrl+/' })
|
.getByRole('button', { name: 'Ctrl+/' })
|
||||||
.or(page.getByRole('button', { name: '⌘K' }))
|
.or(page.getByRole('button', { name: '⌘K' }))
|
||||||
.click()
|
.click()
|
||||||
await expect(cmdSearchBar).toBeVisible()
|
await expect(cmdSearchBar).toBeVisible()
|
||||||
await page.keyboard.press('Escape')
|
await page.keyboard.press('Escape')
|
||||||
await expect(cmdSearchBar).not.toBeVisible()
|
await expect(cmdSearchBar).not.toBeVisible()
|
||||||
|
|
||||||
// Now try the same, but with the keyboard shortcut, check focus
|
// Now try the same, but with the keyboard shortcut, check focus
|
||||||
await page.keyboard.press('Meta+K')
|
await page.keyboard.press('Meta+K')
|
||||||
await expect(cmdSearchBar).toBeVisible()
|
await expect(cmdSearchBar).toBeVisible()
|
||||||
await expect(cmdSearchBar).toBeFocused()
|
await expect(cmdSearchBar).toBeFocused()
|
||||||
|
|
||||||
// Try typing in the command bar
|
// Try typing in the command bar
|
||||||
await page.keyboard.type('theme')
|
await page.keyboard.type('theme')
|
||||||
const themeOption = page.getByRole('option', { name: 'Set Theme' })
|
const themeOption = page.getByRole('option', {
|
||||||
await expect(themeOption).toBeVisible()
|
name: 'Settings · app · theme',
|
||||||
await themeOption.click()
|
})
|
||||||
const themeInput = page.getByPlaceholder('system')
|
await expect(themeOption).toBeVisible()
|
||||||
await expect(themeInput).toBeVisible()
|
await themeOption.click()
|
||||||
await expect(themeInput).toBeFocused()
|
const themeInput = page.getByPlaceholder('Select an option')
|
||||||
// Select dark theme
|
await expect(themeInput).toBeVisible()
|
||||||
await page.keyboard.press('ArrowDown')
|
await expect(themeInput).toBeFocused()
|
||||||
await page.keyboard.press('ArrowUp')
|
// Select dark theme
|
||||||
await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute(
|
await page.keyboard.press('ArrowDown')
|
||||||
'data-headlessui-state',
|
await page.keyboard.press('ArrowDown')
|
||||||
'active'
|
await page.keyboard.press('ArrowDown')
|
||||||
)
|
await expect(page.getByRole('option', { name: 'system' })).toHaveAttribute(
|
||||||
await page.keyboard.press('Enter')
|
'data-headlessui-state',
|
||||||
|
'active'
|
||||||
// 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`)
|
||||||
})
|
})
|
||||||
|
|
||||||
const u = getUtils(page)
|
// Override test setup code
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
const storageState = structuredClone(basicStorageState)
|
||||||
await page.goto('/')
|
storageState.origins[0].localStorage[1].value = `const distance = sqrt(20)
|
||||||
await u.waitForAuthSkipAppStart()
|
const part001 = startSketchOn('-XZ')
|
||||||
await u.openDebugPanel()
|
|> startProfileAt([-6.95, 4.98], %)
|
||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
|> line([25.1, 0.41], %)
|
||||||
|
|> line([0.73, -14.93], %)
|
||||||
|
|> line([-23.44, 0.52], %)
|
||||||
|
|> close(%)
|
||||||
|
`
|
||||||
|
test.use({ storageState })
|
||||||
|
|
||||||
let cmdSearchBar = page.getByPlaceholder('Search commands')
|
test('Can extrude from the command bar', async ({ page, context }) => {
|
||||||
await page.keyboard.press('Meta+K')
|
const u = getUtils(page)
|
||||||
await expect(cmdSearchBar).toBeVisible()
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
// Search for extrude command and choose it
|
// Make sure the stream is up
|
||||||
await page.getByRole('option', { name: 'Extrude' }).click()
|
await u.openDebugPanel()
|
||||||
await expect(page.locator('#arg-form > label')).toContainText(
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
'Please select one face'
|
await u.closeDebugPanel()
|
||||||
)
|
|
||||||
await expect(page.getByRole('button', { name: 'selection' })).toBeDisabled()
|
|
||||||
|
|
||||||
// Click to select face and set distance
|
await expect(
|
||||||
await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click()
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
await page.getByRole('button', { name: 'Continue' }).click()
|
).not.toBeDisabled()
|
||||||
|
await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click()
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Extrude' })
|
||||||
|
).not.toBeDisabled()
|
||||||
|
|
||||||
// Assert that we're on the distance step
|
let cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||||
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled()
|
await page.keyboard.press('Meta+K')
|
||||||
|
await expect(cmdSearchBar).toBeVisible()
|
||||||
|
|
||||||
// Assert that the an alternative variable name is chosen,
|
// Search for extrude command and choose it
|
||||||
// since the default variable name is already in use (distance)
|
await page.getByRole('option', { name: 'Extrude' }).click()
|
||||||
await page.getByRole('button', { name: 'Create new variable' }).click()
|
|
||||||
await expect(page.getByPlaceholder('Variable name')).toHaveValue(
|
|
||||||
'distance001'
|
|
||||||
)
|
|
||||||
await expect(page.getByRole('button', { name: 'Continue' })).toBeEnabled()
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click()
|
|
||||||
|
|
||||||
// Review step and argument hotkeys
|
// Assert that we're on the distance step
|
||||||
await expect(
|
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled()
|
||||||
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()
|
// 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()
|
||||||
|
|
||||||
// Check that the code was updated
|
// Review step and argument hotkeys
|
||||||
await page.keyboard.press('Enter')
|
await expect(
|
||||||
// Unfortunately this indentation seems to matter for the test
|
page.getByRole('button', { name: 'Submit command' })
|
||||||
await expect(page.locator('.cm-content')).toHaveText(
|
).toBeEnabled()
|
||||||
`const distance = sqrt(20)
|
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)
|
||||||
const distance001 = 5 + 7
|
const distance001 = 5 + 7
|
||||||
const part001 = startSketchOn('-XZ')
|
const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt([-6.95, 4.98], %)
|
|> startProfileAt([-6.95, 4.98], %)
|
||||||
|> line([25.1, 0.41], %)
|
|> line([25.1, 0.41], %)
|
||||||
|> line([0.73, -14.93], %)
|
|> line([0.73, -14.93], %)
|
||||||
|> line([-23.44, 0.52], %)
|
|> line([-23.44, 0.52], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(distance001, %)`.replace(/(\r\n|\n|\r)/gm, '') // remove newlines
|
|> extrude(distance001, %)`.replace(/(\r\n|\n|\r)/gm, '') // remove newlines
|
||||||
)
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Can add multiple sketches', async ({ page }) => {
|
test('Can add multiple sketches', async ({ page }) => {
|
||||||
@ -1470,9 +1496,13 @@ test('Sketch on face', async ({ page, context }) => {
|
|||||||
await page.getByText('startProfileAt([1.03, 1.03], %)').click()
|
await page.getByText('startProfileAt([1.03, 1.03], %)').click()
|
||||||
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
|
||||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
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)
|
await page.waitForTimeout(200)
|
||||||
|
|
||||||
const pointToDragFirst = [691, 237]
|
const pointToDragFirst = [787, 565]
|
||||||
await page.mouse.move(pointToDragFirst[0], pointToDragFirst[1])
|
await page.mouse.move(pointToDragFirst[0], pointToDragFirst[1])
|
||||||
await page.mouse.down()
|
await page.mouse.down()
|
||||||
await page.mouse.move(pointToDragFirst[0] - 20, pointToDragFirst[1], {
|
await page.mouse.move(pointToDragFirst[0] - 20, pointToDragFirst[1], {
|
||||||
@ -1486,7 +1516,9 @@ test('Sketch on face', async ({ page, context }) => {
|
|||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|
||||||
|> startProfileAt([1.03, 1.03], %)
|
|> startProfileAt([1.03, 1.03], %)
|
||||||
|> line([2.81, -0.33], %)
|
|> line([${process?.env?.CI ? 2.74 : 2.93}, -${
|
||||||
|
process?.env?.CI ? 0.24 : 0.2
|
||||||
|
}], %)
|
||||||
|> line([-4.44, -2.13], %)
|
|> line([-4.44, -2.13], %)
|
||||||
|> close(%)`)
|
|> close(%)`)
|
||||||
|
|
||||||
@ -1501,6 +1533,7 @@ test('Sketch on face', async ({ page, context }) => {
|
|||||||
await page.getByRole('button', { name: 'Extrude' }).click()
|
await page.getByRole('button', { name: 'Extrude' }).click()
|
||||||
|
|
||||||
await expect(page.getByTestId('command-bar')).toBeVisible()
|
await expect(page.getByTestId('command-bar')).toBeVisible()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
await expect(page.getByText('Confirm Extrude')).toBeVisible()
|
await expect(page.getByText('Confirm Extrude')).toBeVisible()
|
||||||
@ -1509,7 +1542,9 @@ test('Sketch on face', async ({ page, context }) => {
|
|||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|
||||||
|> startProfileAt([1.03, 1.03], %)
|
|> startProfileAt([1.03, 1.03], %)
|
||||||
|> line([2.81, -0.33], %)
|
|> line([${process?.env?.CI ? 2.74 : 2.93}, -${
|
||||||
|
process?.env?.CI ? 0.24 : 0.2
|
||||||
|
}], %)
|
||||||
|> line([-4.44, -2.13], %)
|
|> line([-4.44, -2.13], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(5 + 7, %)`)
|
|> extrude(5 + 7, %)`)
|
||||||
|
@ -7,30 +7,18 @@ import { spawn } from 'child_process'
|
|||||||
import { APP_NAME } from 'lib/constants'
|
import { APP_NAME } from 'lib/constants'
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import { basicSettings, basicStorageState } from './storageStates'
|
||||||
|
import * as TOML from '@iarna/toml'
|
||||||
|
|
||||||
test.beforeEach(async ({ context, page }) => {
|
test.beforeEach(async ({ 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
|
// reducedMotion kills animations, which speeds up tests and reduces flakiness
|
||||||
await page.emulateMedia({ reducedMotion: 'reduce' })
|
await page.emulateMedia({ reducedMotion: 'reduce' })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test.use({
|
||||||
|
storageState: structuredClone(basicStorageState),
|
||||||
|
})
|
||||||
|
|
||||||
test.setTimeout(60_000)
|
test.setTimeout(60_000)
|
||||||
|
|
||||||
test('exports of each format should work', async ({ page, context }) => {
|
test('exports of each format should work', async ({ page, context }) => {
|
||||||
@ -332,6 +320,22 @@ test('extrude on each default plane should be stable', async ({
|
|||||||
page,
|
page,
|
||||||
context,
|
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 u = getUtils(page)
|
||||||
const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}')
|
const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}')
|
||||||
|> startProfileAt([7.00, 4.40], %)
|
|> startProfileAt([7.00, 4.40], %)
|
||||||
@ -353,29 +357,26 @@ test('extrude on each default plane should be stable', async ({
|
|||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
await u.clearAndCloseDebugPanel()
|
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') => {
|
const runSnapshotsForOtherPlanes = async (plane = 'XY') => {
|
||||||
// clear code
|
// clear code
|
||||||
await u.removeCurrentCode()
|
await u.removeCurrentCode()
|
||||||
// add makeCode('XZ')
|
// add makeCode('XZ')
|
||||||
|
await u.openAndClearDebugPanel()
|
||||||
await page.locator('.cm-content').fill(makeCode(plane))
|
await page.locator('.cm-content').fill(makeCode(plane))
|
||||||
// wait for execution done
|
// wait for execution done
|
||||||
await u.openDebugPanel()
|
|
||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
await u.clearAndCloseDebugPanel()
|
await u.clearAndCloseDebugPanel()
|
||||||
|
|
||||||
await page.getByText('Code').click()
|
await page.getByText('Code').click()
|
||||||
|
await page.waitForTimeout(150)
|
||||||
await expect(page).toHaveScreenshot({
|
await expect(page).toHaveScreenshot({
|
||||||
maxDiffPixels: 100,
|
maxDiffPixels: 100,
|
||||||
})
|
})
|
||||||
await page.getByText('Code').click()
|
await page.getByText('Code').click()
|
||||||
}
|
}
|
||||||
|
await runSnapshotsForOtherPlanes('XY')
|
||||||
await runSnapshotsForOtherPlanes('-XY')
|
await runSnapshotsForOtherPlanes('-XY')
|
||||||
|
|
||||||
await runSnapshotsForOtherPlanes('XZ')
|
await runSnapshotsForOtherPlanes('XZ')
|
||||||
@ -386,22 +387,6 @@ test('extrude on each default plane should be stable', async ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('Draft segments should look right', async ({ page, context }) => {
|
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)
|
const u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
@ -460,26 +445,9 @@ 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,
|
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 u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
@ -512,7 +480,7 @@ test('Client side scene scale should match engine scale inch', async ({
|
|||||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt([9.06, -12.22], %)`)
|
|> startProfileAt([9.06, -12.22], %)`)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
@ -522,8 +490,8 @@ test('Client side scene scale should match engine scale inch', async ({
|
|||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt([9.06, -12.22], %)
|
|> startProfileAt([9.06, -12.22], %)
|
||||||
|> line([9.14, 0], %)`)
|
|> line([9.14, 0], %)`)
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
@ -532,9 +500,9 @@ test('Client side scene scale should match engine scale inch', async ({
|
|||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt([9.06, -12.22], %)
|
|> startProfileAt([9.06, -12.22], %)
|
||||||
|> line([9.14, 0], %)
|
|> line([9.14, 0], %)
|
||||||
|> tangentialArcTo([27.34, -3.08], %)`)
|
|> tangentialArcTo([27.34, -3.08], %)`)
|
||||||
|
|
||||||
// click tangential arc tool again to unequip it
|
// click tangential arc tool again to unequip it
|
||||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||||
@ -560,102 +528,101 @@ test('Client side scene scale should match engine scale inch', async ({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Client side scene scale should match engine scale mm', async ({
|
test.describe('Client side scene scale should match engine scale - Millimeters', () => {
|
||||||
page,
|
const storageState = structuredClone(basicStorageState)
|
||||||
context,
|
storageState.origins[0].localStorage[2].value = TOML.stringify({
|
||||||
}) => {
|
settings: {
|
||||||
await context.addInitScript(async () => {
|
...basicSettings,
|
||||||
localStorage.setItem(
|
modeling: {
|
||||||
'SETTINGS_PERSIST_KEY',
|
...basicSettings.modeling,
|
||||||
JSON.stringify({
|
defaultUnit: 'mm',
|
||||||
baseUnit: 'mm',
|
},
|
||||||
cameraControls: 'KittyCAD',
|
},
|
||||||
defaultDirectory: '',
|
})
|
||||||
defaultProjectName: 'project-$nnn',
|
test.use({
|
||||||
onboardingStatus: 'dismissed',
|
storageState,
|
||||||
showDebugPanel: true,
|
})
|
||||||
textWrapping: 'On',
|
|
||||||
theme: 'dark',
|
test('Millimeters', async ({ page }) => {
|
||||||
unitSystem: 'metric',
|
const u = getUtils(page)
|
||||||
})
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
await u.openDebugPanel()
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
|
).not.toBeDisabled()
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
|
).toBeVisible()
|
||||||
|
|
||||||
|
// click on "Start Sketch" button
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
await u.doAndWaitForImageDiff(
|
||||||
|
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
||||||
|
200
|
||||||
)
|
)
|
||||||
})
|
|
||||||
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(
|
// select a plane
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
await page.mouse.click(700, 200)
|
||||||
).not.toBeDisabled()
|
|
||||||
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
|
||||||
|
|
||||||
// click on "Start Sketch" button
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
await u.clearCommandLogs()
|
`const part001 = startSketchOn('-XZ')`
|
||||||
await u.doAndWaitForImageDiff(
|
)
|
||||||
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
|
||||||
200
|
|
||||||
)
|
|
||||||
|
|
||||||
// select a plane
|
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
|
||||||
await page.mouse.click(700, 200)
|
|
||||||
|
|
||||||
await expect(page.locator('.cm-content')).toHaveText(
|
const startXPx = 600
|
||||||
`const part001 = startSketchOn('-XZ')`
|
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 page.waitForTimeout(300) // TODO detect animation ending, or disable animation
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
const startXPx = 600
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
await page.waitForTimeout(100)
|
||||||
await expect(page.locator('.cm-content'))
|
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
|
||||||
|> startProfileAt([230.03, -310.32], %)`)
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
|
|
||||||
await u.closeDebugPanel()
|
await expect(page.locator('.cm-content'))
|
||||||
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt([230.03, -310.32], %)
|
||||||
|
|> line([232.2, 0], %)`)
|
||||||
|
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
|
||||||
|> startProfileAt([230.03, -310.32], %)
|
|
||||||
|> line([232.2, 0], %)`)
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
await expect(page.locator('.cm-content'))
|
||||||
await page.waitForTimeout(100)
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt([230.03, -310.32], %)
|
||||||
|
|> line([232.2, 0], %)
|
||||||
|
|> tangentialArcTo([694.43, -78.12], %)`)
|
||||||
|
|
||||||
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
|
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
// screen shot should show the sketch
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
await expect(page).toHaveScreenshot({
|
||||||
|> startProfileAt([230.03, -310.32], %)
|
maxDiffPixels: 100,
|
||||||
|> line([232.2, 0], %)
|
})
|
||||||
|> tangentialArcTo([694.43, -78.12], %)`)
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
// exit sketch
|
||||||
await page.waitForTimeout(100)
|
await u.openAndClearDebugPanel()
|
||||||
|
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||||
|
|
||||||
// screen shot should show the sketch
|
// wait for execution done
|
||||||
await expect(page).toHaveScreenshot({
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
maxDiffPixels: 100,
|
await u.clearAndCloseDebugPanel()
|
||||||
})
|
await page.waitForTimeout(200)
|
||||||
|
|
||||||
// exit sketch
|
// second screen shot should look almost identical, i.e. scale should be the same.
|
||||||
await u.openAndClearDebugPanel()
|
await expect(page).toHaveScreenshot({
|
||||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
maxDiffPixels: 100,
|
||||||
|
})
|
||||||
// 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,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -666,14 +633,14 @@ test('Sketch on face with none z-up', async ({ page, context }) => {
|
|||||||
'persistCode',
|
'persistCode',
|
||||||
`const part001 = startSketchOn('-XZ')
|
`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt([1.4, 2.47], %)
|
|> startProfileAt([1.4, 2.47], %)
|
||||||
|> line({ to: [9.31, 10.55], tag: 'seg01' }, %)
|
|> line([9.31, 10.55], %, 'seg01')
|
||||||
|> line([11.91, -10.42], %)
|
|> line([11.91, -10.42], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(5 + 7, %)
|
|> extrude(5 + 7, %)
|
||||||
const part002 = startSketchOn(part001, 'seg01')
|
const part002 = startSketchOn(part001, 'seg01')
|
||||||
|> startProfileAt([-2.89, 1.82], %)
|
|> startProfileAt([-2.89, 1.82], %)
|
||||||
|> line([4.68, 3.05], %)
|
|> line([4.68, 3.05], %)
|
||||||
|> line({ to: [0, -7.79], tag: 'seg02' }, %)
|
|> line([0, -7.79], %, 'seg02')
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(5 + 7, %)
|
|> extrude(5 + 7, %)
|
||||||
`
|
`
|
||||||
@ -700,6 +667,4 @@ const part002 = startSketchOn(part001, 'seg01')
|
|||||||
await expect(page).toHaveScreenshot({
|
await expect(page).toHaveScreenshot({
|
||||||
maxDiffPixels: 100,
|
maxDiffPixels: 100,
|
||||||
})
|
})
|
||||||
|
|
||||||
await page.waitForTimeout(200)
|
|
||||||
})
|
})
|
||||||
|
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 79 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 48 KiB |
40
e2e/playwright/storageStates.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
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 }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
@ -33,7 +33,7 @@ async function clearCommandLogs(page: Page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function expectCmdLog(page: Page, locatorStr: string) {
|
async function expectCmdLog(page: Page, locatorStr: string) {
|
||||||
await expect(page.locator(locatorStr)).toBeVisible()
|
await expect(page.locator(locatorStr).last()).toBeVisible()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForDefaultPlanesToBeVisible(page: Page) {
|
async function waitForDefaultPlanesToBeVisible(page: Page) {
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { browser, $, expect } from '@wdio/globals'
|
import { browser, $, expect } from '@wdio/globals'
|
||||||
import fs from 'fs/promises'
|
import fs from 'fs/promises'
|
||||||
|
|
||||||
const defaultDir = `${process.env.HOME}/Documents/zoo-modeling-app-projects`
|
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 userCodeDir = '/tmp/kittycad_user_code'
|
const userCodeDir = '/tmp/kittycad_user_code'
|
||||||
|
|
||||||
async function click(element: WebdriverIO.Element): Promise<void> {
|
async function click(element: WebdriverIO.Element): Promise<void> {
|
||||||
@ -10,12 +13,25 @@ async function click(element: WebdriverIO.Element): Promise<void> {
|
|||||||
await browser.execute('arguments[0].click();', element)
|
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)', () => {
|
describe('ZMA (Tauri, Linux)', () => {
|
||||||
it('opens the auth page and signs in', async () => {
|
it('opens the auth page and signs in', async () => {
|
||||||
// Clean up filesystem from previous tests
|
// Clean up filesystem from previous tests
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
await fs.rm(defaultDir, { force: true, recursive: true })
|
await fs.rm(defaultProjectDir, { force: true, recursive: true })
|
||||||
await fs.rm(userCodeDir, { force: 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"]')
|
const signInButton = await $('[data-testid="sign-in-button"]')
|
||||||
expect(await signInButton.getText()).toEqual('Sign in')
|
expect(await signInButton.getText()).toEqual('Sign in')
|
||||||
@ -65,13 +81,25 @@ describe('ZMA (Tauri, Linux)', () => {
|
|||||||
const settingsButton = await $('[data-testid="settings-button"]')
|
const settingsButton = await $('[data-testid="settings-button"]')
|
||||||
await click(settingsButton)
|
await click(settingsButton)
|
||||||
|
|
||||||
const defaultDirInput = await $('[data-testid="default-directory-input"]')
|
const projectDirInput = await $('[data-testid="project-directory-input"]')
|
||||||
expect(await defaultDirInput.getValue()).toEqual(defaultDir)
|
expect(await projectDirInput.getValue()).toEqual(defaultProjectDir)
|
||||||
|
|
||||||
const nameInput = await $('[data-testid="name-input"]')
|
/*
|
||||||
|
* 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"]')
|
||||||
expect(await nameInput.getValue()).toEqual('project-$nnn')
|
expect(await nameInput.getValue()).toEqual('project-$nnn')
|
||||||
|
|
||||||
const closeButton = await $('[data-testid="close-button"]')
|
const closeButton = await $('[data-testid="settings-close-button"]')
|
||||||
await click(closeButton)
|
await click(closeButton)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
24
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "untitled-app",
|
"name": "untitled-app",
|
||||||
"version": "0.17.1",
|
"version": "0.17.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.15.0",
|
"@codemirror/autocomplete": "^6.15.0",
|
||||||
@ -10,12 +10,18 @@
|
|||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@headlessui/react": "^1.7.18",
|
"@headlessui/react": "^1.7.18",
|
||||||
"@headlessui/tailwindcss": "^0.2.0",
|
"@headlessui/tailwindcss": "^0.2.0",
|
||||||
|
"@iarna/toml": "^2.2.5",
|
||||||
"@kittycad/lib": "^0.0.56",
|
"@kittycad/lib": "^0.0.56",
|
||||||
"@lezer/javascript": "^1.4.9",
|
"@lezer/javascript": "^1.4.9",
|
||||||
"@open-rpc/client-js": "^1.8.1",
|
"@open-rpc/client-js": "^1.8.1",
|
||||||
"@react-hook/resize-observer": "^1.2.6",
|
"@react-hook/resize-observer": "^1.2.6",
|
||||||
"@replit/codemirror-interact": "^6.3.0",
|
"@replit/codemirror-interact": "^6.3.0",
|
||||||
"@tauri-apps/api": "^1.5.3",
|
"@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",
|
||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@testing-library/user-event": "^14.5.2",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
@ -24,11 +30,12 @@
|
|||||||
"@types/node": "^18.19.26",
|
"@types/node": "^18.19.26",
|
||||||
"@types/react": "^18.2.73",
|
"@types/react": "^18.2.73",
|
||||||
"@types/react-dom": "^18.2.22",
|
"@types/react-dom": "^18.2.22",
|
||||||
"@uiw/react-codemirror": "^4.21.24",
|
"@uiw/react-codemirror": "^4.21.25",
|
||||||
"@xstate/inspect": "^0.8.0",
|
"@xstate/inspect": "^0.8.0",
|
||||||
"@xstate/react": "^3.2.2",
|
"@xstate/react": "^3.2.2",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"debounce-promise": "^3.1.2",
|
"debounce-promise": "^3.1.2",
|
||||||
|
"decamelize": "^6.0.0",
|
||||||
"formik": "^2.4.3",
|
"formik": "^2.4.3",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
@ -46,7 +53,6 @@
|
|||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.22.3",
|
||||||
"sketch-helpers": "^0.0.4",
|
"sketch-helpers": "^0.0.4",
|
||||||
"swr": "^2.2.2",
|
"swr": "^2.2.2",
|
||||||
"tauri-plugin-fs-extra-api": "https://github.com/tauri-apps/tauri-plugin-fs-extra#v1",
|
|
||||||
"three": "^0.160.0",
|
"three": "^0.160.0",
|
||||||
"toml": "^3.0.0",
|
"toml": "^3.0.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
@ -84,7 +90,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\"",
|
"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",
|
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
|
||||||
"lint": "eslint --fix src",
|
"lint": "eslint --fix src",
|
||||||
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json",
|
"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",
|
||||||
"postinstall": "yarn xstate:typegen",
|
"postinstall": "yarn xstate:typegen",
|
||||||
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\""
|
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\""
|
||||||
},
|
},
|
||||||
@ -108,9 +114,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-env": "^7.23.3",
|
"@babel/preset-env": "^7.24.3",
|
||||||
"@playwright/test": "^1.39.0",
|
"@playwright/test": "^1.39.0",
|
||||||
"@tauri-apps/cli": "^1.5.11",
|
"@tauri-apps/cli": "^2.0.0-beta.12",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/debounce-promise": "^3.1.9",
|
"@types/debounce-promise": "^3.1.9",
|
||||||
"@types/pixelmatch": "^5.2.6",
|
"@types/pixelmatch": "^5.2.6",
|
||||||
@ -132,7 +138,7 @@
|
|||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"eslint-plugin-css-modules": "^2.12.0",
|
"eslint-plugin-css-modules": "^2.12.0",
|
||||||
"happy-dom": "^14.3.1",
|
"happy-dom": "^14.3.10",
|
||||||
"husky": "^9.0.11",
|
"husky": "^9.0.11",
|
||||||
"pixelmatch": "^5.3.0",
|
"pixelmatch": "^5.3.0",
|
||||||
"pngjs": "^7.0.0",
|
"pngjs": "^7.0.0",
|
||||||
@ -141,7 +147,7 @@
|
|||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.0",
|
||||||
"setimmediate": "^1.0.5",
|
"setimmediate": "^1.0.5",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"vite": "^5.2.2",
|
"vite": "^5.2.6",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-package-version": "^1.1.0",
|
"vite-plugin-package-version": "^1.1.0",
|
||||||
"vite-tsconfig-paths": "^4.3.2",
|
"vite-tsconfig-paths": "^4.3.2",
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test'
|
import { defineConfig, devices } from '@playwright/test'
|
||||||
|
import { basicStorageState } from './e2e/playwright/storageStates'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read environment variables from file.
|
* Read environment variables from file.
|
||||||
@ -28,6 +29,9 @@ export default defineConfig({
|
|||||||
|
|
||||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
trace: 'on-first-retry',
|
trace: 'on-first-retry',
|
||||||
|
|
||||||
|
/* Use a common shared localStorage */
|
||||||
|
storageState: basicStorageState,
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Configure projects for major browsers */
|
/* Configure projects for major browsers */
|
||||||
|
BIN
public/clientSideSceneAssets/extra-segment-texture.png
Normal file
After Width: | Height: | Size: 327 B |
2549
src-tauri/Cargo.lock
generated
@ -7,12 +7,12 @@ license = ""
|
|||||||
repository = "https://github.com/KittyCAD/modeling-app"
|
repository = "https://github.com/KittyCAD/modeling-app"
|
||||||
default-run = "app"
|
default-run = "app"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.60"
|
rust-version = "1.70"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "1.5.1", features = [] }
|
tauri-build = { version = "2.0.0-beta", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
@ -20,9 +20,14 @@ kittycad = "0.2.63"
|
|||||||
oauth2 = "4.4.2"
|
oauth2 = "4.4.2"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tauri = { version = "1.6.1", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "devtools"] }
|
tauri = { version = "2.0.0-beta", features = [ "devtools", "unstable"] }
|
||||||
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
tauri-plugin-dialog = { version = "2.0.0-beta.0" }
|
||||||
tokio = { version = "1.36.0", features = ["time"] }
|
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"] }
|
||||||
toml = "0.8.2"
|
toml = "0.8.2"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
87
src-tauri/capabilities/desktop.json
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
{
|
||||||
|
"$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"
|
||||||
|
]
|
||||||
|
}
|
@ -4,11 +4,15 @@
|
|||||||
use std::env;
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use oauth2::TokenResponse;
|
use oauth2::TokenResponse;
|
||||||
|
use serde::Serialize;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use tauri::{InvokeError, Manager};
|
use tauri::ipc::InvokeError;
|
||||||
|
use tauri_plugin_shell::ShellExt;
|
||||||
const DEFAULT_HOST: &str = "https://api.kittycad.io";
|
const DEFAULT_HOST: &str = "https://api.kittycad.io";
|
||||||
|
|
||||||
/// This command returns the a json string parse from a toml file at the path.
|
/// This command returns the a json string parse from a toml file at the path.
|
||||||
@ -24,6 +28,56 @@ fn read_toml(path: &str) -> Result<String, InvokeError> {
|
|||||||
Ok(value)
|
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.
|
/// This command returns a string that is the contents of a file at the path.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn read_txt_file(path: &str) -> Result<String, InvokeError> {
|
fn read_txt_file(path: &str) -> Result<String, InvokeError> {
|
||||||
@ -85,7 +139,8 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
|
|||||||
fs::write("/tmp/kittycad_user_code", details.user_code().secret())
|
fs::write("/tmp/kittycad_user_code", details.user_code().secret())
|
||||||
.expect("Unable to write /tmp/kittycad_user_code file");
|
.expect("Unable to write /tmp/kittycad_user_code file");
|
||||||
} else {
|
} else {
|
||||||
tauri::api::shell::open(&app.shell_scope(), auth_uri.secret(), None)
|
app.shell()
|
||||||
|
.open(auth_uri.secret(), None)
|
||||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,12 +220,15 @@ fn show_in_folder(path: String) {
|
|||||||
fn main() {
|
fn main() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.setup(|_app| {
|
.setup(|_app| {
|
||||||
#[cfg(debug_assertions)] // only include this code on debug builds
|
#[cfg(debug_assertions)]
|
||||||
{
|
{
|
||||||
let window = _app.get_window("main").unwrap();
|
use tauri::Manager;
|
||||||
// comment out the below if you don't devtools to open everytime.
|
_app.get_webview("main").unwrap().open_devtools();
|
||||||
// it's useful because otherwise devtools shuts everytime rust code changes.
|
}
|
||||||
window.open_devtools();
|
#[cfg(not(debug_assertions))]
|
||||||
|
{
|
||||||
|
_app.handle()
|
||||||
|
.plugin(tauri_plugin_updater::Builder::new().build())?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@ -179,9 +237,14 @@ fn main() {
|
|||||||
login,
|
login,
|
||||||
read_toml,
|
read_toml,
|
||||||
read_txt_file,
|
read_txt_file,
|
||||||
|
read_dir_recursive,
|
||||||
show_in_folder,
|
show_in_folder,
|
||||||
])
|
])
|
||||||
.plugin(tauri_plugin_fs_extra::init())
|
.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())
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
@ -1,90 +1,9 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
"build": {
|
"app": {
|
||||||
"beforeDevCommand": "yarn start",
|
|
||||||
"devPath": "http://localhost:3000",
|
|
||||||
"distDir": "../build"
|
|
||||||
},
|
|
||||||
"package": {
|
|
||||||
"productName": "zoo-modeling-app",
|
|
||||||
"version": "0.17.1"
|
|
||||||
},
|
|
||||||
"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": {
|
"security": {
|
||||||
"csp": null
|
"csp": null
|
||||||
},
|
},
|
||||||
"updater": {
|
|
||||||
"active": false
|
|
||||||
},
|
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"fullscreen": false,
|
"fullscreen": false,
|
||||||
@ -94,5 +13,47 @@
|
|||||||
"width": 1800
|
"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"
|
||||||
}
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
|
||||||
"package": {
|
|
||||||
"productName": "Zoo Modeling App"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +1,13 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
"tauri": {
|
"bundle": {
|
||||||
|
"windows": {
|
||||||
|
"certificateThumbprint": "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D",
|
||||||
|
"digestAlgorithm": "sha256",
|
||||||
|
"timestampUrl": "http://timestamp.digicert.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
"updater": {
|
"updater": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
@ -8,14 +15,6 @@
|
|||||||
],
|
],
|
||||||
"dialog": true,
|
"dialog": true,
|
||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K"
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K"
|
||||||
},
|
|
||||||
"bundle": {
|
|
||||||
"identifier": "io.kittycad.modeling-app",
|
|
||||||
"windows": {
|
|
||||||
"certificateThumbprint": "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D",
|
|
||||||
"digestAlgorithm": "sha256",
|
|
||||||
"timestampUrl": "http://timestamp.digicert.com"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
|
||||||
"package": {
|
|
||||||
"productName": "Zoo Modeling App"
|
|
||||||
}
|
|
||||||
}
|
|
26
src/App.tsx
@ -1,6 +1,6 @@
|
|||||||
import { useCallback, MouseEventHandler, useEffect } from 'react'
|
import { useCallback, MouseEventHandler, useEffect } from 'react'
|
||||||
import { DebugPanel } from './components/DebugPanel'
|
import { DebugPanel } from './components/DebugPanel'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { uuidv4 } from 'lib/utils'
|
||||||
import { PaneType, useStore } from './useStore'
|
import { PaneType, useStore } from './useStore'
|
||||||
import { Logs, KCLErrors } from './components/Logs'
|
import { Logs, KCLErrors } from './components/Logs'
|
||||||
import { CollapsiblePanel } from './components/CollapsiblePanel'
|
import { CollapsiblePanel } from './components/CollapsiblePanel'
|
||||||
@ -33,10 +33,10 @@ import { useModelingContext } from 'hooks/useModelingContext'
|
|||||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
import { useLspContext } from 'components/LspProvider'
|
import { useLspContext } from 'components/LspProvider'
|
||||||
import { useValidateSettings } from 'hooks/useValidateSettings'
|
import { useRefreshSettings } from 'hooks/useRefreshSettings'
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
useValidateSettings()
|
useRefreshSettings(paths.FILE + 'SETTINGS')
|
||||||
const { project, file } = useLoaderData() as IndexLoaderData
|
const { project, file } = useLoaderData() as IndexLoaderData
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const filePath = useAbsoluteFilePath()
|
const filePath = useAbsoluteFilePath()
|
||||||
@ -64,10 +64,14 @@ export function App() {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const { showDebugPanel, onboardingStatus, theme } = settings?.context || {}
|
const {
|
||||||
|
modeling: { showDebugPanel },
|
||||||
|
app: { theme, onboardingStatus },
|
||||||
|
} = settings.context
|
||||||
const { state, send } = useModelingContext()
|
const { state, send } = useModelingContext()
|
||||||
|
|
||||||
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
|
const editorTheme =
|
||||||
|
theme.current === Themes.System ? getSystemTheme() : theme.current
|
||||||
|
|
||||||
// Pane toggling keyboard shortcuts
|
// Pane toggling keyboard shortcuts
|
||||||
const togglePane = useCallback(
|
const togglePane = useCallback(
|
||||||
@ -95,7 +99,7 @@ export function App() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const paneOpacity = [onboardingPaths.CAMERA, onboardingPaths.STREAMING].some(
|
const paneOpacity = [onboardingPaths.CAMERA, onboardingPaths.STREAMING].some(
|
||||||
(p) => p === onboardingStatus
|
(p) => p === onboardingStatus.current
|
||||||
)
|
)
|
||||||
? 'opacity-20'
|
? 'opacity-20'
|
||||||
: didDragInStream
|
: didDragInStream
|
||||||
@ -149,7 +153,7 @@ export function App() {
|
|||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
<Resizable
|
<Resizable
|
||||||
className={
|
className={
|
||||||
'pointer-events-none h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' +
|
'pointer-events-none h-full flex flex-col flex-1 z-10 my-2 ml-2 pr-1 transition-opacity transition-duration-75 ' +
|
||||||
+paneOpacity
|
+paneOpacity
|
||||||
}
|
}
|
||||||
defaultSize={{
|
defaultSize={{
|
||||||
@ -162,8 +166,8 @@ export function App() {
|
|||||||
maxHeight={'auto'}
|
maxHeight={'auto'}
|
||||||
handleClasses={{
|
handleClasses={{
|
||||||
right:
|
right:
|
||||||
'hover:bg-chalkboard-10/50 bg-transparent transition-colors duration-75 transition-ease-out delay-100 ' +
|
'hover:bg-chalkboard-10 hover:dark:bg-chalkboard-110 bg-transparent transition-colors duration-75 transition-ease-out delay-100 ' +
|
||||||
(buttonDownInStream || onboardingStatus === 'camera'
|
(buttonDownInStream || onboardingStatus.current === 'camera'
|
||||||
? 'pointer-events-none '
|
? 'pointer-events-none '
|
||||||
: 'pointer-events-auto'),
|
: 'pointer-events-auto'),
|
||||||
}}
|
}}
|
||||||
@ -198,13 +202,13 @@ export function App() {
|
|||||||
theme={editorTheme}
|
theme={editorTheme}
|
||||||
open={openPanes.includes('kclErrors')}
|
open={openPanes.includes('kclErrors')}
|
||||||
title="KCL Errors"
|
title="KCL Errors"
|
||||||
iconClassNames={{ icon: 'group-open:text-destroy-30' }}
|
iconClassNames={{ bg: 'group-open:bg-destroy-70' }}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</Resizable>
|
</Resizable>
|
||||||
<Stream className="absolute inset-0 z-0" />
|
<Stream className="absolute inset-0 z-0" />
|
||||||
{showDebugPanel && (
|
{showDebugPanel.current && (
|
||||||
<DebugPanel
|
<DebugPanel
|
||||||
title="Debug"
|
title="Debug"
|
||||||
className={
|
className={
|
||||||
|
@ -22,19 +22,18 @@ import { paths } from 'lib/paths'
|
|||||||
import {
|
import {
|
||||||
fileLoader,
|
fileLoader,
|
||||||
homeLoader,
|
homeLoader,
|
||||||
indexLoader,
|
|
||||||
onboardingRedirectLoader,
|
onboardingRedirectLoader,
|
||||||
|
settingsLoader,
|
||||||
} from 'lib/routeLoaders'
|
} from 'lib/routeLoaders'
|
||||||
import { CommandBarProvider } from 'components/CommandBar/CommandBarProvider'
|
import { CommandBarProvider } from 'components/CommandBar/CommandBarProvider'
|
||||||
import SettingsAuthProvider from 'components/SettingsAuthProvider'
|
import SettingsAuthProvider from 'components/SettingsAuthProvider'
|
||||||
import LspProvider from 'components/LspProvider'
|
import LspProvider from 'components/LspProvider'
|
||||||
import { KclContextProvider } from 'lang/KclProvider'
|
import { KclContextProvider } from 'lang/KclProvider'
|
||||||
|
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||||
export const BROWSER_FILE_NAME = 'new'
|
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
loader: indexLoader,
|
loader: settingsLoader,
|
||||||
id: paths.INDEX,
|
id: paths.INDEX,
|
||||||
element: (
|
element: (
|
||||||
<CommandBarProvider>
|
<CommandBarProvider>
|
||||||
@ -47,14 +46,14 @@ const router = createBrowserRouter([
|
|||||||
</KclContextProvider>
|
</KclContextProvider>
|
||||||
</CommandBarProvider>
|
</CommandBarProvider>
|
||||||
),
|
),
|
||||||
|
errorElement: <ErrorPage />,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: paths.INDEX,
|
path: paths.INDEX,
|
||||||
loader: () =>
|
loader: () =>
|
||||||
isTauri()
|
isTauri()
|
||||||
? redirect(paths.HOME)
|
? redirect(paths.HOME)
|
||||||
: redirect(paths.FILE + '/' + BROWSER_FILE_NAME),
|
: redirect(paths.FILE + '/%2F' + BROWSER_PROJECT_NAME),
|
||||||
errorElement: <ErrorPage />,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loader: fileLoader,
|
loader: fileLoader,
|
||||||
@ -67,29 +66,29 @@ const router = createBrowserRouter([
|
|||||||
<Outlet />
|
<Outlet />
|
||||||
<App />
|
<App />
|
||||||
<CommandBar />
|
<CommandBar />
|
||||||
|
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
|
||||||
</ModelingMachineProvider>
|
</ModelingMachineProvider>
|
||||||
<WasmErrBanner />
|
<WasmErrBanner />
|
||||||
</FileMachineProvider>
|
</FileMachineProvider>
|
||||||
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
|
|
||||||
</Auth>
|
</Auth>
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
loader: onboardingRedirectLoader,
|
id: paths.FILE + 'SETTINGS',
|
||||||
index: true,
|
loader: settingsLoader,
|
||||||
element: <></>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
loader: onboardingRedirectLoader,
|
||||||
|
index: true,
|
||||||
|
element: <></>,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: makeUrlPathRelative(paths.SETTINGS),
|
path: makeUrlPathRelative(paths.SETTINGS),
|
||||||
loader: indexLoader, // very rare someone will load into settings first, but it's possible in the browser
|
|
||||||
element: <Settings />,
|
element: <Settings />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: makeUrlPathRelative(paths.ONBOARDING.INDEX),
|
path: makeUrlPathRelative(paths.ONBOARDING.INDEX),
|
||||||
element: <Onboarding />,
|
element: <Onboarding />,
|
||||||
loader: indexLoader, // very rare someone will load into settings first, but it's possible in the browser
|
|
||||||
children: onboardingRoutes,
|
children: onboardingRoutes,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -108,8 +107,15 @@ const router = createBrowserRouter([
|
|||||||
id: paths.HOME,
|
id: paths.HOME,
|
||||||
loader: homeLoader,
|
loader: homeLoader,
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
element: <></>,
|
||||||
|
id: paths.HOME + 'SETTINGS',
|
||||||
|
loader: settingsLoader,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: makeUrlPathRelative(paths.SETTINGS),
|
path: makeUrlPathRelative(paths.SETTINGS),
|
||||||
|
loader: settingsLoader,
|
||||||
element: <Settings />,
|
element: <Settings />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -18,8 +18,12 @@ export const Toolbar = () => {
|
|||||||
const { commandBarSend } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
const { state, send, context } = useModelingContext()
|
const { state, send, context } = useModelingContext()
|
||||||
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
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 =
|
const bgClassName =
|
||||||
'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'
|
'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'
|
||||||
const pathId = useMemo(() => {
|
const pathId = useMemo(() => {
|
||||||
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) {
|
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) {
|
||||||
return false
|
return false
|
||||||
@ -64,12 +68,14 @@ export const Toolbar = () => {
|
|||||||
{state.nextEvents.includes('Enter sketch') && (
|
{state.nextEvents.includes('Enter sketch') && (
|
||||||
<li className="contents">
|
<li className="contents">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
className={buttonClassName}
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
send({ type: 'Enter sketch', data: { forceNewSketch: true } })
|
send({ type: 'Enter sketch', data: { forceNewSketch: true } })
|
||||||
}
|
}
|
||||||
icon={{
|
icon={{
|
||||||
icon: 'sketch',
|
icon: 'sketch',
|
||||||
|
iconClassName,
|
||||||
bgClassName,
|
bgClassName,
|
||||||
}}
|
}}
|
||||||
disabled={disableAllButtons}
|
disabled={disableAllButtons}
|
||||||
@ -81,10 +87,12 @@ export const Toolbar = () => {
|
|||||||
{state.nextEvents.includes('Enter sketch') && pathId && (
|
{state.nextEvents.includes('Enter sketch') && pathId && (
|
||||||
<li className="contents">
|
<li className="contents">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
className={buttonClassName}
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={() => send({ type: 'Enter sketch' })}
|
onClick={() => send({ type: 'Enter sketch' })}
|
||||||
icon={{
|
icon={{
|
||||||
icon: 'sketch',
|
icon: 'sketch',
|
||||||
|
iconClassName,
|
||||||
bgClassName,
|
bgClassName,
|
||||||
}}
|
}}
|
||||||
disabled={disableAllButtons}
|
disabled={disableAllButtons}
|
||||||
@ -96,10 +104,12 @@ export const Toolbar = () => {
|
|||||||
{state.nextEvents.includes('Cancel') && !state.matches('idle') && (
|
{state.nextEvents.includes('Cancel') && !state.matches('idle') && (
|
||||||
<li className="contents">
|
<li className="contents">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
className={buttonClassName}
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={() => send({ type: 'Cancel' })}
|
onClick={() => send({ type: 'Cancel' })}
|
||||||
icon={{
|
icon={{
|
||||||
icon: 'arrowLeft',
|
icon: 'arrowLeft',
|
||||||
|
iconClassName,
|
||||||
bgClassName,
|
bgClassName,
|
||||||
}}
|
}}
|
||||||
disabled={disableAllButtons}
|
disabled={disableAllButtons}
|
||||||
@ -112,6 +122,7 @@ export const Toolbar = () => {
|
|||||||
<>
|
<>
|
||||||
<li className="contents" key="line-button">
|
<li className="contents" key="line-button">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
className={buttonClassName}
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
state?.matches('Sketch.Line tool')
|
state?.matches('Sketch.Line tool')
|
||||||
@ -119,9 +130,9 @@ export const Toolbar = () => {
|
|||||||
: send('Equip Line tool')
|
: send('Equip Line tool')
|
||||||
}
|
}
|
||||||
aria-pressed={state?.matches('Sketch.Line tool')}
|
aria-pressed={state?.matches('Sketch.Line tool')}
|
||||||
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
|
|
||||||
icon={{
|
icon={{
|
||||||
icon: 'line',
|
icon: 'line',
|
||||||
|
iconClassName,
|
||||||
bgClassName,
|
bgClassName,
|
||||||
}}
|
}}
|
||||||
disabled={disableAllButtons}
|
disabled={disableAllButtons}
|
||||||
@ -131,6 +142,7 @@ export const Toolbar = () => {
|
|||||||
</li>
|
</li>
|
||||||
<li className="contents" key="tangential-arc-button">
|
<li className="contents" key="tangential-arc-button">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
className={buttonClassName}
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
state.matches('Sketch.Tangential arc to')
|
state.matches('Sketch.Tangential arc to')
|
||||||
@ -138,9 +150,9 @@ export const Toolbar = () => {
|
|||||||
: send('Equip tangential arc to')
|
: send('Equip tangential arc to')
|
||||||
}
|
}
|
||||||
aria-pressed={state.matches('Sketch.Tangential arc to')}
|
aria-pressed={state.matches('Sketch.Tangential arc to')}
|
||||||
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
|
|
||||||
icon={{
|
icon={{
|
||||||
icon: 'arc',
|
icon: 'arc',
|
||||||
|
iconClassName,
|
||||||
bgClassName,
|
bgClassName,
|
||||||
}}
|
}}
|
||||||
disabled={
|
disabled={
|
||||||
@ -179,8 +191,8 @@ export const Toolbar = () => {
|
|||||||
.map((eventName) => (
|
.map((eventName) => (
|
||||||
<li className="contents" key={eventName}>
|
<li className="contents" key={eventName}>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
className={buttonClassName}
|
||||||
Element="button"
|
Element="button"
|
||||||
className="text-sm"
|
|
||||||
key={eventName}
|
key={eventName}
|
||||||
onClick={() => send(eventName)}
|
onClick={() => send(eventName)}
|
||||||
disabled={
|
disabled={
|
||||||
@ -191,6 +203,7 @@ export const Toolbar = () => {
|
|||||||
title={eventName}
|
title={eventName}
|
||||||
icon={{
|
icon={{
|
||||||
icon: 'line',
|
icon: 'line',
|
||||||
|
iconClassName,
|
||||||
bgClassName,
|
bgClassName,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -203,8 +216,8 @@ export const Toolbar = () => {
|
|||||||
{state.matches('idle') && (
|
{state.matches('idle') && (
|
||||||
<li className="contents">
|
<li className="contents">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
className={buttonClassName}
|
||||||
Element="button"
|
Element="button"
|
||||||
className="text-sm"
|
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
commandBarSend({
|
commandBarSend({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
@ -219,6 +232,7 @@ export const Toolbar = () => {
|
|||||||
}
|
}
|
||||||
icon={{
|
icon={{
|
||||||
icon: 'extrude',
|
icon: 'extrude',
|
||||||
|
iconClassName,
|
||||||
bgClassName,
|
bgClassName,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -231,16 +245,16 @@ export const Toolbar = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-full flex items-stretch rounded-l-sm rounded-r-full bg-chalkboard-10 dark:bg-chalkboard-100 relative">
|
<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 bg-chalkboard-10 dark:bg-chalkboard-100 border-solid border border-energy-10 dark:border-chalkboard-90 border-r-0">
|
<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">
|
||||||
<ToolbarButtons />
|
<ToolbarButtons />
|
||||||
</menu>
|
</menu>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={() => commandBarSend({ type: 'Open' })}
|
onClick={() => commandBarSend({ type: 'Open' })}
|
||||||
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"
|
className="rounded-r-full pr-4 self-stretch border-primary/30 hover:border-primary dark:border-chalkboard-80 dark:bg-chalkboard-80 text-primary"
|
||||||
>
|
>
|
||||||
{platform === 'darwin' ? '⌘K' : 'Ctrl+/'}
|
{platform === 'macos' ? '⌘K' : 'Ctrl+/'}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -21,7 +21,7 @@ import {
|
|||||||
Subscription,
|
Subscription,
|
||||||
EngineCommandManager,
|
EngineCommandManager,
|
||||||
} from 'lang/std/engineConnection'
|
} from 'lang/std/engineConnection'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { uuidv4 } from 'lib/utils'
|
||||||
import { deg2Rad } from 'lib/utils2d'
|
import { deg2Rad } from 'lib/utils2d'
|
||||||
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
|
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
|
||||||
import * as TWEEN from '@tweenjs/tween.js'
|
import * as TWEEN from '@tweenjs/tween.js'
|
||||||
|
@ -4,10 +4,15 @@ import { useModelingContext } from 'hooks/useModelingContext'
|
|||||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { useStore } from 'useStore'
|
import { useStore } from 'useStore'
|
||||||
import { DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
|
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
|
||||||
import { ReactCameraProperties } from './CameraControls'
|
import { ReactCameraProperties } from './CameraControls'
|
||||||
import { throttle } from 'lib/utils'
|
import { throttle } from 'lib/utils'
|
||||||
import { sceneInfra } from 'lib/singletons'
|
import { sceneInfra } from 'lib/singletons'
|
||||||
|
import {
|
||||||
|
EXTRA_SEGMENT_HANDLE,
|
||||||
|
PROFILE_START,
|
||||||
|
getParentGroup,
|
||||||
|
} from './sceneEntities'
|
||||||
|
|
||||||
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
||||||
const [isCamMoving, setIsCamMoving] = useState(false)
|
const [isCamMoving, setIsCamMoving] = useState(false)
|
||||||
@ -37,10 +42,10 @@ export const ClientSideScene = ({
|
|||||||
}: {
|
}: {
|
||||||
cameraControls: ReturnType<
|
cameraControls: ReturnType<
|
||||||
typeof useSettingsAuthContext
|
typeof useSettingsAuthContext
|
||||||
>['settings']['context']['cameraControls']
|
>['settings']['context']['modeling']['mouseControls']['current']
|
||||||
}) => {
|
}) => {
|
||||||
const canvasRef = useRef<HTMLDivElement>(null)
|
const canvasRef = useRef<HTMLDivElement>(null)
|
||||||
const { state, send } = useModelingContext()
|
const { state, send, context } = useModelingContext()
|
||||||
const { hideClient, hideServer } = useShouldHideScene()
|
const { hideClient, hideServer } = useShouldHideScene()
|
||||||
const { setHighlightRange } = useStore((s) => ({
|
const { setHighlightRange } = useStore((s) => ({
|
||||||
setHighlightRange: s.setHighlightRange,
|
setHighlightRange: s.setHighlightRange,
|
||||||
@ -76,9 +81,33 @@ 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
|
style={{ cursor: cursor }}
|
||||||
className={`absolute inset-0 h-full w-full transition-all duration-300 ${
|
className={`absolute inset-0 h-full w-full transition-all duration-300 ${
|
||||||
hideClient ? 'opacity-0' : 'opacity-100'
|
hideClient ? 'opacity-0' : 'opacity-100'
|
||||||
} ${hideServer ? 'bg-black' : ''} ${
|
} ${hideServer ? 'bg-black' : ''} ${
|
||||||
|
@ -28,12 +28,15 @@ export function createGridHelper({
|
|||||||
gridHelper.rotation.x = Math.PI / 2
|
gridHelper.rotation.x = Math.PI / 2
|
||||||
return gridHelper
|
return gridHelper
|
||||||
}
|
}
|
||||||
|
const fudgeFactor = 72.66985970437086
|
||||||
|
|
||||||
export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) =>
|
export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) =>
|
||||||
0.55 / cam.zoom
|
(0.55 * fudgeFactor) / cam.zoom / window.innerHeight
|
||||||
|
|
||||||
export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) =>
|
export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) =>
|
||||||
(group.position.distanceTo(cam.position) * cam.fov) / 4000
|
(group.position.distanceTo(cam.position) * cam.fov * fudgeFactor) /
|
||||||
|
4000 /
|
||||||
|
window.innerHeight
|
||||||
|
|
||||||
export function isQuaternionVertical(q: Quaternion) {
|
export function isQuaternionVertical(q: Quaternion) {
|
||||||
const v = new Vector3(0, 0, 1).applyQuaternion(q)
|
const v = new Vector3(0, 0, 1).applyQuaternion(q)
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
OrthographicCamera,
|
OrthographicCamera,
|
||||||
PerspectiveCamera,
|
PerspectiveCamera,
|
||||||
PlaneGeometry,
|
PlaneGeometry,
|
||||||
|
Points,
|
||||||
Quaternion,
|
Quaternion,
|
||||||
Scene,
|
Scene,
|
||||||
Shape,
|
Shape,
|
||||||
@ -81,20 +82,25 @@ import {
|
|||||||
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
|
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
|
||||||
import { createGridHelper, orthoScale, perspScale } from './helpers'
|
import { createGridHelper, orthoScale, perspScale } from './helpers'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { uuidv4 } from 'lib/utils'
|
||||||
import { SketchDetails } from 'machines/modelingMachine'
|
import { SketchDetails } from 'machines/modelingMachine'
|
||||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||||
|
|
||||||
type DraftSegment = 'line' | 'tangentialArcTo'
|
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 = 'straight-segment'
|
||||||
export const STRAIGHT_SEGMENT_BODY = 'straight-segment-body'
|
export const STRAIGHT_SEGMENT_BODY = 'straight-segment-body'
|
||||||
export const STRAIGHT_SEGMENT_DASH = 'straight-segment-body-dashed'
|
export const STRAIGHT_SEGMENT_DASH = 'straight-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 TANGENTIAL_ARC_TO__SEGMENT_DASH =
|
export const TANGENTIAL_ARC_TO__SEGMENT_DASH =
|
||||||
'tangential-arc-to-segment-body-dashed'
|
'tangential-arc-to-segment-body-dashed'
|
||||||
export const PROFILE_START = 'profile-start'
|
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
|
||||||
|
|
||||||
// This singleton Class is responsible for all of the things the user sees and interacts with.
|
// This singleton Class is responsible for all of the things the user sees and interacts with.
|
||||||
// That mostly mean sketch elements.
|
// That mostly mean sketch elements.
|
||||||
@ -111,8 +117,12 @@ export class SceneEntities {
|
|||||||
this.engineCommandManager = engineCommandManager
|
this.engineCommandManager = engineCommandManager
|
||||||
this.scene = sceneInfra?.scene
|
this.scene = sceneInfra?.scene
|
||||||
sceneInfra?.camControls.subscribeToCamChange(this.onCamChange)
|
sceneInfra?.camControls.subscribeToCamChange(this.onCamChange)
|
||||||
|
window.addEventListener('resize', this.onWindowResize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onWindowResize = () => {
|
||||||
|
this.onCamChange()
|
||||||
|
}
|
||||||
onCamChange = () => {
|
onCamChange = () => {
|
||||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||||
|
|
||||||
@ -282,7 +292,6 @@ export class SceneEntities {
|
|||||||
sketchGroup: SketchGroup
|
sketchGroup: SketchGroup
|
||||||
variableDeclarationName: string
|
variableDeclarationName: string
|
||||||
}> {
|
}> {
|
||||||
sceneInfra.resetMouseListeners()
|
|
||||||
this.createIntersectionPlane()
|
this.createIntersectionPlane()
|
||||||
|
|
||||||
const { truncatedAst, programMemoryOverride, variableDeclarationName } =
|
const { truncatedAst, programMemoryOverride, variableDeclarationName } =
|
||||||
@ -295,7 +304,7 @@ export class SceneEntities {
|
|||||||
})
|
})
|
||||||
const sketchGroup = sketchGroupFromPathToNode({
|
const sketchGroup = sketchGroupFromPathToNode({
|
||||||
pathToNode: sketchPathToNode,
|
pathToNode: sketchPathToNode,
|
||||||
ast: kclManager.ast,
|
ast: maybeModdedAst,
|
||||||
programMemory,
|
programMemory,
|
||||||
})
|
})
|
||||||
if (!Array.isArray(sketchGroup?.value))
|
if (!Array.isArray(sketchGroup?.value))
|
||||||
@ -383,6 +392,7 @@ export class SceneEntities {
|
|||||||
pathToNode: segPathToNode,
|
pathToNode: segPathToNode,
|
||||||
isDraftSegment,
|
isDraftSegment,
|
||||||
scale: factor,
|
scale: factor,
|
||||||
|
texture: sceneInfra.extraSegmentTexture,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
seg = straightSegment({
|
seg = straightSegment({
|
||||||
@ -393,6 +403,7 @@ export class SceneEntities {
|
|||||||
isDraftSegment,
|
isDraftSegment,
|
||||||
scale: factor,
|
scale: factor,
|
||||||
callExpName,
|
callExpName,
|
||||||
|
texture: sceneInfra.extraSegmentTexture,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
seg.layers.set(SKETCH_LAYER)
|
seg.layers.set(SKETCH_LAYER)
|
||||||
@ -435,6 +446,7 @@ export class SceneEntities {
|
|||||||
) => {
|
) => {
|
||||||
await kclManager.updateAst(modifiedAst, false)
|
await kclManager.updateAst(modifiedAst, false)
|
||||||
await this.tearDownSketch({ removeAxis: false })
|
await this.tearDownSketch({ removeAxis: false })
|
||||||
|
sceneInfra.resetMouseListeners()
|
||||||
await this.setupSketch({
|
await this.setupSketch({
|
||||||
sketchPathToNode,
|
sketchPathToNode,
|
||||||
forward,
|
forward,
|
||||||
@ -442,7 +454,12 @@ export class SceneEntities {
|
|||||||
position: origin,
|
position: origin,
|
||||||
maybeModdedAst: kclManager.ast,
|
maybeModdedAst: kclManager.ast,
|
||||||
})
|
})
|
||||||
this.setupSketchIdleCallbacks(sketchPathToNode)
|
this.setupSketchIdleCallbacks({
|
||||||
|
forward,
|
||||||
|
up,
|
||||||
|
position: origin,
|
||||||
|
pathToNode: sketchPathToNode,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
setUpDraftSegment = async (
|
setUpDraftSegment = async (
|
||||||
sketchPathToNode: PathToNode,
|
sketchPathToNode: PathToNode,
|
||||||
@ -467,19 +484,20 @@ 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 index = sg.value.length // because we've added a new segment that's not in the memory yet, no need for `-1`
|
||||||
|
|
||||||
let modifiedAst = addNewSketchLn({
|
const mod = addNewSketchLn({
|
||||||
node: kclManager.ast,
|
node: _ast,
|
||||||
programMemory: kclManager.programMemory,
|
programMemory: kclManager.programMemory,
|
||||||
to: [lastSeg.to[0], lastSeg.to[1]],
|
to: [lastSeg.to[0], lastSeg.to[1]],
|
||||||
from: [lastSeg.to[0], lastSeg.to[1]],
|
from: [lastSeg.to[0], lastSeg.to[1]],
|
||||||
fnName: segmentName,
|
fnName: segmentName,
|
||||||
pathToNode: sketchPathToNode,
|
pathToNode: sketchPathToNode,
|
||||||
}).modifiedAst
|
})
|
||||||
modifiedAst = parse(recast(modifiedAst))
|
const modifiedAst = parse(recast(mod.modifiedAst))
|
||||||
|
|
||||||
const draftExpressionsIndices = { start: index, end: index }
|
const draftExpressionsIndices = { start: index, end: index }
|
||||||
|
|
||||||
if (shouldTearDown) await this.tearDownSketch({ removeAxis: false })
|
if (shouldTearDown) await this.tearDownSketch({ removeAxis: false })
|
||||||
|
sceneInfra.resetMouseListeners()
|
||||||
const { truncatedAst, programMemoryOverride, sketchGroup } =
|
const { truncatedAst, programMemoryOverride, sketchGroup } =
|
||||||
await this.setupSketch({
|
await this.setupSketch({
|
||||||
sketchPathToNode,
|
sketchPathToNode,
|
||||||
@ -546,13 +564,104 @@ export class SceneEntities {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
...mouseEnterLeaveCallbacks(),
|
...this.mouseEnterLeaveCallbacks(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
setupSketchIdleCallbacks = (pathToNode: PathToNode) => {
|
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'
|
||||||
sceneInfra.setCallbacks({
|
sceneInfra.setCallbacks({
|
||||||
onDrag: ({ selected, intersectionPoint, mouseEvent, intersects }) => {
|
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,
|
||||||
|
}) => {
|
||||||
if (mouseEvent.which !== 1) return
|
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({
|
this.onDragSegment({
|
||||||
object: selected,
|
object: selected,
|
||||||
intersection2d: intersectionPoint.twoD,
|
intersection2d: intersectionPoint.twoD,
|
||||||
@ -577,7 +686,7 @@ export class SceneEntities {
|
|||||||
if (!event) return
|
if (!event) return
|
||||||
sceneInfra.modelingSend(event)
|
sceneInfra.modelingSend(event)
|
||||||
},
|
},
|
||||||
...mouseEnterLeaveCallbacks(),
|
...this.mouseEnterLeaveCallbacks(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
prepareTruncatedMemoryAndAst = (
|
prepareTruncatedMemoryAndAst = (
|
||||||
@ -755,8 +864,7 @@ export class SceneEntities {
|
|||||||
group.userData.to = to
|
group.userData.to = to
|
||||||
group.userData.prevSegment = prevSegment
|
group.userData.prevSegment = prevSegment
|
||||||
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
||||||
|
const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
||||||
arrowGroup.position.set(to[0], to[1], 0)
|
|
||||||
|
|
||||||
const previousPoint =
|
const previousPoint =
|
||||||
prevSegment?.type === 'TangentialArcTo'
|
prevSegment?.type === 'TangentialArcTo'
|
||||||
@ -774,13 +882,49 @@ export class SceneEntities {
|
|||||||
obtuse: true,
|
obtuse: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const arrowheadAngle =
|
const pxLength = arcInfo.arcLength / scale
|
||||||
arcInfo.endAngle + (Math.PI / 2) * (arcInfo.ccw ? 1 : -1)
|
const shouldHideIdle = pxLength < HIDE_SEGMENT_LENGTH
|
||||||
arrowGroup.quaternion.setFromUnitVectors(
|
const shouldHideHover = pxLength < HIDE_HOVER_SEGMENT_LENGTH
|
||||||
new Vector3(0, 1, 0),
|
|
||||||
new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0)
|
const hoveredParent =
|
||||||
)
|
sceneInfra.hoveredObject &&
|
||||||
arrowGroup.scale.set(scale, scale, scale)
|
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 tangentialArcToSegmentBody = group.children.find(
|
const tangentialArcToSegmentBody = group.children.find(
|
||||||
(child) => child.userData.type === TANGENTIAL_ARC_TO_SEGMENT_BODY
|
(child) => child.userData.type === TANGENTIAL_ARC_TO_SEGMENT_BODY
|
||||||
@ -827,10 +971,26 @@ export class SceneEntities {
|
|||||||
group.userData.from = from
|
group.userData.from = from
|
||||||
group.userData.to = to
|
group.userData.to = to
|
||||||
const shape = new Shape()
|
const shape = new Shape()
|
||||||
shape.moveTo(0, -0.08 * scale)
|
shape.moveTo(0, (-SEGMENT_WIDTH_PX / 2) * scale) // The width of the line in px (2.4px in this case)
|
||||||
shape.lineTo(0, 0.08 * scale) // The width of the line
|
shape.lineTo(0, (SEGMENT_WIDTH_PX / 2) * scale)
|
||||||
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
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) {
|
if (arrowGroup) {
|
||||||
arrowGroup.position.set(to[0], to[1], 0)
|
arrowGroup.position.set(to[0], to[1], 0)
|
||||||
|
|
||||||
@ -842,6 +1002,21 @@ export class SceneEntities {
|
|||||||
.normalize()
|
.normalize()
|
||||||
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
|
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
|
||||||
arrowGroup.scale.set(scale, scale, scale)
|
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(
|
const straightSegmentBody = group.children.find(
|
||||||
@ -1019,6 +1194,119 @@ 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'
|
export type DefaultPlaneStr = 'XY' | 'XZ' | 'YZ' | '-XY' | '-XZ' | '-YZ'
|
||||||
@ -1160,7 +1448,7 @@ function colorSegment(object: any, color: number) {
|
|||||||
])
|
])
|
||||||
if (straightSegmentBody) {
|
if (straightSegmentBody) {
|
||||||
straightSegmentBody.traverse((child) => {
|
straightSegmentBody.traverse((child) => {
|
||||||
if (child instanceof Mesh) {
|
if (child instanceof Mesh && !child.userData.ignoreColorChange) {
|
||||||
child.material.color.set(color)
|
child.material.color.set(color)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -1261,53 +1549,3 @@ function massageFormats(a: any): Vector3 {
|
|||||||
? new Vector3(a[0], a[1], a[2])
|
? new Vector3(a[0], a[1], a[2])
|
||||||
: new Vector3(a.x, a.y, a.z)
|
: 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)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -18,6 +18,8 @@ import {
|
|||||||
Intersection,
|
Intersection,
|
||||||
Object3D,
|
Object3D,
|
||||||
Object3DEventMap,
|
Object3DEventMap,
|
||||||
|
TextureLoader,
|
||||||
|
Texture,
|
||||||
} from 'three'
|
} from 'three'
|
||||||
import { compareVec2Epsilon2 } from 'lang/std/sketch'
|
import { compareVec2Epsilon2 } from 'lang/std/sketch'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
@ -25,9 +27,10 @@ import * as TWEEN from '@tweenjs/tween.js'
|
|||||||
import { SourceRange } from 'lang/wasm'
|
import { SourceRange } from 'lang/wasm'
|
||||||
import { Axis } from 'lib/selections'
|
import { Axis } from 'lib/selections'
|
||||||
import { type BaseUnit } from 'lib/settings/settingsTypes'
|
import { type BaseUnit } from 'lib/settings/settingsTypes'
|
||||||
import { SETTINGS_PERSIST_KEY } from 'lib/constants'
|
|
||||||
import { CameraControls } from './CameraControls'
|
import { CameraControls } from './CameraControls'
|
||||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||||
|
import { settings } from 'lib/settings/initialSettings'
|
||||||
|
import { MouseState } from 'machines/modelingMachine'
|
||||||
|
|
||||||
type SendType = ReturnType<typeof useModelingContext>['send']
|
type SendType = ReturnType<typeof useModelingContext>['send']
|
||||||
|
|
||||||
@ -54,6 +57,7 @@ export const ARROWHEAD = 'arrowhead'
|
|||||||
|
|
||||||
export interface OnMouseEnterLeaveArgs {
|
export interface OnMouseEnterLeaveArgs {
|
||||||
selected: Object3D<Object3DEventMap>
|
selected: Object3D<Object3DEventMap>
|
||||||
|
dragSelected?: Object3D<Object3DEventMap>
|
||||||
mouseEvent: MouseEvent
|
mouseEvent: MouseEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,18 +102,26 @@ export class SceneInfra {
|
|||||||
isFovAnimationInProgress = false
|
isFovAnimationInProgress = false
|
||||||
_baseUnit: BaseUnit = 'mm'
|
_baseUnit: BaseUnit = 'mm'
|
||||||
_baseUnitMultiplier = 1
|
_baseUnitMultiplier = 1
|
||||||
|
extraSegmentTexture: Texture
|
||||||
|
lastMouseState: MouseState = { type: 'idle' }
|
||||||
|
onDragStartCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
||||||
|
onDragEndCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
||||||
onDragCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
onDragCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
||||||
onMoveCallback: (arg: OnMoveCallbackArgs) => void = () => {}
|
onMoveCallback: (arg: OnMoveCallbackArgs) => void = () => {}
|
||||||
onClickCallback: (arg: OnClickCallbackArgs) => void = () => {}
|
onClickCallback: (arg: OnClickCallbackArgs) => void = () => {}
|
||||||
onMouseEnter: (arg: OnMouseEnterLeaveArgs) => void = () => {}
|
onMouseEnter: (arg: OnMouseEnterLeaveArgs) => void = () => {}
|
||||||
onMouseLeave: (arg: OnMouseEnterLeaveArgs) => void = () => {}
|
onMouseLeave: (arg: OnMouseEnterLeaveArgs) => void = () => {}
|
||||||
setCallbacks = (callbacks: {
|
setCallbacks = (callbacks: {
|
||||||
|
onDragStart?: (arg: OnDragCallbackArgs) => void
|
||||||
|
onDragEnd?: (arg: OnDragCallbackArgs) => void
|
||||||
onDrag?: (arg: OnDragCallbackArgs) => void
|
onDrag?: (arg: OnDragCallbackArgs) => void
|
||||||
onMove?: (arg: OnMoveCallbackArgs) => void
|
onMove?: (arg: OnMoveCallbackArgs) => void
|
||||||
onClick?: (arg: OnClickCallbackArgs) => void
|
onClick?: (arg: OnClickCallbackArgs) => void
|
||||||
onMouseEnter?: (arg: OnMouseEnterLeaveArgs) => void
|
onMouseEnter?: (arg: OnMouseEnterLeaveArgs) => void
|
||||||
onMouseLeave?: (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.onDragCallback = callbacks.onDrag || this.onDragCallback
|
||||||
this.onMoveCallback = callbacks.onMove || this.onMoveCallback
|
this.onMoveCallback = callbacks.onMove || this.onMoveCallback
|
||||||
this.onClickCallback = callbacks.onClick || this.onClickCallback
|
this.onClickCallback = callbacks.onClick || this.onClickCallback
|
||||||
@ -128,6 +140,8 @@ export class SceneInfra {
|
|||||||
}
|
}
|
||||||
resetMouseListeners = () => {
|
resetMouseListeners = () => {
|
||||||
this.setCallbacks({
|
this.setCallbacks({
|
||||||
|
onDragStart: () => {},
|
||||||
|
onDragEnd: () => {},
|
||||||
onDrag: () => {},
|
onDrag: () => {},
|
||||||
onMove: () => {},
|
onMove: () => {},
|
||||||
onClick: () => {},
|
onClick: () => {},
|
||||||
@ -170,9 +184,7 @@ export class SceneInfra {
|
|||||||
|
|
||||||
// CAMERA
|
// CAMERA
|
||||||
const camHeightDistanceRatio = 0.5
|
const camHeightDistanceRatio = 0.5
|
||||||
const baseUnit: BaseUnit =
|
const baseUnit: BaseUnit = settings.modeling.defaultUnit.current
|
||||||
JSON.parse(localStorage?.getItem(SETTINGS_PERSIST_KEY) || ('{}' as any))
|
|
||||||
.baseUnit || 'mm'
|
|
||||||
const baseRadius = 5.6
|
const baseRadius = 5.6
|
||||||
const length = baseUnitTomm(baseUnit) * baseRadius
|
const length = baseUnitTomm(baseUnit) * baseRadius
|
||||||
const ang = Math.atan(camHeightDistanceRatio)
|
const ang = Math.atan(camHeightDistanceRatio)
|
||||||
@ -212,6 +224,13 @@ export class SceneInfra {
|
|||||||
const light = new AmbientLight(0x505050) // soft white light
|
const light = new AmbientLight(0x505050) // soft white light
|
||||||
this.scene.add(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
|
SceneInfra.instance = this
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,8 +340,6 @@ export class SceneInfra {
|
|||||||
planeIntersectPoint.twoD &&
|
planeIntersectPoint.twoD &&
|
||||||
planeIntersectPoint.threeD
|
planeIntersectPoint.threeD
|
||||||
) {
|
) {
|
||||||
// // console.log('onDrag', this.selected)
|
|
||||||
|
|
||||||
this.onDragCallback({
|
this.onDragCallback({
|
||||||
mouseEvent,
|
mouseEvent,
|
||||||
intersectionPoint: {
|
intersectionPoint: {
|
||||||
@ -332,6 +349,10 @@ export class SceneInfra {
|
|||||||
intersects,
|
intersects,
|
||||||
selected: this.selected.object,
|
selected: this.selected.object,
|
||||||
})
|
})
|
||||||
|
this.updateMouseState({
|
||||||
|
type: 'isDragging',
|
||||||
|
on: this.selected.object,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
planeIntersectPoint &&
|
planeIntersectPoint &&
|
||||||
@ -351,25 +372,34 @@ export class SceneInfra {
|
|||||||
if (intersects[0]) {
|
if (intersects[0]) {
|
||||||
const firstIntersectObject = intersects[0].object
|
const firstIntersectObject = intersects[0].object
|
||||||
if (this.hoveredObject !== firstIntersectObject) {
|
if (this.hoveredObject !== firstIntersectObject) {
|
||||||
if (this.hoveredObject) {
|
const hoveredObj = this.hoveredObject
|
||||||
this.onMouseLeave({
|
this.hoveredObject = null
|
||||||
selected: this.hoveredObject,
|
this.onMouseLeave({
|
||||||
mouseEvent: mouseEvent,
|
selected: hoveredObj,
|
||||||
})
|
mouseEvent: mouseEvent,
|
||||||
}
|
})
|
||||||
this.hoveredObject = firstIntersectObject
|
this.hoveredObject = firstIntersectObject
|
||||||
this.onMouseEnter({
|
this.onMouseEnter({
|
||||||
selected: this.hoveredObject,
|
selected: this.hoveredObject,
|
||||||
|
dragSelected: this.selected?.object,
|
||||||
mouseEvent: mouseEvent,
|
mouseEvent: mouseEvent,
|
||||||
})
|
})
|
||||||
|
if (!this.selected)
|
||||||
|
this.updateMouseState({
|
||||||
|
type: 'isHovering',
|
||||||
|
on: this.hoveredObject,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this.hoveredObject) {
|
if (this.hoveredObject) {
|
||||||
|
const hoveredObj = this.hoveredObject
|
||||||
|
this.hoveredObject = null
|
||||||
this.onMouseLeave({
|
this.onMouseLeave({
|
||||||
selected: this.hoveredObject,
|
selected: hoveredObj,
|
||||||
|
dragSelected: this.selected?.object,
|
||||||
mouseEvent: mouseEvent,
|
mouseEvent: mouseEvent,
|
||||||
})
|
})
|
||||||
this.hoveredObject = null
|
if (!this.selected) this.updateMouseState({ type: 'idle' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -426,6 +456,11 @@ export class SceneInfra {
|
|||||||
(a, b) => a.distance - b.distance
|
(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) => {
|
onMouseDown = (event: MouseEvent) => {
|
||||||
this.currentMouseVector.x = (event.clientX / window.innerWidth) * 2 - 1
|
this.currentMouseVector.x = (event.clientX / window.innerWidth) * 2 - 1
|
||||||
@ -455,8 +490,26 @@ export class SceneInfra {
|
|||||||
|
|
||||||
if (this.selected) {
|
if (this.selected) {
|
||||||
if (this.selected.hasBeenDragged) {
|
if (this.selected.hasBeenDragged) {
|
||||||
// this is where we could fire a onDragEnd event
|
// TODO do the types properly here
|
||||||
// console.log('onDragEnd', this.selected)
|
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',
|
||||||
|
})
|
||||||
|
}
|
||||||
} else if (planeIntersectPoint?.twoD && planeIntersectPoint?.threeD) {
|
} else if (planeIntersectPoint?.twoD && planeIntersectPoint?.threeD) {
|
||||||
// fire onClick event as there was no drags
|
// fire onClick event as there was no drags
|
||||||
this.onClickCallback({
|
this.onClickCallback({
|
||||||
|
@ -12,15 +12,22 @@ import {
|
|||||||
Mesh,
|
Mesh,
|
||||||
MeshBasicMaterial,
|
MeshBasicMaterial,
|
||||||
NormalBufferAttributes,
|
NormalBufferAttributes,
|
||||||
|
Points,
|
||||||
|
PointsMaterial,
|
||||||
Shape,
|
Shape,
|
||||||
SphereGeometry,
|
SphereGeometry,
|
||||||
|
Texture,
|
||||||
Vector2,
|
Vector2,
|
||||||
Vector3,
|
Vector3,
|
||||||
} from 'three'
|
} from 'three'
|
||||||
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
||||||
import { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm'
|
import { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm'
|
||||||
import {
|
import {
|
||||||
|
EXTRA_SEGMENT_HANDLE,
|
||||||
|
EXTRA_SEGMENT_OFFSET_PX,
|
||||||
|
HIDE_SEGMENT_LENGTH,
|
||||||
PROFILE_START,
|
PROFILE_START,
|
||||||
|
SEGMENT_WIDTH_PX,
|
||||||
STRAIGHT_SEGMENT,
|
STRAIGHT_SEGMENT,
|
||||||
STRAIGHT_SEGMENT_BODY,
|
STRAIGHT_SEGMENT_BODY,
|
||||||
STRAIGHT_SEGMENT_DASH,
|
STRAIGHT_SEGMENT_DASH,
|
||||||
@ -44,7 +51,7 @@ export function profileStart({
|
|||||||
}) {
|
}) {
|
||||||
const group = new Group()
|
const group = new Group()
|
||||||
|
|
||||||
const geometry = new BoxGeometry(0.8, 0.8, 0.8)
|
const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later
|
||||||
const body = new MeshBasicMaterial({ color: 0xffffff })
|
const body = new MeshBasicMaterial({ color: 0xffffff })
|
||||||
const mesh = new Mesh(geometry, body)
|
const mesh = new Mesh(geometry, body)
|
||||||
|
|
||||||
@ -71,6 +78,7 @@ export function straightSegment({
|
|||||||
isDraftSegment,
|
isDraftSegment,
|
||||||
scale = 1,
|
scale = 1,
|
||||||
callExpName,
|
callExpName,
|
||||||
|
texture,
|
||||||
}: {
|
}: {
|
||||||
from: Coords2d
|
from: Coords2d
|
||||||
to: Coords2d
|
to: Coords2d
|
||||||
@ -79,12 +87,13 @@ export function straightSegment({
|
|||||||
isDraftSegment?: boolean
|
isDraftSegment?: boolean
|
||||||
scale?: number
|
scale?: number
|
||||||
callExpName: string
|
callExpName: string
|
||||||
|
texture: Texture
|
||||||
}): Group {
|
}): Group {
|
||||||
const group = new Group()
|
const group = new Group()
|
||||||
|
|
||||||
const shape = new Shape()
|
const shape = new Shape()
|
||||||
shape.moveTo(0, -0.08 * scale)
|
shape.moveTo(0, (-SEGMENT_WIDTH_PX / 2) * scale)
|
||||||
shape.lineTo(0, 0.08 * scale) // The width of the line
|
shape.lineTo(0, (SEGMENT_WIDTH_PX / 2) * scale)
|
||||||
|
|
||||||
let geometry
|
let geometry
|
||||||
if (isDraftSegment) {
|
if (isDraftSegment) {
|
||||||
@ -122,24 +131,44 @@ export function straightSegment({
|
|||||||
}
|
}
|
||||||
group.name = STRAIGHT_SEGMENT
|
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)
|
const arrowGroup = createArrowhead(scale)
|
||||||
arrowGroup.position.set(to[0], to[1], 0)
|
arrowGroup.position.set(to[0], to[1], 0)
|
||||||
const dir = new Vector3()
|
const dir = new Vector3()
|
||||||
.subVectors(new Vector3(to[0], to[1], 0), new Vector3(from[0], from[1], 0))
|
.subVectors(new Vector3(to[0], to[1], 0), new Vector3(from[0], from[1], 0))
|
||||||
.normalize()
|
.normalize()
|
||||||
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
|
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)
|
group.add(mesh)
|
||||||
if (callExpName !== 'close') group.add(arrowGroup)
|
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
|
return group
|
||||||
}
|
}
|
||||||
|
|
||||||
function createArrowhead(scale = 1): Group {
|
function createArrowhead(scale = 1): Group {
|
||||||
const arrowMaterial = new MeshBasicMaterial({ color: 0xffffff })
|
const arrowMaterial = new MeshBasicMaterial({ color: 0xffffff })
|
||||||
const arrowheadMesh = new Mesh(new ConeGeometry(0.31, 1.5, 12), arrowMaterial)
|
// specify the size of the geometry in pixels (i.e. cone height = 20px, cone radius = 4.5px)
|
||||||
arrowheadMesh.position.set(0, -0.6, 0)
|
// we'll scale the group to the correct size later to match these sizes in screen space
|
||||||
const sphereMesh = new Mesh(new SphereGeometry(0.27, 12, 12), arrowMaterial)
|
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 arrowGroup = new Group()
|
const arrowGroup = new Group()
|
||||||
arrowGroup.userData.type = ARROWHEAD
|
arrowGroup.userData.type = ARROWHEAD
|
||||||
@ -150,6 +179,36 @@ function createArrowhead(scale = 1): Group {
|
|||||||
return arrowGroup
|
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({
|
export function tangentialArcToSegment({
|
||||||
prevSegment,
|
prevSegment,
|
||||||
from,
|
from,
|
||||||
@ -158,6 +217,7 @@ export function tangentialArcToSegment({
|
|||||||
pathToNode,
|
pathToNode,
|
||||||
isDraftSegment,
|
isDraftSegment,
|
||||||
scale = 1,
|
scale = 1,
|
||||||
|
texture,
|
||||||
}: {
|
}: {
|
||||||
prevSegment: SketchGroup['value'][number]
|
prevSegment: SketchGroup['value'][number]
|
||||||
from: Coords2d
|
from: Coords2d
|
||||||
@ -166,6 +226,7 @@ export function tangentialArcToSegment({
|
|||||||
pathToNode: PathToNode
|
pathToNode: PathToNode
|
||||||
isDraftSegment?: boolean
|
isDraftSegment?: boolean
|
||||||
scale?: number
|
scale?: number
|
||||||
|
texture: Texture
|
||||||
}): Group {
|
}): Group {
|
||||||
const group = new Group()
|
const group = new Group()
|
||||||
|
|
||||||
@ -178,12 +239,13 @@ export function tangentialArcToSegment({
|
|||||||
)
|
)
|
||||||
: prevSegment.from
|
: prevSegment.from
|
||||||
|
|
||||||
const { center, radius, startAngle, endAngle, ccw } = getTangentialArcToInfo({
|
const { center, radius, startAngle, endAngle, ccw, arcLength } =
|
||||||
arcStartPoint: from,
|
getTangentialArcToInfo({
|
||||||
arcEndPoint: to,
|
arcStartPoint: from,
|
||||||
tanPreviousPoint: previousPoint,
|
arcEndPoint: to,
|
||||||
obtuse: true,
|
tanPreviousPoint: previousPoint,
|
||||||
})
|
obtuse: true,
|
||||||
|
})
|
||||||
|
|
||||||
const geometry = createArcGeometry({
|
const geometry = createArcGeometry({
|
||||||
center,
|
center,
|
||||||
@ -219,8 +281,28 @@ export function tangentialArcToSegment({
|
|||||||
new Vector3(0, 1, 0),
|
new Vector3(0, 1, 0),
|
||||||
new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0)
|
new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0)
|
||||||
)
|
)
|
||||||
|
const pxLength = arcLength / scale
|
||||||
|
const shouldHide = pxLength < HIDE_SEGMENT_LENGTH
|
||||||
|
arrowGroup.visible = !shouldHide
|
||||||
|
|
||||||
group.add(mesh, arrowGroup)
|
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)
|
||||||
|
|
||||||
return group
|
return group
|
||||||
}
|
}
|
||||||
@ -242,8 +324,8 @@ export function createArcGeometry({
|
|||||||
isDashed?: boolean
|
isDashed?: boolean
|
||||||
scale?: number
|
scale?: number
|
||||||
}): BufferGeometry {
|
}): BufferGeometry {
|
||||||
const dashSize = 1.2 * scale
|
const dashSizePx = 18 * scale
|
||||||
const gapSize = 1.2 * scale
|
const gapSizePx = 18 * scale
|
||||||
const arcStart = new EllipseCurve(
|
const arcStart = new EllipseCurve(
|
||||||
center[0],
|
center[0],
|
||||||
center[1],
|
center[1],
|
||||||
@ -265,8 +347,8 @@ export function createArcGeometry({
|
|||||||
0
|
0
|
||||||
)
|
)
|
||||||
const shape = new Shape()
|
const shape = new Shape()
|
||||||
shape.moveTo(0, -0.08 * scale)
|
shape.moveTo(0, (-SEGMENT_WIDTH_PX / 2) * scale)
|
||||||
shape.lineTo(0, 0.08 * scale) // The width of the line
|
shape.lineTo(0, (SEGMENT_WIDTH_PX / 2) * scale) // The width of the line
|
||||||
|
|
||||||
if (!isDashed) {
|
if (!isDashed) {
|
||||||
const points = arcStart.getPoints(50)
|
const points = arcStart.getPoints(50)
|
||||||
@ -281,7 +363,7 @@ export function createArcGeometry({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const length = arcStart.getLength()
|
const length = arcStart.getLength()
|
||||||
const totalDashes = length / (dashSize + gapSize) // rounding makes the dashes jittery since the new dash is suddenly appears instead of growing into place
|
const totalDashes = length / (dashSizePx + gapSizePx) // 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 dashesAtEachEnd = Math.min(100, totalDashes / 2) // Assuming we want 50 dashes total, 25 at each end
|
||||||
|
|
||||||
const dashGeometries = []
|
const dashGeometries = []
|
||||||
@ -289,8 +371,8 @@ export function createArcGeometry({
|
|||||||
// Function to create a dash at a specific t value (0 to 1 along the curve)
|
// Function to create a dash at a specific t value (0 to 1 along the curve)
|
||||||
const createDashAt = (t: number, curve: EllipseCurve) => {
|
const createDashAt = (t: number, curve: EllipseCurve) => {
|
||||||
const startVec = curve.getPoint(t)
|
const startVec = curve.getPoint(t)
|
||||||
const endVec = curve.getPoint(Math.min(0.5, t + dashSize / length))
|
const endVec = curve.getPoint(Math.min(0.5, t + dashSizePx / length))
|
||||||
const midVec = curve.getPoint(Math.min(0.5, t + dashSize / length / 2))
|
const midVec = curve.getPoint(Math.min(0.5, t + dashSizePx / length / 2))
|
||||||
const dashCurve = new CurvePath<Vector3>()
|
const dashCurve = new CurvePath<Vector3>()
|
||||||
dashCurve.add(
|
dashCurve.add(
|
||||||
new CatmullRomCurve3([
|
new CatmullRomCurve3([
|
||||||
@ -314,7 +396,8 @@ export function createArcGeometry({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fill in the remaining arc
|
// fill in the remaining arc
|
||||||
const remainingArcLength = length - dashesAtEachEnd * 2 * (dashSize + gapSize)
|
const remainingArcLength =
|
||||||
|
length - dashesAtEachEnd * 2 * (dashSizePx + gapSizePx)
|
||||||
if (remainingArcLength > 0) {
|
if (remainingArcLength > 0) {
|
||||||
const remainingArcStartT = dashesAtEachEnd / totalDashes
|
const remainingArcStartT = dashesAtEachEnd / totalDashes
|
||||||
const remainingArcEndT = 1 - remainingArcStartT
|
const remainingArcEndT = 1 - remainingArcStartT
|
||||||
@ -359,8 +442,8 @@ export function dashedStraight(
|
|||||||
shape: Shape,
|
shape: Shape,
|
||||||
scale = 1
|
scale = 1
|
||||||
): BufferGeometry<NormalBufferAttributes> {
|
): BufferGeometry<NormalBufferAttributes> {
|
||||||
const dashSize = 1.2 * scale
|
const dashSize = 18 * scale
|
||||||
const gapSize = 1.2 * scale // todo: gabSize is not respected
|
const gapSize = 18 * scale // TODO: gapSize is not respected
|
||||||
const dashLine = new LineCurve3(
|
const dashLine = new LineCurve3(
|
||||||
new Vector3(from[0], from[1], 0),
|
new Vector3(from[0], from[1], 0),
|
||||||
new Vector3(to[0], to[1], 0)
|
new Vector3(to[0], to[1], 0)
|
||||||
|
@ -1,11 +1,21 @@
|
|||||||
:root {
|
: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
|
Generated using Catmosphere Theme Builder
|
||||||
by KittyCAD
|
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
|
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 */
|
||||||
--chalkboard-10: oklch(99.7% 0.008766 102.8deg);
|
--chalkboard-10: oklch(99.9% 0.003766 102.8deg);
|
||||||
--chalkboard-20: oklch(91.34% 0.009353 109deg);
|
--chalkboard-20: oklch(91.34% 0.009353 109deg);
|
||||||
--chalkboard-30: oklch(82.99% 0.00994 115.2deg);
|
--chalkboard-30: oklch(82.99% 0.00994 115.2deg);
|
||||||
--chalkboard-40: oklch(74.63% 0.01053 121.4deg);
|
--chalkboard-40: oklch(74.63% 0.01053 121.4deg);
|
||||||
|
@ -30,9 +30,9 @@ export const ActionIcon = ({
|
|||||||
children,
|
children,
|
||||||
}: ActionIconProps) => {
|
}: ActionIconProps) => {
|
||||||
// By default, we reverse the icon color and background color in dark mode
|
// By default, we reverse the icon color and background color in dark mode
|
||||||
const computedIconClassName = `h-auto dark:text-energy-10 !group-disabled:text-chalkboard-60 !group-disabled:text-chalkboard-60 ${iconClassName}`
|
const computedIconClassName = `h-auto text-primary dark:text-current !group-disabled:text-chalkboard-60 !group-disabled:text-chalkboard-60 ${iconClassName}`
|
||||||
|
|
||||||
const computedBgClassName = `bg-chalkboard-20 dark:bg-chalkboard-90 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 ${bgClassName}`
|
const computedBgClassName = `bg-primary/10 dark:bg-chalkboard-90 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 ${bgClassName}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|