Compare commits
83 Commits
coredump-e
...
achalmers/
Author | SHA1 | Date | |
---|---|---|---|
c6f43cb607 | |||
9df69fb1c2 | |||
24f69a2b6f | |||
cf6f474dc6 | |||
d0c244e05e | |||
a315b77f02 | |||
15c854ff18 | |||
acd3a5717d | |||
8a2555550f | |||
62e75c852a | |||
dd3601ea7b | |||
a5e7782d9a | |||
79b0b70688 | |||
1d134c1be0 | |||
1c58572234 | |||
ecee51e82b | |||
978ac42f1c | |||
893996430e | |||
41e65fc4e9 | |||
99aa74ceba | |||
0bcf33ed00 | |||
d0a9b5ecab | |||
a569f818cf | |||
f73556ba7b | |||
29cdc66b34 | |||
c9800a58d0 | |||
e46aca4992 | |||
9564890b29 | |||
f8a1f40f20 | |||
c551d88db4 | |||
8eee3e1c58 | |||
b02529cae0 | |||
cf03021366 | |||
f52d2d55f1 | |||
59b1319e50 | |||
b07bbda20b | |||
3c01924184 | |||
bd16902f02 | |||
8c3af1a72a | |||
33f5d7740d | |||
b388f60648 | |||
8f4380be74 | |||
9ae8042a57 | |||
4b676d47da | |||
e6641e68f3 | |||
450afb1605 | |||
04433fecad | |||
6567e2ff92 | |||
91c32a7fe2 | |||
f735cdc22e | |||
1b72c7df85 | |||
062abd148f | |||
c93ed0f306 | |||
27e2518dde | |||
dc6505acaf | |||
6ff3284eca | |||
4cb6ceb043 | |||
1db3e1b5e4 | |||
d797d20d50 | |||
cf52e151fb | |||
87c551b869 | |||
2001262494 | |||
777b225066 | |||
ae6373e4f5 | |||
87979b17cf | |||
4be63e7331 | |||
56d930c4f2 | |||
d48eb0c66c | |||
a69d7d03d0 | |||
00a8273173 | |||
51868f892b | |||
8e9286a747 | |||
023ed1a687 | |||
5b7d707b26 | |||
5b95194aa7 | |||
6080a99e73 | |||
5106c49e21 | |||
25f18845c7 | |||
0a7f1a41fc | |||
1625b58577 | |||
ab6115c4e2 | |||
fe621240c3 | |||
97faf5ae2b |
21
.github/workflows/ci.yml
vendored
@ -147,6 +147,14 @@ jobs:
|
||||
cp artifact/src-tauri/tauri.conf.json src-tauri/tauri.conf.json
|
||||
cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json
|
||||
|
||||
- name: Update WebView2 on Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
# Workaround needed to build the tauri windows app with matching edge version.
|
||||
# From https://github.com/actions/runner-images/issues/9538
|
||||
run: |
|
||||
Invoke-WebRequest -Uri 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' -OutFile 'setup.exe'
|
||||
Start-Process -FilePath setup.exe -Verb RunAs -Wait
|
||||
|
||||
- name: Install ubuntu system dependencies
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
@ -172,9 +180,7 @@ jobs:
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
# TODO: re-enable for Windows builds, see https://github.com/tauri-apps/tauri/issues/9045
|
||||
- name: Setup Rust cache
|
||||
if: matrix.os != 'windows-latest'
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src-tauri -> target'
|
||||
@ -364,6 +370,17 @@ jobs:
|
||||
E2E_APPLICATION: "./src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/zoo-modeling-app"
|
||||
KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
|
||||
- name: Run e2e tests (windows only)
|
||||
if: ${{ matrix.os == 'windows-latest' && github.event_name != 'release' && github.event_name != 'schedule' }}
|
||||
run: |
|
||||
cargo install tauri-driver --force
|
||||
yarn wdio run wdio.conf.ts
|
||||
env:
|
||||
E2E_APPLICATION: ".\\src-tauri\\target\\${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}\\Zoo Modeling App.exe"
|
||||
KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
VITE_KC_API_BASE_URL: ${{ env.BUILD_RELEASE == 'true' && 'https://api.zoo.dev' || 'https://api.dev.zoo.dev' }}
|
||||
E2E_TAURI_ENABLED: true
|
||||
TS_NODE_COMPILER_OPTIONS: '{"module": "commonjs"}'
|
||||
|
||||
publish-apps-release:
|
||||
runs-on: ubuntu-latest
|
||||
|
18
.github/workflows/playwright.yml
vendored
@ -46,12 +46,18 @@ jobs:
|
||||
- uses: KittyCAD/action-install-cli@main
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
- name: Cache Playwright Browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/ms-playwright/
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('yarn.lock') }}
|
||||
- name: Install Playwright Browsers
|
||||
run: yarn playwright install --with-deps
|
||||
- name: Download Wasm Cache
|
||||
id: download-wasm
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
||||
uses: dawidd6/action-download-artifact@v3
|
||||
uses: dawidd6/action-download-artifact@v5
|
||||
continue-on-error: true
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
@ -143,12 +149,20 @@ jobs:
|
||||
cache: 'yarn'
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
- name: Cache Playwright Browsers
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-playwright-
|
||||
- name: Install Playwright Browsers
|
||||
run: yarn playwright install --with-deps
|
||||
- name: Download Wasm Cache
|
||||
id: download-wasm
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
||||
uses: dawidd6/action-download-artifact@v3
|
||||
uses: dawidd6/action-download-artifact@v5
|
||||
continue-on-error: true
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
|
14
Makefile
Normal file
@ -0,0 +1,14 @@
|
||||
.PHONY: dev
|
||||
|
||||
WASM_LIB_FILES := $(wildcard src/wasm-lib/**/*.rs)
|
||||
|
||||
dev: node_modules public/wasm_lib_bg.wasm
|
||||
yarn start
|
||||
|
||||
public/wasm_lib_bg.wasm: $(WASM_LIB_FILES)
|
||||
yarn build:wasm-dev
|
||||
|
||||
node_modules: package.json
|
||||
|
||||
package.json:
|
||||
yarn install
|
55
README.md
@ -197,28 +197,32 @@ For more information on fuzzing you can check out
|
||||
|
||||
### Playwright
|
||||
|
||||
First time running plawright locally, you'll need to add the secrets file
|
||||
For a portable way to run Playwright you'll need Docker.
|
||||
|
||||
After that, open a terminal and run:
|
||||
|
||||
```bash
|
||||
touch ./e2e/playwright/playwright-secrets.env
|
||||
printf 'token="your-token"\nsnapshottoken="your-snapshot-token"' > ./e2e/playwright/playwright-secrets.env
|
||||
docker run --network host --rm --init -it playwright/chrome:playwright-1.43.1
|
||||
```
|
||||
|
||||
and in another terminal, run:
|
||||
|
||||
```bash
|
||||
PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:4444/ yarn playwright test --project="Google Chrome" <test suite>
|
||||
```
|
||||
|
||||
An example of a `<test suite>` is: `e2e/playwright/flow-tests.spec.ts`
|
||||
|
||||
YOU WILL NEED A PLAYWRIGHT-SECRETS.ENV FILE:
|
||||
|
||||
|
||||
```bash
|
||||
# ./e2e/playwright/playwright-secrets.env
|
||||
token=<your-token>
|
||||
snapshottoken=<your-snapshot-token>
|
||||
```
|
||||
then replace "your-token" with a dev token from dev.zoo.dev/account/api-tokens
|
||||
|
||||
then:
|
||||
run playwright
|
||||
|
||||
```
|
||||
yarn playwright test
|
||||
```
|
||||
|
||||
run a specific test suite
|
||||
|
||||
```
|
||||
yarn playwright test src/e2e-tests/example.spec.ts
|
||||
```
|
||||
|
||||
run a specific test change the test from `test('...` to `test.only('...`
|
||||
(note if you commit this, the tests will instantly fail without running any of the tests)
|
||||
|
||||
@ -309,6 +313,25 @@ PS: for the debug panel, the following JSON is useful for snapping the camera
|
||||
|
||||
</details>
|
||||
|
||||
### Tauri e2e tests
|
||||
|
||||
#### Windows (local only until the CI edge version mismatch is fixed)
|
||||
|
||||
```
|
||||
yarn install
|
||||
yarn build:wasm
|
||||
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
||||
yarn vite build --mode development
|
||||
yarn tauri build --debug -b
|
||||
$env:KITTYCAD_API_TOKEN="<YOUR_KITTYCAD_API_TOKEN>"
|
||||
$env:VITE_KC_API_BASE_URL="https://api.dev.zoo.dev"
|
||||
$env:E2E_TAURI_ENABLED="true"
|
||||
$env:TS_NODE_COMPILER_OPTIONS='{"module": "commonjs"}'
|
||||
$env:E2E_APPLICATION=".\src-tauri\target\debug\Zoo Modeling App.exe"
|
||||
Stop-Process -Name msedgedriver
|
||||
yarn wdio run wdio.conf.ts
|
||||
```
|
||||
|
||||
## KCL
|
||||
|
||||
For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl).
|
||||
|
@ -56095,7 +56095,8 @@
|
||||
"const part001 = startSketchOn('XY')\n |> startProfileAt([4, 12], %)\n |> line([2, 0], %)\n |> line([0, -6], %)\n |> line([4, -6], %)\n |> line([0, -6], %)\n |> line([-3.75, -4.5], %)\n |> line([0, -5.5], %)\n |> line([-2, 0], %)\n |> close(%)\n |> revolve({ axis: 'y', angle: 180 }, %)",
|
||||
"const part001 = startSketchOn('XY')\n |> startProfileAt([4, 12], %)\n |> line([2, 0], %)\n |> line([0, -6], %)\n |> line([4, -6], %)\n |> line([0, -6], %)\n |> line([-3.75, -4.5], %)\n |> line([0, -5.5], %)\n |> line([-2, 0], %)\n |> close(%)\n |> revolve({ axis: 'y', angle: 180 }, %)\nconst part002 = startSketchOn(part001, 'end')\n |> startProfileAt([4.5, -5], %)\n |> line([0, 5], %)\n |> line([5, 0], %)\n |> line([0, -5], %)\n |> close(%)\n |> extrude(5, %)",
|
||||
"const box = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([0, 20], %)\n |> line([20, 0], %)\n |> line([0, -20], %)\n |> close(%)\n |> extrude(20, %)\n\nconst sketch001 = startSketchOn(box, \"END\")\n |> circle([10, 10], 4, %)\n |> revolve({ angle: -90, axis: 'y' }, %)",
|
||||
"const box = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([0, 20], %)\n |> line([20, 0], %)\n |> line([0, -20], %, 'revolveAxis')\n |> close(%)\n |> extrude(20, %)\n\nconst sketch001 = startSketchOn(box, \"END\")\n |> circle([10, 10], 4, %)\n |> revolve({\n angle: 90,\n axis: getOppositeEdge('revolveAxis', box)\n }, %)"
|
||||
"const box = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([0, 20], %)\n |> line([20, 0], %)\n |> line([0, -20], %, 'revolveAxis')\n |> close(%)\n |> extrude(20, %)\n\nconst sketch001 = startSketchOn(box, \"END\")\n |> circle([10, 10], 4, %)\n |> revolve({\n angle: 90,\n axis: getOppositeEdge('revolveAxis', box)\n }, %)",
|
||||
"const sketch001 = startSketchOn('XY')\n |> startProfileAt([10, 0], %)\n |> line([5, -5], %)\n |> line([5, 5], %)\n |> lineTo([profileStartX(%), profileStartY(%)], %)\n |> close(%)\n\nconst part001 = revolve({\n axis: {\n custom: {\n axis: [0.0, 1.0, 0.0],\n origin: [0.0, 0.0, 0.0]\n }\n }\n}, sketch001)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -63731,7 +63732,8 @@
|
||||
"examples": [
|
||||
"const exampleSketch = startSketchOn(\"XY\")\n |> startProfileAt([0, 0], %)\n |> line([10, 0], %)\n |> line([0, 10], %)\n |> line([-10, 0], %)\n |> close(%)\n\nconst example = extrude(5, exampleSketch)\n\nconst exampleSketch002 = startSketchOn(example, 'end')\n |> startProfileAt([1, 1], %)\n |> line([8, 0], %)\n |> line([0, 8], %)\n |> line([-8, 0], %)\n |> close(%)\n\nconst example002 = extrude(5, exampleSketch002)\n\nconst exampleSketch003 = startSketchOn(example002, 'end')\n |> startProfileAt([2, 2], %)\n |> line([6, 0], %)\n |> line([0, 6], %)\n |> line([-6, 0], %)\n |> close(%)\n\nconst example003 = extrude(5, exampleSketch003)",
|
||||
"const exampleSketch = startSketchOn(\"XY\")\n |> startProfileAt([0, 0], %)\n |> line([10, 0], %)\n |> line([0, 10], %, 'sketchingFace')\n |> line([-10, 0], %)\n |> close(%)\n\nconst example = extrude(10, exampleSketch)\n\nconst exampleSketch002 = startSketchOn(example, 'sketchingFace')\n |> startProfileAt([1, 1], %)\n |> line([8, 0], %)\n |> line([0, 8], %)\n |> line([-8, 0], %)\n |> close(%, 'sketchingFace002')\n\nconst example002 = extrude(10, exampleSketch002)\n\nconst exampleSketch003 = startSketchOn(example002, 'sketchingFace002')\n |> startProfileAt([-8, 12], %)\n |> line([0, 6], %)\n |> line([6, 0], %)\n |> line([0, -6], %)\n |> close(%)\n\nconst example003 = extrude(5, exampleSketch003)",
|
||||
"const exampleSketch = startSketchOn('XY')\n |> startProfileAt([4, 12], %)\n |> line([2, 0], %)\n |> line([0, -6], %)\n |> line([4, -6], %)\n |> line([0, -6], %)\n |> line([-3.75, -4.5], %)\n |> line([0, -5.5], %)\n |> line([-2, 0], %)\n |> close(%)\n\nconst example = revolve({ axis: 'y', angle: 180 }, exampleSketch)\n\nconst exampleSketch002 = startSketchOn(example, 'end')\n |> startProfileAt([4.5, -5], %)\n |> line([0, 5], %)\n |> line([5, 0], %)\n |> line([0, -5], %)\n |> close(%)\n\nconst example002 = extrude(5, exampleSketch002)"
|
||||
"const exampleSketch = startSketchOn('XY')\n |> startProfileAt([4, 12], %)\n |> line([2, 0], %)\n |> line([0, -6], %)\n |> line([4, -6], %)\n |> line([0, -6], %)\n |> line([-3.75, -4.5], %)\n |> line([0, -5.5], %)\n |> line([-2, 0], %)\n |> close(%)\n\nconst example = revolve({ axis: 'y', angle: 180 }, exampleSketch)\n\nconst exampleSketch002 = startSketchOn(example, 'end')\n |> startProfileAt([4.5, -5], %)\n |> line([0, 5], %)\n |> line([5, 0], %)\n |> line([0, -5], %)\n |> close(%)\n\nconst example002 = extrude(5, exampleSketch002)",
|
||||
"const a1 = startSketchOn({\n plane: {\n origin: { x: 0, y: 0, z: 0 },\n x_axis: { x: 1, y: 0, z: 0 },\n y_axis: { x: 0, y: 1, z: 0 },\n z_axis: { x: 0, y: 0, z: 1 }\n }\n })\n |> startProfileAt([0, 0], %)\n |> line([100.0, 0], %)\n |> yLine(-100.0, %)\n |> xLine(-100.0, %)\n |> yLine(100.0, %)\n |> close(%)\n |> extrude(3.14, %)"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { test, expect, Download } from '@playwright/test'
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { secrets } from './secrets'
|
||||
import { getUtils } from './test-utils'
|
||||
import { Paths, doExport, getUtils } from './test-utils'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import fsp from 'fs/promises'
|
||||
import { spawn } from 'child_process'
|
||||
import { APP_NAME, KCL_DEFAULT_LENGTH } from 'lib/constants'
|
||||
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
|
||||
import JSZip from 'jszip'
|
||||
import path from 'path'
|
||||
import { TEST_SETTINGS, TEST_SETTINGS_KEY } from './storageStates'
|
||||
@ -20,6 +20,7 @@ test.beforeEach(async ({ page }) => {
|
||||
localStorage.setItem('TOKEN_PERSIST_KEY', token)
|
||||
localStorage.setItem('persistCode', ``)
|
||||
localStorage.setItem(settingsKey, settings)
|
||||
localStorage.setItem('playwright', 'true')
|
||||
},
|
||||
{
|
||||
token: secrets.token,
|
||||
@ -44,7 +45,7 @@ test.setTimeout(60_000)
|
||||
test('exports of each format should work', async ({ page, context }) => {
|
||||
// FYI this test doesn't work with only engine running locally
|
||||
// And you will need to have the KittyCAD CLI installed
|
||||
const u = getUtils(page)
|
||||
const u = await getUtils(page)
|
||||
await context.addInitScript(async () => {
|
||||
;(window as any).playwrightSkipFilePicker = true
|
||||
localStorage.setItem(
|
||||
@ -98,78 +99,6 @@ const part001 = startSketchOn('-XZ')
|
||||
await page.waitForTimeout(1000)
|
||||
await u.clearAndCloseDebugPanel()
|
||||
|
||||
interface Paths {
|
||||
modelPath: string
|
||||
imagePath: string
|
||||
outputType: string
|
||||
}
|
||||
const doExport = async (
|
||||
output: Models['OutputFormat_type']
|
||||
): Promise<Paths> => {
|
||||
await page.getByRole('button', { name: APP_NAME }).click()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Export Part' })
|
||||
).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Export Part' }).click()
|
||||
await expect(page.getByTestId('command-bar')).toBeVisible()
|
||||
|
||||
// Go through export via command bar
|
||||
await page.getByRole('option', { name: output.type, exact: false }).click()
|
||||
await page.locator('#arg-form').waitFor({ state: 'detached' })
|
||||
if ('storage' in output) {
|
||||
await page.getByTestId('arg-name-storage').waitFor({ timeout: 1000 })
|
||||
await page.getByRole('button', { name: 'storage', exact: false }).click()
|
||||
await page
|
||||
.getByRole('option', { name: output.storage, exact: false })
|
||||
.click()
|
||||
await page.locator('#arg-form').waitFor({ state: 'detached' })
|
||||
}
|
||||
await expect(page.getByText('Confirm Export')).toBeVisible()
|
||||
|
||||
const getPromiseAndResolve = () => {
|
||||
let resolve: any = () => {}
|
||||
const promise = new Promise<Download>((r) => {
|
||||
resolve = r
|
||||
})
|
||||
return [promise, resolve]
|
||||
}
|
||||
|
||||
const [downloadPromise1, downloadResolve1] = getPromiseAndResolve()
|
||||
let downloadCnt = 0
|
||||
|
||||
page.on('download', async (download) => {
|
||||
if (downloadCnt === 0) {
|
||||
downloadResolve1(download)
|
||||
}
|
||||
downloadCnt++
|
||||
})
|
||||
await page.getByRole('button', { name: 'Submit command' }).click()
|
||||
|
||||
// Handle download
|
||||
const download = await downloadPromise1
|
||||
const downloadLocationer = (extra = '', isImage = false) =>
|
||||
`./e2e/playwright/export-snapshots/${output.type}-${
|
||||
'storage' in output ? output.storage : ''
|
||||
}${extra}.${isImage ? 'png' : output.type}`
|
||||
const downloadLocation = downloadLocationer()
|
||||
|
||||
await download.saveAs(downloadLocation)
|
||||
|
||||
if (output.type === 'step') {
|
||||
// stable timestamps for step files
|
||||
const fileContents = await fsp.readFile(downloadLocation, 'utf-8')
|
||||
const newFileContents = fileContents.replace(
|
||||
/[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+[0-9]+[0-9]\+[0-9]{2}:[0-9]{2}/g,
|
||||
'1970-01-01T00:00:00.0+00:00'
|
||||
)
|
||||
await fsp.writeFile(downloadLocation, newFileContents)
|
||||
}
|
||||
return {
|
||||
modelPath: downloadLocation,
|
||||
imagePath: downloadLocationer('', true),
|
||||
outputType: output.type,
|
||||
}
|
||||
}
|
||||
const axisDirectionPair: Models['AxisDirectionPair_type'] = {
|
||||
axis: 'z',
|
||||
direction: 'positive',
|
||||
@ -185,84 +114,114 @@ const part001 = startSketchOn('-XZ')
|
||||
// just note that only `type` and `storage` are used for selecting the drop downs is the app
|
||||
// the rest are only there to make typescript happy
|
||||
exportLocations.push(
|
||||
await doExport({
|
||||
await doExport(
|
||||
{
|
||||
type: 'step',
|
||||
coords: sysType,
|
||||
})
|
||||
},
|
||||
page
|
||||
)
|
||||
)
|
||||
exportLocations.push(
|
||||
await doExport({
|
||||
await doExport(
|
||||
{
|
||||
type: 'ply',
|
||||
coords: sysType,
|
||||
selection: { type: 'default_scene' },
|
||||
storage: 'ascii',
|
||||
units: 'in',
|
||||
})
|
||||
},
|
||||
page
|
||||
)
|
||||
)
|
||||
exportLocations.push(
|
||||
await doExport({
|
||||
await doExport(
|
||||
{
|
||||
type: 'ply',
|
||||
storage: 'binary_little_endian',
|
||||
coords: sysType,
|
||||
selection: { type: 'default_scene' },
|
||||
units: 'in',
|
||||
})
|
||||
},
|
||||
page
|
||||
)
|
||||
)
|
||||
exportLocations.push(
|
||||
await doExport({
|
||||
await doExport(
|
||||
{
|
||||
type: 'ply',
|
||||
storage: 'binary_big_endian',
|
||||
coords: sysType,
|
||||
selection: { type: 'default_scene' },
|
||||
units: 'in',
|
||||
})
|
||||
},
|
||||
page
|
||||
)
|
||||
)
|
||||
exportLocations.push(
|
||||
await doExport({
|
||||
await doExport(
|
||||
{
|
||||
type: 'stl',
|
||||
storage: 'ascii',
|
||||
coords: sysType,
|
||||
units: 'in',
|
||||
selection: { type: 'default_scene' },
|
||||
})
|
||||
},
|
||||
page
|
||||
)
|
||||
)
|
||||
exportLocations.push(
|
||||
await doExport({
|
||||
await doExport(
|
||||
{
|
||||
type: 'stl',
|
||||
storage: 'binary',
|
||||
coords: sysType,
|
||||
units: 'in',
|
||||
selection: { type: 'default_scene' },
|
||||
})
|
||||
},
|
||||
page
|
||||
)
|
||||
)
|
||||
exportLocations.push(
|
||||
await doExport({
|
||||
await doExport(
|
||||
{
|
||||
// obj seems to be a little flaky, times out tests sometimes
|
||||
type: 'obj',
|
||||
coords: sysType,
|
||||
units: 'in',
|
||||
})
|
||||
},
|
||||
page
|
||||
)
|
||||
)
|
||||
exportLocations.push(
|
||||
await doExport({
|
||||
await doExport(
|
||||
{
|
||||
type: 'gltf',
|
||||
storage: 'embedded',
|
||||
presentation: 'pretty',
|
||||
})
|
||||
},
|
||||
page
|
||||
)
|
||||
)
|
||||
exportLocations.push(
|
||||
await doExport({
|
||||
await doExport(
|
||||
{
|
||||
type: 'gltf',
|
||||
storage: 'binary',
|
||||
presentation: 'pretty',
|
||||
})
|
||||
},
|
||||
page
|
||||
)
|
||||
)
|
||||
exportLocations.push(
|
||||
await doExport({
|
||||
await doExport(
|
||||
{
|
||||
type: 'gltf',
|
||||
storage: 'standard',
|
||||
presentation: 'pretty',
|
||||
})
|
||||
},
|
||||
page
|
||||
)
|
||||
)
|
||||
|
||||
// close page to disconnect websocket since we can only have one open atm
|
||||
@ -369,7 +328,7 @@ const extrudeDefaultPlane = async (context: any, page: any, plane: string) => {
|
||||
localStorage.setItem('persistCode', code)
|
||||
})
|
||||
|
||||
const u = getUtils(page)
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
@ -424,7 +383,7 @@ test.describe('extrude on default planes should be stable', () => {
|
||||
})
|
||||
|
||||
test('Draft segments should look right', async ({ page, context }) => {
|
||||
const u = getUtils(page)
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
await page.goto('/')
|
||||
@ -447,7 +406,7 @@ test('Draft segments should look right', async ({ page, context }) => {
|
||||
await page.mouse.click(700, 200)
|
||||
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`const part001 = startSketchOn('XZ')`
|
||||
`const sketch001 = startSketchOn('XZ')`
|
||||
)
|
||||
|
||||
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
|
||||
@ -455,7 +414,7 @@ test('Draft segments should look right', async ({ page, context }) => {
|
||||
const startXPx = 600
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('XZ')
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([9.06, -12.22], %)`)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
@ -469,7 +428,7 @@ test('Draft segments should look right', async ({ page, context }) => {
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('XZ')
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([9.06, -12.22], %)
|
||||
|> line([9.14, 0], %)`)
|
||||
|
||||
@ -483,7 +442,7 @@ test('Draft segments should look right', async ({ page, context }) => {
|
||||
})
|
||||
|
||||
test('Draft rectangles should look right', async ({ page, context }) => {
|
||||
const u = getUtils(page)
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
await page.goto('/')
|
||||
@ -506,7 +465,7 @@ test('Draft rectangles should look right', async ({ page, context }) => {
|
||||
await page.mouse.click(700, 200)
|
||||
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`const part001 = startSketchOn('XZ')`
|
||||
`const sketch001 = startSketchOn('XZ')`
|
||||
)
|
||||
|
||||
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||
@ -530,7 +489,7 @@ test('Draft rectangles should look right', async ({ page, context }) => {
|
||||
|
||||
test.describe('Client side scene scale should match engine scale', () => {
|
||||
test('Inch scale', async ({ page }) => {
|
||||
const u = getUtils(page)
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
await page.goto('/')
|
||||
@ -555,7 +514,7 @@ test.describe('Client side scene scale should match engine scale', () => {
|
||||
await page.mouse.click(700, 200)
|
||||
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`const part001 = startSketchOn('XZ')`
|
||||
`const sketch001 = startSketchOn('XZ')`
|
||||
)
|
||||
|
||||
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
|
||||
@ -563,7 +522,7 @@ test.describe('Client side scene scale should match engine scale', () => {
|
||||
const startXPx = 600
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('XZ')
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([9.06, -12.22], %)`)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
@ -573,7 +532,7 @@ test.describe('Client side scene scale should match engine scale', () => {
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('XZ')
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([9.06, -12.22], %)
|
||||
|> line([9.14, 0], %)`)
|
||||
|
||||
@ -583,7 +542,7 @@ test.describe('Client side scene scale should match engine scale', () => {
|
||||
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('XZ')
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([9.06, -12.22], %)
|
||||
|> line([9.14, 0], %)
|
||||
|> tangentialArcTo([27.34, -3.08], %)`)
|
||||
@ -633,7 +592,7 @@ test.describe('Client side scene scale should match engine scale', () => {
|
||||
}),
|
||||
}
|
||||
)
|
||||
const u = getUtils(page)
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
await page.goto('/')
|
||||
@ -658,7 +617,7 @@ test.describe('Client side scene scale should match engine scale', () => {
|
||||
await page.mouse.click(700, 200)
|
||||
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`const part001 = startSketchOn('XZ')`
|
||||
`const sketch001 = startSketchOn('XZ')`
|
||||
)
|
||||
|
||||
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
|
||||
@ -666,7 +625,7 @@ test.describe('Client side scene scale should match engine scale', () => {
|
||||
const startXPx = 600
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('XZ')
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([230.03, -310.32], %)`)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
@ -676,7 +635,7 @@ test.describe('Client side scene scale should match engine scale', () => {
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('XZ')
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([230.03, -310.32], %)
|
||||
|> line([232.2, 0], %)`)
|
||||
|
||||
@ -686,7 +645,7 @@ test.describe('Client side scene scale should match engine scale', () => {
|
||||
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('XZ')
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([230.03, -310.32], %)
|
||||
|> line([232.2, 0], %)
|
||||
|> tangentialArcTo([694.43, -78.12], %)`)
|
||||
@ -719,7 +678,7 @@ test.describe('Client side scene scale should match engine scale', () => {
|
||||
})
|
||||
|
||||
test('Sketch on face with none z-up', async ({ page, context }) => {
|
||||
const u = getUtils(page)
|
||||
const u = await getUtils(page)
|
||||
await context.addInitScript(async (KCL_DEFAULT_LENGTH) => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
@ -773,3 +732,76 @@ const part002 = startSketchOn(part001, 'seg01')
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
})
|
||||
|
||||
test('Zoom to fit on load - solid 2d', async ({ page, context }) => {
|
||||
const u = await getUtils(page)
|
||||
await context.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const part001 = startSketchOn('XY')
|
||||
|> startProfileAt([-10, -10], %)
|
||||
|> line([20, 0], %)
|
||||
|> line([0, 20], %)
|
||||
|> line([-20, 0], %)
|
||||
|> close(%)
|
||||
`
|
||||
)
|
||||
}, KCL_DEFAULT_LENGTH)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.openDebugPanel()
|
||||
// wait for execution done
|
||||
await expect(
|
||||
page.locator('[data-message-type="execution-done"]')
|
||||
).toHaveCount(2)
|
||||
await u.closeDebugPanel()
|
||||
|
||||
// Wait for the second extrusion to appear
|
||||
// TODO: Find a way to truly know that the objects have finished
|
||||
// rendering, because an execution-done message is not sufficient.
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
})
|
||||
|
||||
test('Zoom to fit on load - solid 3d', async ({ page, context }) => {
|
||||
const u = await getUtils(page)
|
||||
await context.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const part001 = startSketchOn('XY')
|
||||
|> startProfileAt([-10, -10], %)
|
||||
|> line([20, 0], %)
|
||||
|> line([0, 20], %)
|
||||
|> line([-20, 0], %)
|
||||
|> close(%)
|
||||
|> extrude(10, %)
|
||||
`
|
||||
)
|
||||
}, KCL_DEFAULT_LENGTH)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.openDebugPanel()
|
||||
// wait for execution done
|
||||
await expect(
|
||||
page.locator('[data-message-type="execution-done"]')
|
||||
).toHaveCount(2)
|
||||
await u.closeDebugPanel()
|
||||
|
||||
// Wait for the second extrusion to appear
|
||||
// TODO: Find a way to truly know that the objects have finished
|
||||
// rendering, because an execution-done message is not sufficient.
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 71 KiB |
After Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 42 KiB |
@ -1,8 +1,12 @@
|
||||
import { expect, Page } from '@playwright/test'
|
||||
import { test, expect, Page, Download } from '@playwright/test'
|
||||
import { EngineCommand } from '../../src/lang/std/engineConnection'
|
||||
import os from 'os'
|
||||
import fsp from 'fs/promises'
|
||||
import pixelMatch from 'pixelmatch'
|
||||
import { PNG } from 'pngjs'
|
||||
import { Protocol } from 'playwright-core/types/protocol'
|
||||
import type { Models } from '@kittycad/lib'
|
||||
import { APP_NAME } from 'lib/constants'
|
||||
|
||||
async function waitForPageLoad(page: Page) {
|
||||
// wait for 'Loading stream...' spinner
|
||||
@ -93,7 +97,80 @@ async function waitForCmdReceive(page: Page, commandType: string) {
|
||||
.waitFor()
|
||||
}
|
||||
|
||||
export function getUtils(page: Page) {
|
||||
export const wiggleMove = async (
|
||||
page: any,
|
||||
x: number,
|
||||
y: number,
|
||||
steps: number,
|
||||
dist: number,
|
||||
ang: number,
|
||||
amplitude: number,
|
||||
freq: number
|
||||
) => {
|
||||
const tau = Math.PI * 2
|
||||
const deg = tau / 360
|
||||
const step = dist / steps
|
||||
for (let i = 0, j = 0; i < dist; i += step, j += 1) {
|
||||
const [x1, y1] = [0, Math.sin((tau / steps) * j * freq) * amplitude]
|
||||
const [x2, y2] = [
|
||||
Math.cos(-ang * deg) * i - Math.sin(-ang * deg) * y1,
|
||||
Math.sin(-ang * deg) * i + Math.cos(-ang * deg) * y1,
|
||||
]
|
||||
const [xr, yr] = [x2, y2]
|
||||
await page.mouse.move(x + xr, y + yr, { steps: 2 })
|
||||
}
|
||||
}
|
||||
|
||||
export const getMovementUtils = (opts: any) => {
|
||||
// The way we truncate is kinda odd apparently, so we need this function
|
||||
// "[k]itty[c]ad round"
|
||||
const kcRound = (n: number) => Math.trunc(n * 100) / 100
|
||||
|
||||
// To translate between screen and engine ("[U]nit") coordinates
|
||||
// NOTE: these pretty much can't be perfect because of screen scaling.
|
||||
// Handle on a case-by-case.
|
||||
const toU = (x: number, y: number) => [
|
||||
kcRound(x * 0.0854),
|
||||
kcRound(-y * 0.0854), // Y is inverted in our coordinate system
|
||||
]
|
||||
|
||||
// Turn the array into a string with specific formatting
|
||||
const fromUToString = (xy: number[]) => `[${xy[0]}, ${xy[1]}]`
|
||||
|
||||
// Combine because used often
|
||||
const toSU = (xy: number[]) => fromUToString(toU(xy[0], xy[1]))
|
||||
|
||||
// Make it easier to click around from center ("click [from] zero zero")
|
||||
const click00 = (x: number, y: number) =>
|
||||
opts.page.mouse.click(opts.center.x + x, opts.center.y + y)
|
||||
|
||||
// Relative clicker, must keep state
|
||||
let last = { x: 0, y: 0 }
|
||||
const click00r = (x?: number, y?: number) => {
|
||||
// reset relative coordinates when anything is undefined
|
||||
if (x === undefined || y === undefined) {
|
||||
last.x = 0
|
||||
last.y = 0
|
||||
return
|
||||
}
|
||||
|
||||
const ret = click00(last.x + x, last.y + y)
|
||||
last.x += x
|
||||
last.y += y
|
||||
|
||||
// Returns the new absolute coordinate if you need it.
|
||||
return ret.then(() => [last.x, last.y])
|
||||
}
|
||||
|
||||
return { toSU, click00r }
|
||||
}
|
||||
|
||||
export async function getUtils(page: Page) {
|
||||
// Chrome devtools protocol session only works in Chromium
|
||||
const browserType = page.context().browser()?.browserType().name()
|
||||
const cdpSession =
|
||||
browserType !== 'chromium' ? null : await page.context().newCDPSession(page)
|
||||
|
||||
return {
|
||||
waitForAuthSkipAppStart: () => waitForPageLoad(page),
|
||||
removeCurrentCode: () => removeCurrentCode(page),
|
||||
@ -124,6 +201,29 @@ export function getUtils(page: Page) {
|
||||
},
|
||||
waitForCmdReceive: (commandType: string) =>
|
||||
waitForCmdReceive(page, commandType),
|
||||
getSegmentBodyCoords: async (locator: string, px = 30) => {
|
||||
const overlay = page.locator(locator)
|
||||
const bbox = await overlay
|
||||
.boundingBox()
|
||||
.then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 }))
|
||||
const angle = Number(await overlay.getAttribute('data-overlay-angle'))
|
||||
const angleXOffset = Math.cos(((angle - 180) * Math.PI) / 180) * px
|
||||
const angleYOffset = Math.sin(((angle - 180) * Math.PI) / 180) * px
|
||||
return {
|
||||
x: Math.round(bbox.x + angleXOffset),
|
||||
y: Math.round(bbox.y - angleYOffset),
|
||||
}
|
||||
},
|
||||
getAngle: async (locator: string) => {
|
||||
const overlay = page.locator(locator)
|
||||
return Number(await overlay.getAttribute('data-overlay-angle'))
|
||||
},
|
||||
getBoundingBox: async (locator: string) =>
|
||||
page
|
||||
.locator(locator)
|
||||
.boundingBox()
|
||||
.then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })),
|
||||
codeLocator: page.locator('.cm-content'),
|
||||
doAndWaitForCmd: async (
|
||||
fn: () => Promise<void>,
|
||||
commandType: string,
|
||||
@ -139,6 +239,30 @@ export function getUtils(page: Page) {
|
||||
await closeDebugPanel(page)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Given an expected RGB value, diff if the channel with the largest difference
|
||||
*/
|
||||
getGreatestPixDiff: async (
|
||||
coords: { x: number; y: number },
|
||||
expected: [number, number, number]
|
||||
): Promise<number> => {
|
||||
const buffer = await page.screenshot({
|
||||
fullPage: true,
|
||||
})
|
||||
const screenshot = await PNG.sync.read(buffer)
|
||||
// most likely related to pixel density but the screenshots for webkit are 2x the size
|
||||
// there might be a more robust way of doing this.
|
||||
const pixMultiplier = browserType === 'webkit' ? 2 : 1
|
||||
const index =
|
||||
(screenshot.width * coords.y * pixMultiplier +
|
||||
coords.x * pixMultiplier) *
|
||||
4 // rbga is 4 channels
|
||||
return Math.max(
|
||||
Math.abs(screenshot.data[index] - expected[0]),
|
||||
Math.abs(screenshot.data[index + 1] - expected[1]),
|
||||
Math.abs(screenshot.data[index + 2] - expected[2])
|
||||
)
|
||||
},
|
||||
doAndWaitForImageDiff: (fn: () => Promise<any>, diffCount = 200) =>
|
||||
new Promise(async (resolve) => {
|
||||
await page.screenshot({
|
||||
@ -180,6 +304,17 @@ export function getUtils(page: Page) {
|
||||
}
|
||||
}, 50)
|
||||
}),
|
||||
emulateNetworkConditions: async (
|
||||
networkOptions: Protocol.Network.emulateNetworkConditionsParameters
|
||||
) => {
|
||||
// Skip on non-Chromium browsers, since we need to use the CDP.
|
||||
test.skip(
|
||||
cdpSession === null,
|
||||
'Network emulation is only supported in Chromium'
|
||||
)
|
||||
|
||||
cdpSession?.send('Network.emulateNetworkConditions', networkOptions)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -255,3 +390,82 @@ export const makeTemplate: (
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export interface Paths {
|
||||
modelPath: string
|
||||
imagePath: string
|
||||
outputType: string
|
||||
}
|
||||
|
||||
export const doExport = async (
|
||||
output: Models['OutputFormat_type'],
|
||||
page: Page
|
||||
): Promise<Paths> => {
|
||||
await page.getByRole('button', { name: APP_NAME }).click()
|
||||
await expect(page.getByRole('button', { name: 'Export Part' })).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Export Part' }).click()
|
||||
await expect(page.getByTestId('command-bar')).toBeVisible()
|
||||
|
||||
// Go through export via command bar
|
||||
await page.getByRole('option', { name: output.type, exact: false }).click()
|
||||
await page.locator('#arg-form').waitFor({ state: 'detached' })
|
||||
if ('storage' in output) {
|
||||
await page.getByTestId('arg-name-storage').waitFor({ timeout: 1000 })
|
||||
await page.getByRole('button', { name: 'storage', exact: false }).click()
|
||||
await page
|
||||
.getByRole('option', { name: output.storage, exact: false })
|
||||
.click()
|
||||
await page.locator('#arg-form').waitFor({ state: 'detached' })
|
||||
}
|
||||
await expect(page.getByText('Confirm Export')).toBeVisible()
|
||||
|
||||
const getPromiseAndResolve = () => {
|
||||
let resolve: any = () => {}
|
||||
const promise = new Promise<Download>((r) => {
|
||||
resolve = r
|
||||
})
|
||||
return [promise, resolve]
|
||||
}
|
||||
|
||||
const [downloadPromise1, downloadResolve1] = getPromiseAndResolve()
|
||||
let downloadCnt = 0
|
||||
|
||||
page.on('download', async (download) => {
|
||||
if (downloadCnt === 0) {
|
||||
downloadResolve1(download)
|
||||
}
|
||||
downloadCnt++
|
||||
})
|
||||
await page.getByRole('button', { name: 'Submit command' }).click()
|
||||
|
||||
// Handle download
|
||||
const download = await downloadPromise1
|
||||
const downloadLocationer = (extra = '', isImage = false) =>
|
||||
`./e2e/playwright/export-snapshots/${output.type}-${
|
||||
'storage' in output ? output.storage : ''
|
||||
}${extra}.${isImage ? 'png' : output.type}`
|
||||
const downloadLocation = downloadLocationer()
|
||||
|
||||
await download.saveAs(downloadLocation)
|
||||
|
||||
if (output.type === 'step') {
|
||||
// stable timestamps for step files
|
||||
const fileContents = await fsp.readFile(downloadLocation, 'utf-8')
|
||||
const newFileContents = fileContents.replace(
|
||||
/[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+[0-9]+[0-9]\+[0-9]{2}:[0-9]{2}/g,
|
||||
'1970-01-01T00:00:00.0+00:00'
|
||||
)
|
||||
await fsp.writeFile(downloadLocation, newFileContents)
|
||||
}
|
||||
|
||||
return {
|
||||
modelPath: downloadLocation,
|
||||
imagePath: downloadLocationer('', true),
|
||||
outputType: output.type,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the appropriate modifier key for the platform.
|
||||
*/
|
||||
export const metaModifier = os.platform() === 'darwin' ? 'Meta' : 'Control'
|
||||
|
@ -1,11 +1,19 @@
|
||||
import { browser, $, expect } from '@wdio/globals'
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import os from 'os'
|
||||
|
||||
const documentsDir = `${process.env.HOME}/Documents`
|
||||
const userSettingsDir = `${process.env.HOME}/.config/dev.zoo.modeling-app`
|
||||
const defaultProjectDir = `${documentsDir}/zoo-modeling-app-projects`
|
||||
const newProjectDir = `${documentsDir}/a-different-directory`
|
||||
const userCodeDir = '/tmp/kittycad_user_code'
|
||||
const isWin32 = os.platform() === 'win32'
|
||||
const documentsDir = path.join(os.homedir(), 'Documents')
|
||||
const userSettingsDir = path.join(
|
||||
os.homedir(),
|
||||
'.config',
|
||||
'dev.zoo.modeling-app'
|
||||
)
|
||||
const defaultProjectDir = path.join(documentsDir, 'zoo-modeling-app-projects')
|
||||
const newProjectDir = path.join(documentsDir, 'a-different-directory')
|
||||
const tmp = process.env.TEMP || '/tmp'
|
||||
const userCodeDir = path.join(tmp, 'kittycad_user_code')
|
||||
|
||||
async function click(element: WebdriverIO.Element): Promise<void> {
|
||||
// Workaround for .click(), see https://github.com/tauri-apps/tauri/issues/6541
|
||||
@ -24,7 +32,7 @@ async function setDatasetValue(
|
||||
await browser.execute(`arguments[0].dataset.${property} = "${value}"`, field)
|
||||
}
|
||||
|
||||
describe('ZMA (Tauri, Linux)', () => {
|
||||
describe('ZMA (Tauri)', () => {
|
||||
it('opens the auth page and signs in', async () => {
|
||||
// Clean up filesystem from previous tests
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
@ -42,9 +50,7 @@ describe('ZMA (Tauri, Linux)', () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
|
||||
// Get from main.rs
|
||||
const userCode = await (
|
||||
await fs.readFile('/tmp/kittycad_user_code')
|
||||
).toString()
|
||||
const userCode = await (await fs.readFile(userCodeDir)).toString()
|
||||
console.log(`Found user code ${userCode}`)
|
||||
|
||||
// Device flow: verify
|
||||
@ -92,7 +98,12 @@ describe('ZMA (Tauri, Linux)', () => {
|
||||
* 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)
|
||||
// TODO: understand why we need to force double \ on Windows
|
||||
await setDatasetValue(
|
||||
projectDirInput,
|
||||
'testValue',
|
||||
isWin32 ? newProjectDir.replaceAll('\\', '\\\\') : newProjectDir
|
||||
)
|
||||
const projectDirButton = await $('[data-testid="project-directory-button"]')
|
||||
await click(projectDirButton)
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
@ -102,6 +113,15 @@ describe('ZMA (Tauri, Linux)', () => {
|
||||
const nameInput = await $('[data-testid="projects-defaultProjectName"]')
|
||||
expect(await nameInput.getValue()).toEqual('project-$nnn')
|
||||
|
||||
// Setting it back (for back to back local tests)
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000))
|
||||
await setDatasetValue(
|
||||
projectDirInput,
|
||||
'testValue',
|
||||
isWin32 ? defaultProjectDir.replaceAll('\\', '\\\\') : newProjectDir
|
||||
)
|
||||
await click(projectDirButton)
|
||||
|
||||
const closeButton = await $('[data-testid="settings-close-button"]')
|
||||
await click(closeButton)
|
||||
})
|
||||
@ -120,12 +140,19 @@ describe('ZMA (Tauri, Linux)', () => {
|
||||
it('opens the new file and expects a loading stream', async () => {
|
||||
const projectLink = await $('[data-testid="project-link"]')
|
||||
await click(projectLink)
|
||||
if (isWin32) {
|
||||
// TODO: actually do something to check that the stream is up
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000))
|
||||
} else {
|
||||
const errorText = await $('[data-testid="unexpected-error"]')
|
||||
expect(await errorText.getText()).toContain('unexpected error')
|
||||
await browser.execute('window.location.href = "tauri://localhost/home"')
|
||||
}
|
||||
const base = isWin32 ? 'http://tauri.localhost' : 'tauri://localhost'
|
||||
await browser.execute(`window.location.href = "${base}/home"`)
|
||||
})
|
||||
|
||||
it('signs out', async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
|
||||
await click(menuButton)
|
||||
const signoutButton = await $('[data-testid="user-sidebar-sign-out"]')
|
||||
|
19
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "untitled-app",
|
||||
"version": "0.21.7",
|
||||
"version": "0.22.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.16.0",
|
||||
@ -10,12 +10,12 @@
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@kittycad/lib": "^0.0.60",
|
||||
"@kittycad/lib": "^0.0.64",
|
||||
"@lezer/javascript": "^1.4.9",
|
||||
"@open-rpc/client-js": "^1.8.1",
|
||||
"@react-hook/resize-observer": "^1.2.6",
|
||||
"@react-hook/resize-observer": "^2.0.1",
|
||||
"@replit/codemirror-interact": "^6.3.1",
|
||||
"@tauri-apps/api": "2.0.0-beta.8",
|
||||
"@tauri-apps/api": "2.0.0-beta.12",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0-beta.2",
|
||||
"@tauri-apps/plugin-fs": "^2.0.0-beta.3",
|
||||
"@tauri-apps/plugin-http": "^2.0.0-beta.2",
|
||||
@ -37,7 +37,7 @@
|
||||
"crypto-js": "^4.2.0",
|
||||
"debounce-promise": "^3.1.2",
|
||||
"decamelize": "^6.0.0",
|
||||
"formik": "^2.4.5",
|
||||
"formik": "^2.4.6",
|
||||
"fuse.js": "^7.0.0",
|
||||
"html2canvas-pro": "^1.4.3",
|
||||
"http-server": "^14.1.1",
|
||||
@ -61,11 +61,11 @@
|
||||
"ua-parser-js": "^1.0.37",
|
||||
"uuid": "^9.0.1",
|
||||
"vitest": "^1.6.0",
|
||||
"vscode-jsonrpc": "^8.1.0",
|
||||
"vscode-jsonrpc": "^8.2.1",
|
||||
"vscode-languageserver-protocol": "^3.17.5",
|
||||
"wasm-pack": "^0.12.1",
|
||||
"web-vitals": "^3.5.2",
|
||||
"ws": "^8.16.0",
|
||||
"ws": "^8.17.0",
|
||||
"xstate": "^4.38.2",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
@ -95,7 +95,8 @@
|
||||
"lint": "eslint --fix src",
|
||||
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json",
|
||||
"postinstall": "yarn xstate:typegen",
|
||||
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\""
|
||||
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"",
|
||||
"make:dev": "make dev"
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "es5",
|
||||
@ -133,7 +134,7 @@
|
||||
"@types/wait-on": "^5.3.4",
|
||||
"@types/wicg-file-system-access": "^2023.10.5",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"@vitest/web-worker": "^1.5.0",
|
||||
"@wdio/cli": "^8.24.3",
|
||||
"@wdio/globals": "^8.36.0",
|
||||
|
@ -12,12 +12,12 @@ import { defineConfig, devices } from '@playwright/test'
|
||||
export default defineConfig({
|
||||
testDir: './e2e/playwright',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
fullyParallel: false,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 3 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
/* Different amount of parallelism on CI and local. */
|
||||
workers: process.env.CI ? 1 : 1,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
@ -34,7 +34,14 @@ export default defineConfig({
|
||||
projects: [
|
||||
{
|
||||
name: 'Google Chrome',
|
||||
use: { ...devices['Desktop Chrome'], channel: 'chrome' }, // or 'chrome-beta'
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
channel: 'chrome',
|
||||
contextOptions: {
|
||||
/* Chromium is the only one with these permission types */
|
||||
permissions: ['clipboard-write', 'clipboard-read'],
|
||||
},
|
||||
}, // or 'chrome-beta'
|
||||
},
|
||||
{
|
||||
name: 'webkit',
|
||||
@ -72,7 +79,7 @@ export default defineConfig({
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'yarn serve',
|
||||
command: 'yarn start',
|
||||
// url: 'http://127.0.0.1:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
|
243
src-tauri/Cargo.lock
generated
@ -195,6 +195,7 @@ dependencies = [
|
||||
"tauri-plugin-http",
|
||||
"tauri-plugin-log",
|
||||
"tauri-plugin-os",
|
||||
"tauri-plugin-persisted-scope",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-updater",
|
||||
@ -343,7 +344,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -378,7 +379,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -424,7 +425,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -573,7 +574,7 @@ dependencies = [
|
||||
"proc-macro-crate 3.1.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
"syn_derive",
|
||||
]
|
||||
|
||||
@ -882,7 +883,7 @@ dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1084,7 +1085,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1094,7 +1095,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1118,7 +1119,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.10.0",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1129,7 +1130,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1176,7 +1177,7 @@ checksum = "377af281d8f23663862a7c84623bc5dcf7f8c44b13c7496a590bdc157f941a43"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@ -1213,7 +1214,7 @@ dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_tokenstream",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1224,7 +1225,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1286,7 +1287,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1318,7 +1319,7 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1416,7 +1417,7 @@ checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1567,7 +1568,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1683,7 +1684,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1959,7 +1960,7 @@ dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1987,7 +1988,7 @@ dependencies = [
|
||||
"inflections",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2062,7 +2063,7 @@ dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2460,15 +2461,6 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.1"
|
||||
@ -2567,7 +2559,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-lib"
|
||||
version = "0.1.56"
|
||||
version = "0.1.58"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"approx",
|
||||
@ -2590,7 +2582,7 @@ dependencies = [
|
||||
"kittycad-execution-plan-traits",
|
||||
"lazy_static",
|
||||
"mime_guess",
|
||||
"parse-display 0.9.0",
|
||||
"parse-display",
|
||||
"reqwest 0.11.27",
|
||||
"ropey",
|
||||
"schemars",
|
||||
@ -2610,7 +2602,7 @@ dependencies = [
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"winnow 0.5.40",
|
||||
"zip 1.3.0",
|
||||
"zip 2.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2626,13 +2618,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kittycad"
|
||||
version = "0.3.1"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c6e12eb45fd9a28c8e99dbdef54556246b39acee14e4aa6f0fc43636caa62d9"
|
||||
checksum = "b0cbef813153197e60c0e96f59eea0b75f8418380f414b20250ee81b60e522c3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64 0.21.7",
|
||||
"base64 0.22.1",
|
||||
"bigdecimal",
|
||||
"bytes",
|
||||
"chrono",
|
||||
@ -2641,10 +2633,10 @@ dependencies = [
|
||||
"format_serde_error",
|
||||
"futures",
|
||||
"http 0.2.12",
|
||||
"itertools 0.10.5",
|
||||
"itertools",
|
||||
"log",
|
||||
"mime_guess",
|
||||
"parse-display 0.8.2",
|
||||
"parse-display",
|
||||
"phonenumber",
|
||||
"rand 0.8.5",
|
||||
"reqwest 0.11.27",
|
||||
@ -2671,7 +2663,7 @@ checksum = "0611fc9b9786175da21d895ffa0f65039e19c9111e94a41b7af999e3b95f045f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3359,43 +3351,17 @@ dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parse-display"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6509d08722b53e8dafe97f2027b22ccbe3a5db83cb352931e9716b0aa44bc5c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"parse-display-derive 0.8.2",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parse-display"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06af5f9333eb47bd9ba8462d612e37a8328a5cb80b13f0af4de4c3b89f52dee5"
|
||||
dependencies = [
|
||||
"parse-display-derive 0.9.0",
|
||||
"parse-display-derive",
|
||||
"regex",
|
||||
"regex-syntax 0.8.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parse-display-derive"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68517892c8daf78da08c0db777fcc17e07f2f63ef70041718f8a7630ad84f341"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"regex-syntax 0.7.5",
|
||||
"structmeta 0.2.0",
|
||||
"syn 2.0.65",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parse-display-derive"
|
||||
version = "0.9.0"
|
||||
@ -3406,8 +3372,8 @@ dependencies = [
|
||||
"quote",
|
||||
"regex",
|
||||
"regex-syntax 0.8.3",
|
||||
"structmeta 0.3.0",
|
||||
"syn 2.0.65",
|
||||
"structmeta",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3549,7 +3515,7 @@ dependencies = [
|
||||
"phf_shared 0.11.2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3581,14 +3547,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "phonenumber"
|
||||
version = "0.3.4+8.13.34"
|
||||
version = "0.3.5+8.13.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d888d375f2963bf06c5079665fbe53db69860879ff5a78524fe3c93c54fb7b8"
|
||||
checksum = "f174c8db59b620032bd52b655fc97000458850fec0db35fcd4e802b668517ec0"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"either",
|
||||
"fnv",
|
||||
"itertools 0.12.1",
|
||||
"itertools",
|
||||
"lazy_static",
|
||||
"nom",
|
||||
"quick-xml",
|
||||
@ -3617,7 +3583,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3769,9 +3735,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.81"
|
||||
version = "1.0.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
|
||||
checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@ -4001,12 +3967,6 @@ version = "0.6.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.3"
|
||||
@ -4455,7 +4415,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4540,9 +4500,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.202"
|
||||
version = "1.0.203"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
|
||||
checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@ -4558,13 +4518,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.202"
|
||||
version = "1.0.203"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
|
||||
checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4575,7 +4535,7 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4608,7 +4568,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4629,7 +4589,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4671,7 +4631,7 @@ dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4930,18 +4890,6 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "structmeta"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ad9e09554f0456d67a69c1584c9798ba733a5b50349a6c0d0948710523922d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"structmeta-derive 0.2.0",
|
||||
"syn 2.0.65",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "structmeta"
|
||||
version = "0.3.0"
|
||||
@ -4950,19 +4898,8 @@ checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"structmeta-derive 0.3.0",
|
||||
"syn 2.0.65",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "structmeta-derive"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a60bcaff7397072dca0017d1db428e30d5002e00b6847703e2e42005c95fbe00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"structmeta-derive",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4973,7 +4910,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4995,7 +4932,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5028,9 +4965,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.65"
|
||||
version = "2.0.66"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106"
|
||||
checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -5046,7 +4983,7 @@ dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5063,7 +5000,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5280,7 +5217,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
"tauri-utils",
|
||||
"thiserror",
|
||||
"time",
|
||||
@ -5298,7 +5235,7 @@ dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
"tauri-codegen",
|
||||
"tauri-utils",
|
||||
]
|
||||
@ -5370,9 +5307,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs"
|
||||
version = "2.0.0-beta.6"
|
||||
version = "2.0.0-beta.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "609f53d90f08808679ecdd81455d9a4d0053291b92780695569f7400fdba27d5"
|
||||
checksum = "35377195c6923beda5f29482a16b492d431de964389fca9aaf81a0f7e908023f"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"glob",
|
||||
@ -5447,6 +5384,22 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-persisted-scope"
|
||||
version = "2.0.0-beta.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b600c870217ec082b0f3482935a8edad347d18e8cd7d422a74bc92d035c0e394"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bincode",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin-fs",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-process"
|
||||
version = "2.0.0-beta.3"
|
||||
@ -5648,7 +5601,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5737,7 +5690,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5926,7 +5879,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5955,7 +5908,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -6059,9 +6012,8 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs"
|
||||
version = "7.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc2cae1fc5d05d47aa24b64f9a4f7cba24cdc9187a2084dd97ac57bef5eccae6"
|
||||
version = "8.1.0"
|
||||
source = "git+https://github.com/Aleph-Alpha/ts-rs#f898578d80d3e2a54080c1c046c45f9eaa2435c3"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"thiserror",
|
||||
@ -6072,14 +6024,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs-macros"
|
||||
version = "7.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73f7f9b821696963053a89a7bd8b292dc34420aea8294d7b225274d488f3ec92"
|
||||
version = "8.1.0"
|
||||
source = "git+https://github.com/Aleph-Alpha/ts-rs#f898578d80d3e2a54080c1c046c45f9eaa2435c3"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
@ -6292,7 +6242,7 @@ dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -6391,7 +6341,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@ -6425,7 +6375,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@ -6566,7 +6516,7 @@ checksum = "ac1345798ecd8122468840bcdf1b95e5dc6d2206c5e4b0eafa078d061f59c9bc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -6672,7 +6622,7 @@ checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -6683,7 +6633,7 @@ checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -7125,7 +7075,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.65",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -7156,15 +7106,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "1.3.0"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1f4a27345eb6f7aa7bd015ba7eb4175fa4e1b462a29874b779e0bbcf96c6ac7"
|
||||
checksum = "1dd56a4d5921bc2f99947ac5b3abe5f510b1be7376fdc5e9fce4a23c6a93e87c"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"displaydoc",
|
||||
"indexmap 2.2.6",
|
||||
"memchr",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
|
@ -28,6 +28,7 @@ tauri-plugin-fs = { version = "2.0.0-beta.6" }
|
||||
tauri-plugin-http = { version = "2.0.0-beta.6" }
|
||||
tauri-plugin-log = { version = "2.0.0-beta.4" }
|
||||
tauri-plugin-os = { version = "2.0.0-beta.2" }
|
||||
tauri-plugin-persisted-scope = { version = "2.0.0-beta.7" }
|
||||
tauri-plugin-process = { version = "2.0.0-beta.2" }
|
||||
tauri-plugin-shell = { version = "2.0.0-beta.2" }
|
||||
tauri-plugin-updater = { version = "2.0.0-beta.4" }
|
||||
|
@ -32,6 +32,15 @@
|
||||
{
|
||||
"identifier": "fs:scope",
|
||||
"allow": [
|
||||
{
|
||||
"path": "$TEMP"
|
||||
},
|
||||
{
|
||||
"path": "$TEMP/**/*"
|
||||
},
|
||||
{
|
||||
"path": "$HOME"
|
||||
},
|
||||
{
|
||||
"path": "$HOME/**/*"
|
||||
},
|
||||
@ -56,6 +65,33 @@
|
||||
]
|
||||
},
|
||||
"shell:allow-open",
|
||||
{
|
||||
"identifier": "shell:allow-execute",
|
||||
"allow": [
|
||||
{
|
||||
"name": "open",
|
||||
"cmd": "open",
|
||||
"args": [
|
||||
"-R",
|
||||
{
|
||||
"validator": "\\S+"
|
||||
}
|
||||
],
|
||||
"sidecar": false
|
||||
},
|
||||
{
|
||||
"name": "explorer",
|
||||
"cmd": "explorer",
|
||||
"args": [
|
||||
"/select",
|
||||
{
|
||||
"validator": "\\S+"
|
||||
}
|
||||
],
|
||||
"sidecar": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"dialog:allow-open",
|
||||
"dialog:allow-save",
|
||||
"dialog:allow-message",
|
||||
|
@ -18,7 +18,6 @@ use oauth2::TokenResponse;
|
||||
use tauri::{ipc::InvokeError, Manager};
|
||||
use tauri_plugin_cli::CliExt;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use tokio::process::Command;
|
||||
|
||||
const DEFAULT_HOST: &str = "https://api.zoo.dev";
|
||||
const SETTINGS_FILE_NAME: &str = "settings.toml";
|
||||
@ -268,7 +267,15 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
|
||||
let e2e_tauri_enabled = env::var("E2E_TAURI_ENABLED").is_ok();
|
||||
if e2e_tauri_enabled {
|
||||
log::warn!("E2E_TAURI_ENABLED is set, won't open {} externally", auth_uri.secret());
|
||||
tokio::fs::write("/tmp/kittycad_user_code", details.user_code().secret())
|
||||
let mut temp = String::from("/tmp");
|
||||
// Overwrite with Windows variable
|
||||
match env::var("TEMP") {
|
||||
Ok(val) => temp = val,
|
||||
Err(_e) => println!("Fallback to default /tmp"),
|
||||
}
|
||||
let path = Path::new(&temp).join("kittycad_user_code");
|
||||
println!("Writing to {}", path.to_string_lossy());
|
||||
tokio::fs::write(path, details.user_code().secret())
|
||||
.await
|
||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||
} else {
|
||||
@ -332,10 +339,20 @@ async fn get_user(token: &str, hostname: &str) -> Result<kittycad::types::User,
|
||||
/// From this GitHub comment: https://github.com/tauri-apps/tauri/issues/4062#issuecomment-1338048169
|
||||
/// But with the Linux support removed since we don't need it for now.
|
||||
#[tauri::command]
|
||||
fn show_in_folder(path: &str) -> Result<(), InvokeError> {
|
||||
fn show_in_folder(app: tauri::AppHandle, path: &str) -> Result<(), InvokeError> {
|
||||
// Check if the file exists.
|
||||
// If it doesn't, return an error.
|
||||
if !Path::new(path).exists() {
|
||||
return Err(InvokeError::from_anyhow(anyhow::anyhow!(
|
||||
"The file `{}` does not exist",
|
||||
path
|
||||
)));
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
Command::new("explorer")
|
||||
app.shell()
|
||||
.command("explorer")
|
||||
.args(["/select,", path]) // The comma after select is not a typo
|
||||
.spawn()
|
||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||
@ -343,7 +360,8 @@ fn show_in_folder(path: &str) -> Result<(), InvokeError> {
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
Command::new("open")
|
||||
app.shell()
|
||||
.command("open")
|
||||
.args(["-R", path])
|
||||
.spawn()
|
||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||
@ -415,6 +433,7 @@ fn main() -> Result<()> {
|
||||
.build(),
|
||||
)
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_persisted_scope::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.setup(|app| {
|
||||
|
@ -74,5 +74,5 @@
|
||||
}
|
||||
},
|
||||
"productName": "Zoo Modeling App",
|
||||
"version": "0.21.7"
|
||||
"version": "0.22.0"
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import { ModelingSidebar } from 'components/ModelingSidebar/ModelingSidebar'
|
||||
import { LowerRightControls } from 'components/LowerRightControls'
|
||||
import ModalContainer from 'react-modal-promise'
|
||||
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
||||
import Gizmo from 'components/Gizmo'
|
||||
|
||||
export function App() {
|
||||
useRefreshSettings(paths.FILE + 'SETTINGS')
|
||||
@ -128,7 +129,9 @@ export function App() {
|
||||
<ModelingSidebar paneOpacity={paneOpacity} />
|
||||
<Stream className="absolute inset-0 z-0" />
|
||||
{/* <CamToggle /> */}
|
||||
<LowerRightControls />
|
||||
<LowerRightControls>
|
||||
<Gizmo />
|
||||
</LowerRightControls>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -12,6 +12,8 @@ import SignIn from './routes/SignIn'
|
||||
import { Auth } from './Auth'
|
||||
import { isTauri } from './lib/isTauri'
|
||||
import Home from './routes/Home'
|
||||
import { NetworkContext } from './hooks/useNetworkContext'
|
||||
import { useNetworkStatus } from './hooks/useNetworkStatus'
|
||||
import makeUrlPathRelative from './lib/makeUrlPathRelative'
|
||||
import DownloadAppBanner from 'components/DownloadAppBanner'
|
||||
import { WasmErrBanner } from 'components/WasmErrBanner'
|
||||
@ -155,5 +157,11 @@ const router = createBrowserRouter([
|
||||
* @returns RouterProvider
|
||||
*/
|
||||
export const Router = () => {
|
||||
return <RouterProvider router={router} />
|
||||
const networkStatus = useNetworkStatus()
|
||||
|
||||
return (
|
||||
<NetworkContext.Provider value={networkStatus}>
|
||||
<RouterProvider router={router} />
|
||||
</NetworkContext.Provider>
|
||||
)
|
||||
}
|
||||
|
110
src/Toolbar.tsx
@ -3,22 +3,22 @@ import { isCursorInSketchCommandRange } from 'lang/util'
|
||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
import { isSingleCursorInPipe } from 'lang/queryAst'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
import {
|
||||
NetworkHealthState,
|
||||
useNetworkStatus,
|
||||
} from 'components/NetworkHealthIndicator'
|
||||
import { useStore } from 'useStore'
|
||||
import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
|
||||
export const Toolbar = () => {
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
export function Toolbar({
|
||||
className = '',
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLElement>) {
|
||||
const { state, send, context } = useModelingContext()
|
||||
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const iconClassName =
|
||||
'group-disabled:text-chalkboard-50 group-enabled:group-hover:!text-primary dark:group-enabled:group-hover:!text-inherit group-pressed:!text-chalkboard-10 group-ui-open:!text-chalkboard-10 dark:group-ui-open:!text-chalkboard-10'
|
||||
const bgClassName =
|
||||
@ -34,13 +34,18 @@ export const Toolbar = () => {
|
||||
context.selectionRanges
|
||||
)
|
||||
}, [engineCommandManager.artifactMap, context.selectionRanges])
|
||||
const { overallState } = useNetworkStatus()
|
||||
|
||||
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
||||
const { overallState } = useNetworkContext()
|
||||
const { isExecuting } = useKclContext()
|
||||
const { isStreamReady } = useStore((s) => ({
|
||||
isStreamReady: s.isStreamReady,
|
||||
}))
|
||||
const disableAllButtons =
|
||||
overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady
|
||||
(overallState !== NetworkHealthState.Ok &&
|
||||
overallState !== NetworkHealthState.Weak) ||
|
||||
isExecuting ||
|
||||
!isStreamReady
|
||||
|
||||
useHotkeys(
|
||||
'l',
|
||||
@ -100,12 +105,45 @@ export const Toolbar = () => {
|
||||
|
||||
span.scrollLeft = span.scrollLeft += ev.deltaY
|
||||
}
|
||||
const nextEvents = useMemo(() => state.nextEvents, [state.nextEvents])
|
||||
const splitMenuItems = useMemo(
|
||||
() =>
|
||||
nextEvents
|
||||
.filter(
|
||||
(eventName) =>
|
||||
eventName.includes('Make segment') ||
|
||||
eventName.includes('Constrain')
|
||||
)
|
||||
.sort((a, b) => {
|
||||
const aisEnabled = nextEvents
|
||||
.filter((event) => state.can(event as any))
|
||||
.includes(a)
|
||||
const bIsEnabled = nextEvents
|
||||
.filter((event) => state.can(event as any))
|
||||
.includes(b)
|
||||
if (aisEnabled && !bIsEnabled) {
|
||||
return -1
|
||||
}
|
||||
if (!aisEnabled && bIsEnabled) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
.map((eventName) => ({
|
||||
label: eventName
|
||||
.replace('Make segment ', '')
|
||||
.replace('Constrain ', ''),
|
||||
onClick: () => send(eventName),
|
||||
disabled:
|
||||
!nextEvents
|
||||
.filter((event) => state.can(event as any))
|
||||
.includes(eventName) || disableAllButtons,
|
||||
})),
|
||||
|
||||
function ToolbarButtons({
|
||||
className = '',
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLElement>) {
|
||||
[JSON.stringify(nextEvents), state]
|
||||
)
|
||||
return (
|
||||
<menu className="max-w-full whitespace-nowrap rounded px-1.5 py-0.5 backdrop-blur-sm bg-chalkboard-10/80 dark:bg-chalkboard-110/70 relative">
|
||||
<ul
|
||||
{...props}
|
||||
ref={toolbarButtonsRef}
|
||||
@ -113,7 +151,7 @@ export const Toolbar = () => {
|
||||
className={'m-0 py-1 rounded-l-sm flex gap-2 items-center ' + className}
|
||||
style={{ scrollbarWidth: 'thin' }}
|
||||
>
|
||||
{state.nextEvents.includes('Enter sketch') && (
|
||||
{nextEvents.includes('Enter sketch') && (
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
className={buttonClassName}
|
||||
@ -139,7 +177,7 @@ export const Toolbar = () => {
|
||||
</ActionButton>
|
||||
</li>
|
||||
)}
|
||||
{state.nextEvents.includes('Enter sketch') && pathId && (
|
||||
{nextEvents.includes('Enter sketch') && pathId && (
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
className={buttonClassName}
|
||||
@ -163,7 +201,7 @@ export const Toolbar = () => {
|
||||
</ActionButton>
|
||||
</li>
|
||||
)}
|
||||
{state.nextEvents.includes('Cancel') && !state.matches('idle') && (
|
||||
{nextEvents.includes('Cancel') && !state.matches('idle') && (
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
className={buttonClassName}
|
||||
@ -286,43 +324,13 @@ export const Toolbar = () => {
|
||||
</>
|
||||
)}
|
||||
{state.matches('Sketch.SketchIdle') &&
|
||||
state.nextEvents.filter(
|
||||
nextEvents.filter(
|
||||
(eventName) =>
|
||||
eventName.includes('Make segment') ||
|
||||
eventName.includes('Constrain')
|
||||
).length > 0 && (
|
||||
<ActionButtonDropdown
|
||||
splitMenuItems={state.nextEvents
|
||||
.filter(
|
||||
(eventName) =>
|
||||
eventName.includes('Make segment') ||
|
||||
eventName.includes('Constrain')
|
||||
)
|
||||
.sort((a, b) => {
|
||||
const aisEnabled = state.nextEvents
|
||||
.filter((event) => state.can(event as any))
|
||||
.includes(a)
|
||||
const bIsEnabled = state.nextEvents
|
||||
.filter((event) => state.can(event as any))
|
||||
.includes(b)
|
||||
if (aisEnabled && !bIsEnabled) {
|
||||
return -1
|
||||
}
|
||||
if (!aisEnabled && bIsEnabled) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
.map((eventName) => ({
|
||||
label: eventName
|
||||
.replace('Make segment ', '')
|
||||
.replace('Constrain ', ''),
|
||||
onClick: () => send(eventName),
|
||||
disabled:
|
||||
!state.nextEvents
|
||||
.filter((event) => state.can(event as any))
|
||||
.includes(eventName) || disableAllButtons,
|
||||
}))}
|
||||
splitMenuItems={splitMenuItems}
|
||||
className={buttonClassName}
|
||||
Element="button"
|
||||
iconStart={{
|
||||
@ -369,12 +377,6 @@ export const Toolbar = () => {
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<menu className="max-w-full whitespace-nowrap rounded px-1.5 py-0.5 backdrop-blur-sm bg-chalkboard-10/80 dark:bg-chalkboard-110/70 relative">
|
||||
<ToolbarButtons />
|
||||
</menu>
|
||||
)
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
EngineCommand,
|
||||
Subscription,
|
||||
EngineCommandManager,
|
||||
UnreliableSubscription,
|
||||
} from 'lang/std/engineConnection'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { deg2Rad } from 'lib/utils2d'
|
||||
@ -47,12 +48,14 @@ export type ReactCameraProperties =
|
||||
type: 'perspective'
|
||||
fov?: number
|
||||
position: [number, number, number]
|
||||
target: [number, number, number]
|
||||
quaternion: [number, number, number, number]
|
||||
}
|
||||
| {
|
||||
type: 'orthographic'
|
||||
zoom?: number
|
||||
position: [number, number, number]
|
||||
target: [number, number, number]
|
||||
quaternion: [number, number, number, number]
|
||||
}
|
||||
|
||||
@ -232,9 +235,19 @@ export class CameraControls {
|
||||
this.update()
|
||||
this._usePerspectiveCamera()
|
||||
|
||||
const cb: Subscription<
|
||||
'default_camera_zoom' | 'camera_drag_end' | 'default_camera_get_settings'
|
||||
>['callback'] = ({ data, type }) => {
|
||||
type CallBackParam = Parameters<
|
||||
(
|
||||
| Subscription<
|
||||
| 'default_camera_zoom'
|
||||
| 'camera_drag_end'
|
||||
| 'default_camera_get_settings'
|
||||
| 'zoom_to_fit'
|
||||
>
|
||||
| UnreliableSubscription<'camera_drag_move'>
|
||||
)['callback']
|
||||
>[0]
|
||||
|
||||
const cb = ({ data, type }: CallBackParam) => {
|
||||
const camSettings = data.settings
|
||||
this.camera.position.set(
|
||||
camSettings.pos.x,
|
||||
@ -246,7 +259,13 @@ export class CameraControls {
|
||||
camSettings.center.y,
|
||||
camSettings.center.z
|
||||
)
|
||||
this.camera.up.set(camSettings.up.x, camSettings.up.y, camSettings.up.z)
|
||||
const quat = new Quaternion(
|
||||
camSettings.orientation.x,
|
||||
camSettings.orientation.y,
|
||||
camSettings.orientation.z,
|
||||
camSettings.orientation.w
|
||||
).invert()
|
||||
this.camera.up.copy(new Vector3(0, 1, 0).applyQuaternion(quat))
|
||||
if (this.camera instanceof PerspectiveCamera && camSettings.ortho) {
|
||||
this.useOrthographicCamera()
|
||||
}
|
||||
@ -287,6 +306,14 @@ export class CameraControls {
|
||||
event: 'default_camera_get_settings',
|
||||
callback: cb,
|
||||
})
|
||||
this.engineCommandManager.subscribeTo({
|
||||
event: 'zoom_to_fit',
|
||||
callback: cb,
|
||||
})
|
||||
this.engineCommandManager.subscribeToUnreliable({
|
||||
event: 'camera_drag_move',
|
||||
callback: cb,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -748,6 +775,75 @@ export class CameraControls {
|
||||
})
|
||||
}
|
||||
|
||||
async updateCameraToAxis(
|
||||
axis: 'x' | 'y' | 'z' | '-x' | '-y' | '-z'
|
||||
): Promise<void> {
|
||||
const distance = this.camera.position.distanceTo(this.target)
|
||||
|
||||
const vantage = this.target.clone()
|
||||
let up = { x: 0, y: 0, z: 1 }
|
||||
|
||||
if (axis === 'x') {
|
||||
vantage.x += distance
|
||||
} else if (axis === 'y') {
|
||||
vantage.y += distance
|
||||
} else if (axis === 'z') {
|
||||
vantage.z += distance
|
||||
up = { x: -1, y: 0, z: 0 }
|
||||
} else if (axis === '-x') {
|
||||
vantage.x -= distance
|
||||
} else if (axis === '-y') {
|
||||
vantage.y -= distance
|
||||
} else if (axis === '-z') {
|
||||
vantage.z -= distance
|
||||
up = { x: -1, y: 0, z: 0 }
|
||||
}
|
||||
|
||||
await this.engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_look_at',
|
||||
center: this.target,
|
||||
vantage: vantage,
|
||||
up: up,
|
||||
},
|
||||
})
|
||||
await this.engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_get_settings',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async resetCameraPosition(): Promise<void> {
|
||||
await this.engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_look_at',
|
||||
center: this.target,
|
||||
vantage: {
|
||||
x: this.target.x,
|
||||
y: this.target.y - 128,
|
||||
z: this.target.z + 64,
|
||||
},
|
||||
up: { x: 0, y: 0, z: 1 },
|
||||
},
|
||||
})
|
||||
await this.engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'zoom_to_fit',
|
||||
object_ids: [], // leave empty to zoom to all objects
|
||||
padding: 0.2, // padding around the objects
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async tweenCameraToQuaternion(
|
||||
targetQuaternion: Quaternion,
|
||||
targetPosition = new Vector3(),
|
||||
@ -932,6 +1028,11 @@ export class CameraControls {
|
||||
roundOff(this.camera.position.y, 2),
|
||||
roundOff(this.camera.position.z, 2),
|
||||
],
|
||||
target: [
|
||||
roundOff(this.target.x, 2),
|
||||
roundOff(this.target.y, 2),
|
||||
roundOff(this.target.z, 2),
|
||||
],
|
||||
quaternion: [
|
||||
roundOff(this.camera.quaternion.x, 2),
|
||||
roundOff(this.camera.quaternion.y, 2),
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useRef, useEffect, useState } from 'react'
|
||||
import { useRef, useEffect, useState, useMemo, Fragment } from 'react'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
|
||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||
@ -6,12 +6,44 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
|
||||
import { ReactCameraProperties } from './CameraControls'
|
||||
import { throttle } from 'lib/utils'
|
||||
import { sceneInfra } from 'lib/singletons'
|
||||
import {
|
||||
sceneInfra,
|
||||
kclManager,
|
||||
codeManager,
|
||||
editorManager,
|
||||
sceneEntitiesManager,
|
||||
engineCommandManager,
|
||||
} from 'lib/singletons'
|
||||
import {
|
||||
EXTRA_SEGMENT_HANDLE,
|
||||
PROFILE_START,
|
||||
getParentGroup,
|
||||
} from './sceneEntities'
|
||||
import { SegmentOverlay, SketchDetails } from 'machines/modelingMachine'
|
||||
import { findUsesOfTagInPipe, getNodeFromPath } from 'lang/queryAst'
|
||||
import {
|
||||
CallExpression,
|
||||
PathToNode,
|
||||
Program,
|
||||
SourceRange,
|
||||
Value,
|
||||
parse,
|
||||
recast,
|
||||
} from 'lang/wasm'
|
||||
import { CustomIcon, CustomIconName } from 'components/CustomIcon'
|
||||
import { ConstrainInfo } from 'lang/std/stdTypes'
|
||||
import { getConstraintInfo } from 'lang/std/sketch'
|
||||
import { Dialog, Popover, Transition } from '@headlessui/react'
|
||||
import { LineInputsType } from 'lang/std/sketchcombos'
|
||||
import toast from 'react-hot-toast'
|
||||
import { InstanceProps, create } from 'react-modal-promise'
|
||||
import { executeAst } from 'useStore'
|
||||
import {
|
||||
deleteSegmentFromPipeExpression,
|
||||
makeRemoveSingleConstraintInput,
|
||||
removeSingleConstraintInfo,
|
||||
} from 'lang/modifyAst'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
|
||||
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
||||
const [isCamMoving, setIsCamMoving] = useState(false)
|
||||
@ -100,6 +132,7 @@ export const ClientSideScene = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={canvasRef}
|
||||
style={{ cursor: cursor }}
|
||||
@ -111,6 +144,522 @@ export const ClientSideScene = ({
|
||||
: ''
|
||||
}`}
|
||||
></div>
|
||||
<Overlays />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Overlays = () => {
|
||||
const { context } = useModelingContext()
|
||||
if (context.mouseState.type === 'isDragging') return null
|
||||
return (
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
{Object.entries(context.segmentOverlays)
|
||||
.filter((a) => a[1].visible)
|
||||
.map(([pathToNodeString, overlay], index) => {
|
||||
return (
|
||||
<Overlay
|
||||
overlay={overlay}
|
||||
key={pathToNodeString}
|
||||
pathToNodeString={pathToNodeString}
|
||||
overlayIndex={index}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Overlay = ({
|
||||
overlay,
|
||||
overlayIndex,
|
||||
pathToNodeString,
|
||||
}: {
|
||||
overlay: SegmentOverlay
|
||||
overlayIndex: number
|
||||
pathToNodeString: string
|
||||
}) => {
|
||||
const { context, send, state } = useModelingContext()
|
||||
let xAlignment = overlay.angle < 0 ? '0%' : '-100%'
|
||||
let yAlignment = overlay.angle < -90 || overlay.angle >= 90 ? '0%' : '-100%'
|
||||
|
||||
const callExpression = getNodeFromPath<CallExpression>(
|
||||
kclManager.ast,
|
||||
overlay.pathToNode,
|
||||
'CallExpression'
|
||||
).node
|
||||
const constraints = getConstraintInfo(
|
||||
callExpression,
|
||||
codeManager.code,
|
||||
overlay.pathToNode
|
||||
)
|
||||
|
||||
const offset = 20 // px
|
||||
// We could put a boolean in settings that
|
||||
const offsetAngle = 90
|
||||
|
||||
const xOffset =
|
||||
Math.cos(((overlay.angle + offsetAngle) * Math.PI) / 180) * offset
|
||||
const yOffset =
|
||||
Math.sin(((overlay.angle + offsetAngle) * Math.PI) / 180) * offset
|
||||
|
||||
const shouldShow =
|
||||
overlay.visible &&
|
||||
typeof context?.segmentHoverMap?.[pathToNodeString] === 'number' &&
|
||||
!(
|
||||
state.matches('Sketch.Line tool') ||
|
||||
state.matches('Sketch.Tangential arc to') ||
|
||||
state.matches('Sketch.Rectangle tool')
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={`absolute w-0 h-0`}>
|
||||
<div
|
||||
data-testid="segment-overlay"
|
||||
data-path-to-node={pathToNodeString}
|
||||
data-overlay-index={overlayIndex}
|
||||
data-overlay-angle={overlay.angle}
|
||||
className="pointer-events-auto absolute w-0 h-0"
|
||||
style={{
|
||||
transform: `translate3d(${overlay.windowCoords[0]}px, ${overlay.windowCoords[1]}px, 0)`,
|
||||
}}
|
||||
></div>
|
||||
{shouldShow && (
|
||||
<div
|
||||
className={`px-0 pointer-events-auto absolute flex gap-1`}
|
||||
style={{
|
||||
transform: `translate3d(calc(${
|
||||
overlay.windowCoords[0] + xOffset
|
||||
}px + ${xAlignment}), calc(${
|
||||
overlay.windowCoords[1] - yOffset
|
||||
}px + ${yAlignment}), 0)`,
|
||||
}}
|
||||
onMouseEnter={() =>
|
||||
send({
|
||||
type: 'Set mouse state',
|
||||
data: {
|
||||
type: 'isHovering',
|
||||
on: overlay.group,
|
||||
},
|
||||
})
|
||||
}
|
||||
onMouseLeave={() =>
|
||||
send({
|
||||
type: 'Set mouse state',
|
||||
data: { type: 'idle' },
|
||||
})
|
||||
}
|
||||
>
|
||||
{constraints &&
|
||||
constraints.map((constraintInfo, i) => (
|
||||
<ConstraintSymbol
|
||||
constrainInfo={constraintInfo}
|
||||
key={i}
|
||||
verticalPosition={
|
||||
overlay.windowCoords[1] > window.innerHeight / 2
|
||||
? 'top'
|
||||
: 'bottom'
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<SegmentMenu
|
||||
verticalPosition={
|
||||
overlay.windowCoords[1] > window.innerHeight / 2
|
||||
? 'top'
|
||||
: 'bottom'
|
||||
}
|
||||
pathToNode={overlay.pathToNode}
|
||||
stdLibFnName={constraints[0]?.stdLibFnName}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type ConfirmModalProps = InstanceProps<boolean, boolean> & { text: string }
|
||||
|
||||
export const ConfirmModal = ({
|
||||
isOpen,
|
||||
onResolve,
|
||||
onReject,
|
||||
text,
|
||||
}: ConfirmModalProps) => {
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={() => onResolve(false)}
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="rounded relative mx-auto px-4 py-8 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg">
|
||||
<div>{text}</div>
|
||||
<div className="mt-8 flex justify-between">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => onResolve(true)}
|
||||
>
|
||||
Continue and unconstrain
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => onReject(false)}
|
||||
>
|
||||
Cancel
|
||||
</ActionButton>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
)
|
||||
}
|
||||
|
||||
export const confirmModal = create<ConfirmModalProps, boolean, boolean>(
|
||||
ConfirmModal
|
||||
)
|
||||
|
||||
export async function deleteSegment({
|
||||
pathToNode,
|
||||
sketchDetails,
|
||||
}: {
|
||||
pathToNode: PathToNode
|
||||
sketchDetails: SketchDetails | null
|
||||
}) {
|
||||
let modifiedAst: Program = kclManager.ast
|
||||
const dependentRanges = findUsesOfTagInPipe(modifiedAst, pathToNode)
|
||||
|
||||
const shouldContinueSegDelete = dependentRanges.length
|
||||
? await confirmModal({
|
||||
text: `At least ${dependentRanges.length} segment rely on the segment you're deleting.\nDo you want to continue and unconstrain these segments?`,
|
||||
isOpen: true,
|
||||
})
|
||||
: true
|
||||
|
||||
if (!shouldContinueSegDelete) return
|
||||
modifiedAst = deleteSegmentFromPipeExpression(
|
||||
dependentRanges,
|
||||
modifiedAst,
|
||||
kclManager.programMemory,
|
||||
codeManager.code,
|
||||
pathToNode
|
||||
)
|
||||
|
||||
const newCode = recast(modifiedAst)
|
||||
modifiedAst = parse(newCode)
|
||||
const testExecute = await executeAst({
|
||||
ast: modifiedAst,
|
||||
useFakeExecutor: true,
|
||||
engineCommandManager: engineCommandManager,
|
||||
})
|
||||
if (testExecute.errors.length) {
|
||||
toast.error('Segment tag used outside of current Sketch. Could not delete.')
|
||||
return
|
||||
}
|
||||
|
||||
if (!sketchDetails) return
|
||||
sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
sketchDetails.sketchPathToNode,
|
||||
modifiedAst,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
sketchDetails.origin
|
||||
)
|
||||
}
|
||||
|
||||
const SegmentMenu = ({
|
||||
verticalPosition,
|
||||
pathToNode,
|
||||
stdLibFnName,
|
||||
}: {
|
||||
verticalPosition: 'top' | 'bottom'
|
||||
pathToNode: PathToNode
|
||||
stdLibFnName: string
|
||||
}) => {
|
||||
const { send } = useModelingContext()
|
||||
const dependentSourceRanges = findUsesOfTagInPipe(kclManager.ast, pathToNode)
|
||||
return (
|
||||
<Popover className="relative">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
data-testid="overlay-menu"
|
||||
data-stdlib-fn-name={stdLibFnName}
|
||||
className="bg-chalkboard-10 dark:bg-chalkboard-100 border !border-transparent hover:!border-chalkboard-40 dark:hover:!border-chalkboard-70 ui-open:!border-chalkboard-40 dark:ui-open:!border-chalkboard-70 h-[26px] w-[26px] rounded-sm p-0 m-0"
|
||||
>
|
||||
<CustomIcon name={'three-dots'} />
|
||||
</Popover.Button>
|
||||
<Popover.Panel
|
||||
as="menu"
|
||||
className={`absolute ${
|
||||
verticalPosition === 'top' ? 'bottom-full' : 'top-full'
|
||||
} z-10 w-36 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch px-0 py-1 bg-chalkboard-10 dark:bg-chalkboard-100 rounded-sm shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50`}
|
||||
>
|
||||
<button
|
||||
className="!border-transparent rounded-sm text-left p-1 text-nowrap"
|
||||
onClick={() => {
|
||||
send({ type: 'Constrain remove constraints', data: pathToNode })
|
||||
}}
|
||||
>
|
||||
Remove constraints
|
||||
</button>
|
||||
<button
|
||||
className="!border-transparent rounded-sm text-left p-1 text-nowrap"
|
||||
title={
|
||||
dependentSourceRanges.length > 0
|
||||
? `At least ${dependentSourceRanges.length} segment rely on this segment's tag.`
|
||||
: ''
|
||||
}
|
||||
onClick={() => {
|
||||
send({ type: 'Delete segment', data: pathToNode })
|
||||
}}
|
||||
>
|
||||
Delete Segment
|
||||
</button>
|
||||
</Popover.Panel>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
const ConstraintSymbol = ({
|
||||
constrainInfo: { type: _type, isConstrained, value, pathToNode, argPosition },
|
||||
verticalPosition,
|
||||
}: {
|
||||
constrainInfo: ConstrainInfo
|
||||
verticalPosition: 'top' | 'bottom'
|
||||
}) => {
|
||||
const { context, send } = useModelingContext()
|
||||
const varNameMap: {
|
||||
[key in ConstrainInfo['type']]: {
|
||||
varName: string
|
||||
displayName: string
|
||||
iconName: CustomIconName
|
||||
implicitConstraintDesc?: string
|
||||
}
|
||||
} = {
|
||||
xRelative: {
|
||||
varName: 'xRel',
|
||||
displayName: 'X Relative',
|
||||
iconName: 'xRelative',
|
||||
},
|
||||
xAbsolute: {
|
||||
varName: 'xAbs',
|
||||
displayName: 'X Absolute',
|
||||
iconName: 'xAbsolute',
|
||||
},
|
||||
yRelative: {
|
||||
varName: 'yRel',
|
||||
displayName: 'Y Relative',
|
||||
iconName: 'yRelative',
|
||||
},
|
||||
yAbsolute: {
|
||||
varName: 'yAbs',
|
||||
displayName: 'Y Absolute',
|
||||
iconName: 'yAbsolute',
|
||||
},
|
||||
angle: {
|
||||
varName: 'angle',
|
||||
displayName: 'Angle',
|
||||
iconName: 'angle',
|
||||
},
|
||||
length: {
|
||||
varName: 'len',
|
||||
displayName: 'Length',
|
||||
iconName: 'dimension',
|
||||
},
|
||||
intersectionOffset: {
|
||||
varName: 'perpDist',
|
||||
displayName: 'Intersection Offset',
|
||||
iconName: 'intersection-offset',
|
||||
},
|
||||
|
||||
// implicit constraints
|
||||
vertical: {
|
||||
varName: '',
|
||||
displayName: '',
|
||||
iconName: 'vertical',
|
||||
implicitConstraintDesc: 'vertically',
|
||||
},
|
||||
horizontal: {
|
||||
varName: '',
|
||||
displayName: '',
|
||||
iconName: 'horizontal',
|
||||
implicitConstraintDesc: 'horizontally',
|
||||
},
|
||||
tangentialWithPrevious: {
|
||||
varName: '',
|
||||
displayName: '',
|
||||
iconName: 'tangent',
|
||||
implicitConstraintDesc: 'tangential to previous segment',
|
||||
},
|
||||
|
||||
// we don't render this one
|
||||
intersectionTag: {
|
||||
varName: '',
|
||||
displayName: '',
|
||||
iconName: 'dimension',
|
||||
},
|
||||
}
|
||||
const varName =
|
||||
_type in varNameMap ? varNameMap[_type as LineInputsType].varName : 'var'
|
||||
const name: CustomIconName = varNameMap[_type as LineInputsType].iconName
|
||||
const displayName = varNameMap[_type as LineInputsType]?.displayName
|
||||
const implicitDesc =
|
||||
varNameMap[_type as LineInputsType]?.implicitConstraintDesc
|
||||
|
||||
const node = useMemo(
|
||||
() => getNodeFromPath<Value>(kclManager.ast, pathToNode).node,
|
||||
[kclManager.ast, pathToNode]
|
||||
)
|
||||
const range: SourceRange = node ? [node.start, node.end] : [0, 0]
|
||||
|
||||
if (_type === 'intersectionTag') return null
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
<button
|
||||
data-testid="constraint-symbol"
|
||||
data-is-implicit-constraint={implicitDesc ? 'true' : 'false'}
|
||||
data-constraint-type={_type}
|
||||
data-is-constrained={isConstrained ? 'true' : 'false'}
|
||||
className={`${
|
||||
implicitDesc
|
||||
? 'bg-chalkboard-10 dark:bg-chalkboard-100 border-transparent border-0 rounded'
|
||||
: isConstrained
|
||||
? 'bg-chalkboard-10 dark:bg-chalkboard-90 dark:hover:bg-chalkboard-80 border-chalkboard-40 dark:border-chalkboard-70 rounded-sm'
|
||||
: 'bg-primary/30 dark:bg-primary text-primary dark:text-chalkboard-10 dark:border-transparent group-hover:bg-primary/40 group-hover:border-primary/50 group-hover:brightness-125'
|
||||
} h-[26px] w-[26px] rounded-sm relative m-0 p-0`}
|
||||
onMouseEnter={() => {
|
||||
editorManager.setHighlightRange(range)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
editorManager.setHighlightRange([0, 0])
|
||||
}}
|
||||
// disabled={isConstrained || !convertToVarEnabled}
|
||||
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
|
||||
onClick={async () => {
|
||||
if (!isConstrained) {
|
||||
send({
|
||||
type: 'Convert to variable',
|
||||
data: {
|
||||
pathToNode,
|
||||
variableName: varName,
|
||||
},
|
||||
})
|
||||
} else if (isConstrained) {
|
||||
try {
|
||||
const shallowPath = getNodeFromPath<CallExpression>(
|
||||
parse(recast(kclManager.ast)),
|
||||
pathToNode,
|
||||
'CallExpression',
|
||||
true
|
||||
).shallowPath
|
||||
const input = makeRemoveSingleConstraintInput(
|
||||
argPosition,
|
||||
shallowPath
|
||||
)
|
||||
if (!input || !context.sketchDetails) return
|
||||
const transform = removeSingleConstraintInfo(
|
||||
input,
|
||||
kclManager.ast,
|
||||
kclManager.programMemory
|
||||
)
|
||||
if (!transform) return
|
||||
const { modifiedAst } = transform
|
||||
kclManager.updateAst(modifiedAst, true)
|
||||
} catch (e) {
|
||||
console.log('error', e)
|
||||
}
|
||||
toast.success('Constraint removed')
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CustomIcon name={name} />
|
||||
</button>
|
||||
|
||||
<div
|
||||
className={`absolute ${
|
||||
verticalPosition === 'top'
|
||||
? 'top-0 -translate-y-full'
|
||||
: 'bottom-0 translate-y-full'
|
||||
} group-hover:block hidden w-[2px] h-2 translate-x-[12px] bg-white/40`}
|
||||
></div>
|
||||
<div
|
||||
className={`absolute ${
|
||||
verticalPosition === 'top' ? 'top-0' : 'bottom-0'
|
||||
} group-hover:block hidden`}
|
||||
style={{
|
||||
transform: `translate3d(calc(-50% + 13px), ${
|
||||
verticalPosition === 'top' ? '-100%' : '100%'
|
||||
}, 0)`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="bg-chalkboard-10 dark:bg-chalkboard-90 p-2 px-3 rounded-sm border border-solid border-chalkboard-20 dark:border-chalkboard-80 shadow-sm"
|
||||
data-testid="constraint-symbol-popover"
|
||||
>
|
||||
{implicitDesc ? (
|
||||
<div className="min-w-48">
|
||||
<pre className="inline-block">
|
||||
<code className="text-primary">{value}</code>
|
||||
</pre>{' '}
|
||||
<span>is implicitly constrained {implicitDesc}</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex mb-1">
|
||||
<span className="text-nowrap">
|
||||
<span className="font-bold">
|
||||
{isConstrained ? 'Constrained' : 'Unconstrained'}
|
||||
</span>
|
||||
<span className="text-white/80 text-sm pl-2">
|
||||
{displayName}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex mb-1">
|
||||
<span className="pr-2 whitespace-nowrap">Set to</span>
|
||||
<pre>
|
||||
<code className="text-primary">{value}</code>
|
||||
</pre>
|
||||
</div>
|
||||
<div className="text-sm text-chalkboard-70 dark:text-chalkboard-40 text-nowrap">
|
||||
{isConstrained
|
||||
? 'Click to unconstrain with raw number'
|
||||
: 'Click to constrain with variable'}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -150,6 +699,15 @@ export const CamDebugSettings = () => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
sceneInfra.camControls.resetCameraPosition()
|
||||
}}
|
||||
>
|
||||
Reset Camera Position
|
||||
</button>
|
||||
</div>
|
||||
{camSettings.type === 'perspective' && (
|
||||
<input
|
||||
type="range"
|
||||
@ -267,6 +825,71 @@ export const CamDebugSettings = () => {
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
target
|
||||
<ul className="flex">
|
||||
<li>
|
||||
<span className="pl-2 pr-1">x:</span>
|
||||
<input
|
||||
type="number"
|
||||
step={5}
|
||||
data-testid="cam-x-target"
|
||||
value={camSettings.target[0]}
|
||||
className="text-black w-16"
|
||||
onChange={(e) => {
|
||||
sceneInfra.camControls.setCam({
|
||||
...camSettings,
|
||||
target: [
|
||||
parseFloat(e.target.value),
|
||||
camSettings.target[1],
|
||||
camSettings.target[2],
|
||||
],
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<span className="pl-2 pr-1">y:</span>
|
||||
<input
|
||||
type="number"
|
||||
step={5}
|
||||
data-testid="cam-y-target"
|
||||
value={camSettings.target[1]}
|
||||
className="text-black w-16"
|
||||
onChange={(e) => {
|
||||
sceneInfra.camControls.setCam({
|
||||
...camSettings,
|
||||
target: [
|
||||
camSettings.target[0],
|
||||
parseFloat(e.target.value),
|
||||
camSettings.target[2],
|
||||
],
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<span className="pl-2 pr-1">z:</span>
|
||||
<input
|
||||
type="number"
|
||||
step={5}
|
||||
data-testid="cam-z-target"
|
||||
value={camSettings.target[2]}
|
||||
className="text-black w-16"
|
||||
onChange={(e) => {
|
||||
sceneInfra.camControls.setCam({
|
||||
...camSettings,
|
||||
target: [
|
||||
camSettings.target[0],
|
||||
camSettings.target[1],
|
||||
parseFloat(e.target.value),
|
||||
],
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -69,12 +69,13 @@ import {
|
||||
tangentialArcToSegment,
|
||||
} from './segments'
|
||||
import {
|
||||
addCallExpressionsToPipe,
|
||||
addCloseToPipe,
|
||||
addNewSketchLn,
|
||||
changeSketchArguments,
|
||||
updateStartProfileAtArgs,
|
||||
} from 'lang/std/sketch'
|
||||
import { roundOff, throttle } from 'lib/utils'
|
||||
import { normaliseAngle, roundOff, throttle } from 'lib/utils'
|
||||
import {
|
||||
createArrayExpression,
|
||||
createCallExpressionStdLib,
|
||||
@ -91,8 +92,11 @@ import { getTangentPointFromPreviousArc } from 'lib/utils2d'
|
||||
import { createGridHelper, orthoScale, perspScale } from './helpers'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { SketchDetails } from 'machines/modelingMachine'
|
||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||
import { SegmentOverlayPayload, SketchDetails } from 'machines/modelingMachine'
|
||||
import {
|
||||
ArtifactMapCommand,
|
||||
EngineCommandManager,
|
||||
} from 'lang/std/engineConnection'
|
||||
import {
|
||||
getRectangleCallExpressions,
|
||||
updateRectangleSketch,
|
||||
@ -138,8 +142,8 @@ export class SceneEntities {
|
||||
}
|
||||
onCamChange = () => {
|
||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||
|
||||
Object.values(this.activeSegments).forEach((segment) => {
|
||||
const callbacks: (() => SegmentOverlayPayload | null)[] = []
|
||||
Object.values(this.activeSegments).forEach((segment, index) => {
|
||||
const factor =
|
||||
(sceneInfra.camControls.camera instanceof OrthographicCamera
|
||||
? orthoFactor
|
||||
@ -150,12 +154,14 @@ export class SceneEntities {
|
||||
segment.userData.to &&
|
||||
segment.userData.type === STRAIGHT_SEGMENT
|
||||
) {
|
||||
callbacks.push(
|
||||
this.updateStraightSegment({
|
||||
from: segment.userData.from,
|
||||
to: segment.userData.to,
|
||||
group: segment,
|
||||
scale: factor,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
@ -164,6 +170,7 @@ export class SceneEntities {
|
||||
segment.userData.prevSegment &&
|
||||
segment.userData.type === TANGENTIAL_ARC_TO_SEGMENT
|
||||
) {
|
||||
callbacks.push(
|
||||
this.updateTangentialArcToSegment({
|
||||
prevSegment: segment.userData.prevSegment,
|
||||
from: segment.userData.from,
|
||||
@ -171,6 +178,7 @@ export class SceneEntities {
|
||||
group: segment,
|
||||
scale: factor,
|
||||
})
|
||||
)
|
||||
}
|
||||
if (segment.name === PROFILE_START) {
|
||||
segment.scale.set(factor, factor, factor)
|
||||
@ -186,6 +194,7 @@ export class SceneEntities {
|
||||
const y = this.axisGroup.getObjectByName(Y_AXIS)
|
||||
y?.scale.set(factor / sceneInfra._baseUnitMultiplier, 1, 1)
|
||||
}
|
||||
sceneInfra.overlayCallbacks(callbacks)
|
||||
}
|
||||
|
||||
createIntersectionPlane() {
|
||||
@ -365,7 +374,7 @@ export class SceneEntities {
|
||||
})
|
||||
group.add(_profileStart)
|
||||
this.activeSegments[JSON.stringify(segPathToNode)] = _profileStart
|
||||
|
||||
const callbacks: (() => SegmentOverlayPayload | null)[] = []
|
||||
sketchGroup.value.forEach((segment, index) => {
|
||||
let segPathToNode = getNodePathFromSourceRange(
|
||||
maybeModdedAst,
|
||||
@ -410,6 +419,15 @@ export class SceneEntities {
|
||||
texture: sceneInfra.extraSegmentTexture,
|
||||
theme: sceneInfra._theme,
|
||||
})
|
||||
callbacks.push(
|
||||
this.updateTangentialArcToSegment({
|
||||
prevSegment: sketchGroup.value[index - 1],
|
||||
from: segment.from,
|
||||
to: segment.to,
|
||||
group: seg,
|
||||
scale: factor,
|
||||
})
|
||||
)
|
||||
} else {
|
||||
seg = straightSegment({
|
||||
from: segment.from,
|
||||
@ -422,6 +440,14 @@ export class SceneEntities {
|
||||
texture: sceneInfra.extraSegmentTexture,
|
||||
theme: sceneInfra._theme,
|
||||
})
|
||||
callbacks.push(
|
||||
this.updateStraightSegment({
|
||||
from: segment.from,
|
||||
to: segment.to,
|
||||
group: seg,
|
||||
scale: factor,
|
||||
})
|
||||
)
|
||||
}
|
||||
seg.layers.set(SKETCH_LAYER)
|
||||
seg.traverse((child) => {
|
||||
@ -446,6 +472,7 @@ export class SceneEntities {
|
||||
this.intersectionPlane.position.set(...position)
|
||||
this.scene.add(group)
|
||||
sceneInfra.camControls.enableRotate = false
|
||||
sceneInfra.overlayCallbacks(callbacks)
|
||||
|
||||
return {
|
||||
truncatedAst,
|
||||
@ -536,10 +563,34 @@ export class SceneEntities {
|
||||
|
||||
let modifiedAst
|
||||
if (profileStart) {
|
||||
modifiedAst = addCloseToPipe({
|
||||
const lastSegment = sketchGroup.value.slice(-1)[0]
|
||||
modifiedAst = addCallExpressionsToPipe({
|
||||
node: kclManager.ast,
|
||||
programMemory: kclManager.programMemory,
|
||||
pathToNode: sketchPathToNode,
|
||||
expressions: [
|
||||
createCallExpressionStdLib(
|
||||
lastSegment.type === 'TangentialArcTo'
|
||||
? 'tangentialArcTo'
|
||||
: 'lineTo',
|
||||
[
|
||||
createArrayExpression([
|
||||
createCallExpressionStdLib('profileStartX', [
|
||||
createPipeSubstitution(),
|
||||
]),
|
||||
createCallExpressionStdLib('profileStartY', [
|
||||
createPipeSubstitution(),
|
||||
]),
|
||||
]),
|
||||
createPipeSubstitution(),
|
||||
]
|
||||
),
|
||||
],
|
||||
})
|
||||
modifiedAst = addCloseToPipe({
|
||||
node: modifiedAst,
|
||||
programMemory: kclManager.programMemory,
|
||||
pathToNode: sketchPathToNode,
|
||||
})
|
||||
} else if (intersection2d) {
|
||||
const lastSegment = sketchGroup.value.slice(-1)[0]
|
||||
@ -560,6 +611,9 @@ export class SceneEntities {
|
||||
}
|
||||
|
||||
await kclManager.executeAstMock(modifiedAst)
|
||||
if (profileStart) {
|
||||
sceneInfra.modelingSend({ type: 'CancelSketch' })
|
||||
} else {
|
||||
this.setUpDraftSegment(
|
||||
sketchPathToNode,
|
||||
forward,
|
||||
@ -567,6 +621,7 @@ export class SceneEntities {
|
||||
origin,
|
||||
segmentName
|
||||
)
|
||||
}
|
||||
},
|
||||
onMove: (args) => {
|
||||
this.onDragSegment({
|
||||
@ -707,14 +762,6 @@ export class SceneEntities {
|
||||
|
||||
_ast = parse(recast(_ast))
|
||||
|
||||
console.log('onClick', {
|
||||
sketchInit: sketchInit,
|
||||
_ast,
|
||||
x,
|
||||
y,
|
||||
truncatedAst,
|
||||
})
|
||||
|
||||
// Update the primary AST and unequip the rectangle tool
|
||||
await kclManager.executeAstMock(_ast)
|
||||
sceneInfra.modelingSend({ type: 'CancelSketch' })
|
||||
@ -990,7 +1037,8 @@ export class SceneEntities {
|
||||
orthoFactor,
|
||||
sketchGroup
|
||||
)
|
||||
sgPaths.forEach((group, index) =>
|
||||
|
||||
const callBacks = sgPaths.map((group, index) =>
|
||||
this.updateSegment(
|
||||
group,
|
||||
index,
|
||||
@ -1000,6 +1048,7 @@ export class SceneEntities {
|
||||
sketchGroup
|
||||
)
|
||||
)
|
||||
sceneInfra.overlayCallbacks(callBacks)
|
||||
})()
|
||||
}
|
||||
|
||||
@ -1020,7 +1069,7 @@ export class SceneEntities {
|
||||
modifiedAst: Program,
|
||||
orthoFactor: number,
|
||||
sketchGroup: SketchGroup
|
||||
) => {
|
||||
): (() => SegmentOverlayPayload | null) => {
|
||||
const segPathToNode = getNodePathFromSourceRange(
|
||||
modifiedAst,
|
||||
segment.__geoMeta.sourceRange
|
||||
@ -1041,7 +1090,7 @@ export class SceneEntities {
|
||||
: perspScale(sceneInfra.camControls.camera, group)) /
|
||||
sceneInfra._baseUnitMultiplier
|
||||
if (type === TANGENTIAL_ARC_TO_SEGMENT) {
|
||||
this.updateTangentialArcToSegment({
|
||||
return this.updateTangentialArcToSegment({
|
||||
prevSegment: sgPaths[index - 1],
|
||||
from: segment.from,
|
||||
to: segment.to,
|
||||
@ -1049,7 +1098,7 @@ export class SceneEntities {
|
||||
scale: factor,
|
||||
})
|
||||
} else if (type === STRAIGHT_SEGMENT) {
|
||||
this.updateStraightSegment({
|
||||
return this.updateStraightSegment({
|
||||
from: segment.from,
|
||||
to: segment.to,
|
||||
group,
|
||||
@ -1059,6 +1108,7 @@ export class SceneEntities {
|
||||
group.position.set(segment.from[0], segment.from[1], 0)
|
||||
group.scale.set(factor, factor, factor)
|
||||
}
|
||||
return () => null
|
||||
}
|
||||
|
||||
updateTangentialArcToSegment({
|
||||
@ -1073,7 +1123,7 @@ export class SceneEntities {
|
||||
to: [number, number]
|
||||
group: Group
|
||||
scale?: number
|
||||
}) {
|
||||
}): () => SegmentOverlayPayload | null {
|
||||
group.userData.from = from
|
||||
group.userData.to = to
|
||||
group.userData.prevSegment = prevSegment
|
||||
@ -1161,6 +1211,18 @@ export class SceneEntities {
|
||||
scale,
|
||||
})
|
||||
}
|
||||
const angle = normaliseAngle(
|
||||
(arcInfo.endAngle * 180) / Math.PI + (arcInfo.ccw ? 90 : -90)
|
||||
)
|
||||
return () =>
|
||||
sceneInfra.updateOverlayDetails({
|
||||
arrowGroup,
|
||||
group,
|
||||
isHandlesVisible,
|
||||
from,
|
||||
to,
|
||||
angle,
|
||||
})
|
||||
}
|
||||
throttledUpdateDashedArcGeo = throttle(
|
||||
(
|
||||
@ -1181,7 +1243,7 @@ export class SceneEntities {
|
||||
to: [number, number]
|
||||
group: Group
|
||||
scale?: number
|
||||
}) {
|
||||
}): () => SegmentOverlayPayload | null {
|
||||
group.userData.from = from
|
||||
group.userData.to = to
|
||||
const shape = new Shape()
|
||||
@ -1258,6 +1320,14 @@ export class SceneEntities {
|
||||
scale
|
||||
)
|
||||
}
|
||||
return () =>
|
||||
sceneInfra.updateOverlayDetails({
|
||||
arrowGroup,
|
||||
group,
|
||||
isHandlesVisible,
|
||||
from,
|
||||
to,
|
||||
})
|
||||
}
|
||||
async animateAfterSketch() {
|
||||
// if (isReducedMotion()) {
|
||||
@ -1347,6 +1417,30 @@ export class SceneEntities {
|
||||
return ['plane', entity_id]
|
||||
}
|
||||
const artifact = this.engineCommandManager.artifactMap[entity_id]
|
||||
// If we clicked on an extrude wall, we climb up the parent Id
|
||||
// to get the sketch profile's face ID. If we clicked on an endcap,
|
||||
// we already have it.
|
||||
const targetId =
|
||||
'additionalData' in artifact &&
|
||||
artifact.additionalData?.type === 'cap'
|
||||
? entity_id
|
||||
: artifact.parentId
|
||||
|
||||
// tsc cannot infer that target can have extrusions
|
||||
// from the commandType (why?) so we need to cast it
|
||||
const target = this.engineCommandManager.artifactMap?.[
|
||||
targetId || ''
|
||||
] as ArtifactMapCommand & { extrusions?: string[] }
|
||||
|
||||
// TODO: We get the first extrusion command ID,
|
||||
// which is fine while backend systems only support one extrusion.
|
||||
// but we need to more robustly handle resolving to the correct extrusion
|
||||
// if there are multiple.
|
||||
const extrusions =
|
||||
this.engineCommandManager.artifactMap?.[
|
||||
target?.extrusions?.[0] || ''
|
||||
]
|
||||
|
||||
if (artifact?.commandType !== 'solid3d_get_extrusion_face_info')
|
||||
return ['other', entity_id]
|
||||
|
||||
@ -1354,10 +1448,13 @@ export class SceneEntities {
|
||||
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
|
||||
return ['other', entity_id]
|
||||
const { z_axis, y_axis, origin } = faceInfo
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
const sketchPathToNode = getNodePathFromSourceRange(
|
||||
kclManager.ast,
|
||||
artifact.range
|
||||
)
|
||||
const extrudePathToNode = extrusions?.range
|
||||
? getNodePathFromSourceRange(kclManager.ast, extrusions.range)
|
||||
: []
|
||||
|
||||
sceneInfra.modelingSend({
|
||||
type: 'Select default plane',
|
||||
@ -1368,7 +1465,8 @@ export class SceneEntities {
|
||||
position: [origin.x, origin.y, origin.z].map(
|
||||
(num) => num / sceneInfra._baseUnitMultiplier
|
||||
) as [number, number, number],
|
||||
extrudeSegmentPathToNode: pathToNode,
|
||||
sketchPathToNode,
|
||||
extrudePathToNode,
|
||||
cap:
|
||||
artifact?.additionalData?.type === 'cap'
|
||||
? artifact.additionalData.info
|
||||
@ -1380,7 +1478,6 @@ export class SceneEntities {
|
||||
}
|
||||
|
||||
const faceResult = await checkExtrudeFaceClick()
|
||||
console.log('faceResult', faceResult)
|
||||
if (faceResult[0] === 'face') return
|
||||
|
||||
if (!args || !args.intersects?.[0]) return
|
||||
@ -1529,6 +1626,14 @@ export class SceneEntities {
|
||||
},
|
||||
}
|
||||
}
|
||||
resetOverlays() {
|
||||
sceneInfra.modelingSend({
|
||||
type: 'Set Segment Overlays',
|
||||
data: {
|
||||
type: 'clear',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export type DefaultPlaneStr = 'XY' | 'XZ' | 'YZ' | '-XY' | '-XZ' | '-YZ'
|
||||
|
@ -21,15 +21,15 @@ import {
|
||||
TextureLoader,
|
||||
Texture,
|
||||
} from 'three'
|
||||
import { compareVec2Epsilon2 } from 'lang/std/sketch'
|
||||
import { Coords2d, compareVec2Epsilon2 } from 'lang/std/sketch'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import * as TWEEN from '@tweenjs/tween.js'
|
||||
import { Axis } from 'lib/selections'
|
||||
import { type BaseUnit } from 'lib/settings/settingsTypes'
|
||||
import { CameraControls } from './CameraControls'
|
||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||
import { settings } from 'lib/settings/initialSettings'
|
||||
import { MouseState } from 'machines/modelingMachine'
|
||||
import { MouseState, SegmentOverlayPayload } from 'machines/modelingMachine'
|
||||
import { getAngle, throttle } from 'lib/utils'
|
||||
import { Themes } from 'lib/theme'
|
||||
|
||||
type SendType = ReturnType<typeof useModelingContext>['send']
|
||||
@ -155,8 +155,88 @@ export class SceneInfra {
|
||||
}
|
||||
|
||||
modelingSend: SendType = (() => {}) as any
|
||||
throttledModelingSend: any = (() => {}) as any
|
||||
setSend(send: SendType) {
|
||||
this.modelingSend = send
|
||||
this.throttledModelingSend = throttle(send, 100)
|
||||
}
|
||||
overlayTimeout = 0
|
||||
callbacks: (() => SegmentOverlayPayload | null)[] = []
|
||||
_overlayCallbacks(callbacks: (() => SegmentOverlayPayload | null)[]) {
|
||||
const segmentOverlayPayload: SegmentOverlayPayload = {
|
||||
type: 'set-many',
|
||||
overlays: {},
|
||||
}
|
||||
callbacks.forEach((cb) => {
|
||||
const overlay = cb()
|
||||
if (overlay?.type === 'set-one') {
|
||||
segmentOverlayPayload.overlays[overlay.pathToNodeString] = overlay.seg
|
||||
}
|
||||
})
|
||||
this.modelingSend({
|
||||
type: 'Set Segment Overlays',
|
||||
data: segmentOverlayPayload,
|
||||
})
|
||||
}
|
||||
overlayCallbacks(
|
||||
callbacks: (() => SegmentOverlayPayload | null)[],
|
||||
instant = false
|
||||
) {
|
||||
if (instant) {
|
||||
this._overlayCallbacks(callbacks)
|
||||
return
|
||||
}
|
||||
this.callbacks = callbacks
|
||||
if (this.overlayTimeout) clearTimeout(this.overlayTimeout)
|
||||
this.overlayTimeout = setTimeout(() => {
|
||||
this._overlayCallbacks(this.callbacks)
|
||||
}, 100) as unknown as number
|
||||
}
|
||||
|
||||
overlayThrottleMap: { [pathToNodeString: string]: number } = {}
|
||||
updateOverlayDetails({
|
||||
arrowGroup,
|
||||
group,
|
||||
isHandlesVisible,
|
||||
from,
|
||||
to,
|
||||
angle,
|
||||
}: {
|
||||
arrowGroup: Group
|
||||
group: Group
|
||||
isHandlesVisible: boolean
|
||||
from: Coords2d
|
||||
to: Coords2d
|
||||
angle?: number
|
||||
}): SegmentOverlayPayload | null {
|
||||
if (group.userData.pathToNode && arrowGroup) {
|
||||
const vector = new Vector3(0, 0, 0)
|
||||
|
||||
// Get the position of the object3D in world space
|
||||
// console.log('arrowGroup', arrowGroup)
|
||||
arrowGroup.getWorldPosition(vector)
|
||||
|
||||
// Project that position to screen space
|
||||
vector.project(this.camControls.camera)
|
||||
|
||||
const _angle = typeof angle === 'number' ? angle : getAngle(from, to)
|
||||
|
||||
const x = (vector.x * 0.5 + 0.5) * window.innerWidth
|
||||
const y = (-vector.y * 0.5 + 0.5) * window.innerHeight
|
||||
const pathToNodeString = JSON.stringify(group.userData.pathToNode)
|
||||
return {
|
||||
type: 'set-one',
|
||||
pathToNodeString,
|
||||
seg: {
|
||||
windowCoords: [x, y],
|
||||
angle: _angle,
|
||||
group,
|
||||
pathToNode: group.userData.pathToNode,
|
||||
visible: isHandlesVisible,
|
||||
},
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
hoveredObject: null | any = null
|
||||
@ -182,15 +262,6 @@ export class SceneInfra {
|
||||
this.renderer.setClearColor(0x000000, 0) // Set clear color to black with 0 alpha (fully transparent)
|
||||
window.addEventListener('resize', this.onWindowResize)
|
||||
|
||||
// CAMERA
|
||||
const camHeightDistanceRatio = 0.5
|
||||
const baseUnit: BaseUnit = settings.modeling.defaultUnit.current
|
||||
const baseRadius = 5.6
|
||||
const length = baseUnitTomm(baseUnit) * baseRadius
|
||||
const ang = Math.atan(camHeightDistanceRatio)
|
||||
const x = Math.cos(ang) * length
|
||||
const y = Math.sin(ang) * length
|
||||
|
||||
this.camControls = new CameraControls(
|
||||
false,
|
||||
this.renderer.domElement,
|
||||
@ -198,7 +269,6 @@ export class SceneInfra {
|
||||
)
|
||||
this.camControls.subscribeToCamChange(() => this.onCameraChange())
|
||||
this.camControls.camera.layers.enable(SKETCH_LAYER)
|
||||
this.camControls.camera.position.set(0, -x, y)
|
||||
if (DEBUG_SHOW_INTERSECTION_PLANE)
|
||||
this.camControls.camera.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||
|
||||
|
@ -39,6 +39,7 @@ export function ActionButtonDropdown({
|
||||
onClick={item.onClick}
|
||||
className="block px-3 py-1 hover:bg-primary/10 dark:hover:bg-chalkboard-80 border-0 m-0 text-sm w-full rounded-none text-left disabled:!bg-transparent dark:disabled:text-chalkboard-60"
|
||||
disabled={item.disabled}
|
||||
data-testid={item.label}
|
||||
>
|
||||
<span className="capitalize">{item.label}</span>
|
||||
{item.shortcut && (
|
||||
|
@ -216,7 +216,7 @@ export const CreateNewVariable = ({
|
||||
<>
|
||||
<label
|
||||
htmlFor="create-new-variable"
|
||||
className="block mt-3 font-mono text-gray-900"
|
||||
className="block mt-3 font-mono text-chalkboard-90"
|
||||
>
|
||||
Create new variable
|
||||
</label>
|
||||
@ -224,11 +224,12 @@ export const CreateNewVariable = ({
|
||||
{showCheckbox && (
|
||||
<input
|
||||
type="checkbox"
|
||||
data-testid="create-new-variable-checkbox"
|
||||
checked={shouldCreateVariable}
|
||||
onChange={(e) => {
|
||||
setShouldCreateVariable(e.target.checked)
|
||||
}}
|
||||
className="bg-white text-gray-900"
|
||||
className="bg-chalkboard-10 dark:bg-chalkboard-80"
|
||||
/>
|
||||
)}
|
||||
<input
|
||||
@ -249,7 +250,7 @@ export const CreateNewVariable = ({
|
||||
/>
|
||||
</div>
|
||||
{!isNewVariableNameUnique && (
|
||||
<div className="bg-pink-200 rounded px-2 py-0.5 text-xs">
|
||||
<div className="bg-pink-200 dark:bg-chalkboard-80 dark:text-pink-200 rounded px-2 py-0.5 text-xs">
|
||||
Sorry, that's not a unique variable name. Please try something else
|
||||
</div>
|
||||
)}
|
||||
|
@ -51,14 +51,6 @@ function CommandBarSelectionInput({
|
||||
inputRef.current?.focus()
|
||||
}, [selection, inputRef])
|
||||
|
||||
// Exit engine's edit mode when this input step is active,
|
||||
// and re-enter it when it's not.
|
||||
// In future the engine's edit mode will go away and this will be handled differently.
|
||||
useEffect(() => {
|
||||
kclManager.exitEditMode()
|
||||
return () => kclManager.defaultSelectionFilter()
|
||||
}, [])
|
||||
|
||||
// Fast-forward through this arg if it's marked as skippable
|
||||
// and we have a valid selection already
|
||||
useEffect(() => {
|
||||
|
@ -11,6 +11,16 @@ const CustomIconMap = {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
angle: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M15 14L10 14H5.00001H4.0495L4.58799 13.2168L7.33799 9.21675L10.088 5.21674L10.912 5.78326L8.45436 9.35807C9.07972 9.78751 9.54479 10.2461 9.87084 10.8329C10.2065 11.437 10.3723 12.1375 10.4598 13H15V14ZM9.45406 13C9.37153 12.2592 9.23025 11.739 8.99671 11.3186C8.7674 10.9059 8.42873 10.5535 7.88782 10.1821L5.95053 13L9.45406 13Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
arrowDown: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
@ -227,6 +237,16 @@ const CustomIconMap = {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
'intersection-offset': (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M13.8189 4.34932L4.21895 13.9493L3.51184 13.2422L13.1118 3.64221L13.8189 4.34932ZM7.17419 15.6636L6.48848 16.3493L5.78137 15.6422L6.46709 14.9564L7.17419 15.6636ZM9.57419 13.2636L8.20276 14.635L7.49566 13.9279L8.86709 12.5564L9.57419 13.2636ZM12.0932 13.0433C12.3807 12.8662 12.6223 12.6217 12.796 12.3319L15.8739 15.4098L15.1668 16.1169L12.0932 13.0433ZM14.3742 8.46355L13.0028 9.83498L12.2957 9.12787L13.6671 7.75644L14.3742 8.46355ZM16.0885 6.74927L15.4028 7.43498L14.6957 6.72787L15.3814 6.04216L16.0885 6.74927ZM10.9933 12.754C11.8217 12.754 12.4933 12.0825 12.4933 11.254C12.4933 10.4256 11.8217 9.75404 10.9933 9.75404C10.1649 9.75404 9.49329 10.4256 9.49329 11.254C9.49329 12.0825 10.1649 12.754 10.9933 12.754Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
kcl: (
|
||||
<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
@ -237,6 +257,14 @@ const CustomIconMap = {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
keyboard: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M16 12V15H13.5M16 12V9M16 12H13.5M4 12V15H6.5M4 12V9M4 12H6.5M4 9V6H6.5M4 9H6.5M16 9V6H13.5M16 9H13.5M6.5 12V15M6.5 12H7.5M6.5 15H13.5M13.5 15V12M13.5 12H12.5M7.5 12V9M7.5 12H10M7.5 9H8.75M7.5 9H6.5M10 12V9M10 12H12.5M10 9H11.25M10 9H8.75M12.5 12V9M12.5 9H13.5M12.5 9H11.25M13.5 9V6M13.5 6H11.25M11.25 9V6M11.25 6H8.75M8.75 9V6M8.75 6H6.5M6.5 9V6"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
line: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
@ -385,6 +413,16 @@ const CustomIconMap = {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
tangent: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12.73 2.73L9.23398 6.226C9.72614 6.46571 10.1178 6.87964 10.3288 7.38755C10.6321 7.31396 10.949 7.27497 11.275 7.27497C13.4841 7.27497 15.275 9.06583 15.275 11.275C15.275 13.4841 13.4841 15.275 11.275 15.275C9.06587 15.275 7.27501 13.4841 7.27501 11.275C7.27501 10.949 7.314 10.6321 7.38757 10.3288C6.87965 10.1178 6.46571 9.72614 6.226 9.23398L2.72998 12.73L3.43709 13.4371L6.32769 10.5465C6.29298 10.7843 6.27501 11.0275 6.27501 11.275C6.27501 14.0364 8.51358 16.275 11.275 16.275C14.0364 16.275 16.275 14.0364 16.275 11.275C16.275 8.51355 14.0364 6.27497 11.275 6.27497C11.0276 6.27497 10.7843 6.29294 10.5465 6.32765L13.4371 3.4371L12.73 2.73ZM8.26001 9.75C9.08844 9.75 9.76001 9.07843 9.76001 8.25C9.76001 7.42157 9.08844 6.75 8.26001 6.75C7.43158 6.75 6.76001 7.42157 6.76001 8.25C6.76001 9.07843 7.43158 9.75 8.26001 9.75Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
'three-dots': (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
@ -395,6 +433,14 @@ const CustomIconMap = {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
trash: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M8.5 6H5V8H6M8.5 6V4H11.5V6M8.5 6H11.5M11.5 6H15V8H14M6 8V15.5H8M6 8H14M14 8V15.5H12M8 15.5V10M8 15.5H10M12 15.5V10M12 15.5H10M10 15.5V12"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
vertical: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
@ -405,6 +451,42 @@ const CustomIconMap = {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
xAbsolute: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4 16V4H5L5 16H4ZM8.75069 6.82599C8.97469 6.78866 9.20803 6.79799 9.45069 6.85399C9.91736 6.95666 10.2627 7.17132 10.4867 7.49799C10.524 7.55399 10.552 7.58199 10.5707 7.58199L10.6547 7.46999C10.8787 7.20866 11.1447 7.01732 11.4527 6.89599C11.8074 6.76532 12.162 6.77932 12.5167 6.93799C12.7874 7.07799 12.9787 7.26466 13.0907 7.49799C13.2774 7.87132 13.282 8.26332 13.1047 8.67399C13.03 8.83266 12.9367 8.95399 12.8247 9.03799C12.4887 9.28066 12.1714 9.32732 11.8727 9.17799C11.8167 9.14999 11.77 9.11732 11.7327 9.07999C11.5927 8.93999 11.5367 8.76266 11.5647 8.54799C11.6207 8.12799 11.8354 7.85266 12.2087 7.72199L12.3207 7.67999L12.2647 7.62399C12.0967 7.47466 11.8914 7.43266 11.6487 7.49799C11.63 7.49799 11.6114 7.50266 11.5927 7.51199C11.3127 7.61466 11.0887 7.88532 10.9207 8.32399C10.8274 8.55732 10.58 9.54199 10.1787 11.278C10.132 11.4367 10.1087 11.5347 10.1087 11.572C10.062 11.8613 10.0667 12.0573 10.1227 12.16C10.1974 12.3 10.314 12.398 10.4727 12.454C10.5194 12.4727 10.6174 12.482 10.7667 12.482C10.9067 12.482 11.0094 12.4727 11.0747 12.454C11.3174 12.3793 11.56 12.23 11.8027 12.006C12.092 11.7073 12.2834 11.3807 12.3767 11.026C12.4047 10.9327 12.442 10.8813 12.4887 10.872C12.526 10.8627 12.61 10.858 12.7407 10.858C12.918 10.858 13.0207 10.8673 13.0487 10.886C13.0674 10.9047 13.0767 10.9373 13.0767 10.984C13.0767 11.18 12.9554 11.474 12.7127 11.866C12.7034 11.894 12.6894 11.9173 12.6707 11.936C12.2507 12.58 11.6954 12.9767 11.0047 13.126C10.79 13.1633 10.5474 13.1633 10.2767 13.126C9.80069 13.0233 9.44603 12.8133 9.21269 12.496L9.12869 12.37L9.08669 12.412C8.72269 12.8787 8.31203 13.126 7.85469 13.154C7.32269 13.182 6.92603 12.986 6.66469 12.566C6.60869 12.4913 6.56669 12.4073 6.53869 12.314C6.47336 12.1087 6.45469 11.8893 6.48269 11.656C6.54803 11.2547 6.73469 10.9793 7.04269 10.83C7.34136 10.69 7.60736 10.6853 7.84069 10.816C8.05536 10.9187 8.15336 11.1053 8.13469 11.376C8.10669 11.7493 7.93403 12.02 7.61669 12.188C7.57003 12.216 7.50469 12.244 7.42069 12.272L7.36469 12.286L7.40669 12.328C7.53736 12.4307 7.68669 12.482 7.85469 12.482C7.97603 12.4913 8.10203 12.4587 8.23269 12.384C8.47536 12.216 8.65736 11.9593 8.77869 11.614L9.54869 8.53399C9.61403 8.19799 9.62336 7.96466 9.57669 7.83399C9.53003 7.68466 9.40869 7.57732 9.21269 7.51199C9.02603 7.45599 8.83469 7.45599 8.63869 7.51199C8.36803 7.58666 8.13003 7.72666 7.92469 7.93199C7.65403 8.21199 7.45803 8.52466 7.33669 8.86999C7.30869 8.98199 7.28069 9.05199 7.25269 9.07999C7.23403 9.09866 7.13603 9.10799 6.95869 9.10799H6.69269L6.65069 9.06599C6.62269 9.03799 6.60869 9.00066 6.60869 8.95399C6.63669 8.81399 6.69269 8.65532 6.77669 8.47799C6.86069 8.28199 6.96803 8.09532 7.09869 7.91799C7.24803 7.69399 7.45336 7.48399 7.71469 7.28799C7.72403 7.27866 7.73803 7.26932 7.75669 7.25999C8.06469 7.04532 8.39603 6.90066 8.75069 6.82599ZM15 4L15 16H16L16 4H15Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
xRelative: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M8.75069 6.82599C8.97469 6.78866 9.20803 6.79799 9.45069 6.85399C9.91736 6.95666 10.2627 7.17132 10.4867 7.49799C10.524 7.55399 10.552 7.58199 10.5707 7.58199L10.6547 7.46999C10.8787 7.20866 11.1447 7.01732 11.4527 6.89599C11.8074 6.76532 12.162 6.77932 12.5167 6.93799C12.7874 7.07799 12.9787 7.26466 13.0907 7.49799C13.2774 7.87132 13.282 8.26332 13.1047 8.67399C13.03 8.83266 12.9367 8.95399 12.8247 9.03799C12.4887 9.28066 12.1714 9.32732 11.8727 9.17799C11.8167 9.14999 11.77 9.11732 11.7327 9.07999C11.5927 8.93999 11.5367 8.76266 11.5647 8.54799C11.6207 8.12799 11.8354 7.85266 12.2087 7.72199L12.3207 7.67999L12.2647 7.62399C12.0967 7.47466 11.8914 7.43266 11.6487 7.49799C11.63 7.49799 11.6114 7.50266 11.5927 7.51199C11.3127 7.61466 11.0887 7.88532 10.9207 8.32399C10.8274 8.55732 10.58 9.54199 10.1787 11.278C10.132 11.4367 10.1087 11.5347 10.1087 11.572C10.062 11.8613 10.0667 12.0573 10.1227 12.16C10.1974 12.3 10.314 12.398 10.4727 12.454C10.5194 12.4727 10.6174 12.482 10.7667 12.482C10.9067 12.482 11.0094 12.4727 11.0747 12.454C11.3174 12.3793 11.56 12.23 11.8027 12.006C12.092 11.7073 12.2834 11.3807 12.3767 11.026C12.4047 10.9327 12.442 10.8813 12.4887 10.872C12.526 10.8627 12.61 10.858 12.7407 10.858C12.918 10.858 13.0207 10.8673 13.0487 10.886C13.0674 10.9047 13.0767 10.9373 13.0767 10.984C13.0767 11.18 12.9554 11.474 12.7127 11.866C12.7034 11.894 12.6894 11.9173 12.6707 11.936C12.2507 12.58 11.6954 12.9767 11.0047 13.126C10.79 13.1633 10.5474 13.1633 10.2767 13.126C9.80069 13.0233 9.44603 12.8133 9.21269 12.496L9.12869 12.37L9.08669 12.412C8.72269 12.8787 8.31203 13.126 7.85469 13.154C7.32269 13.182 6.92603 12.986 6.66469 12.566C6.60869 12.4913 6.56669 12.4073 6.53869 12.314C6.47336 12.1087 6.45469 11.8893 6.48269 11.656C6.54803 11.2547 6.73469 10.9793 7.04269 10.83C7.34136 10.69 7.60736 10.6853 7.84069 10.816C8.05536 10.9187 8.15336 11.1053 8.13469 11.376C8.10669 11.7493 7.93403 12.02 7.61669 12.188C7.57003 12.216 7.50469 12.244 7.42069 12.272L7.36469 12.286L7.40669 12.328C7.53736 12.4307 7.68669 12.482 7.85469 12.482C7.97603 12.4913 8.10203 12.4587 8.23269 12.384C8.47536 12.216 8.65736 11.9593 8.77869 11.614L9.54869 8.53399C9.61403 8.19799 9.62336 7.96466 9.57669 7.83399C9.53003 7.68466 9.40869 7.57732 9.21269 7.51199C9.02603 7.45599 8.83469 7.45599 8.63869 7.51199C8.36803 7.58666 8.13003 7.72666 7.92469 7.93199C7.65403 8.21199 7.45803 8.52466 7.33669 8.86999C7.30869 8.98199 7.28069 9.05199 7.25269 9.07999C7.23403 9.09866 7.13603 9.10799 6.95869 9.10799H6.69269L6.65069 9.06599C6.62269 9.03799 6.60869 9.00066 6.60869 8.95399C6.63669 8.81399 6.69269 8.65532 6.77669 8.47799C6.86069 8.28199 6.96803 8.09532 7.09869 7.91799C7.24803 7.69399 7.45336 7.48399 7.71469 7.28799C7.72403 7.27866 7.73803 7.26932 7.75669 7.25999C8.06469 7.04532 8.39603 6.90066 8.75069 6.82599Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
yAbsolute: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4 16V4H5L5 16H4ZM7.92469 6.83999C8.11136 6.79332 8.33069 6.79799 8.58269 6.85399C9.05869 6.98466 9.36203 7.26932 9.49269 7.70799C9.53003 7.86666 9.53003 8.06732 9.49269 8.30999C9.47403 8.37532 9.40403 8.56666 9.28269 8.88399C8.94669 9.78932 8.72736 10.4847 8.62469 10.97C8.52203 11.5953 8.57803 12.0293 8.79269 12.272C8.95136 12.4587 9.17536 12.5287 9.46469 12.482C9.88469 12.426 10.2534 12.174 10.5707 11.726L10.6547 11.6L11.1587 9.54199C11.5134 8.15132 11.7047 7.43266 11.7327 7.38599C11.882 7.11532 12.106 6.97532 12.4047 6.96599C12.554 6.96599 12.68 7.00799 12.7827 7.09199C12.8294 7.11066 12.8714 7.16199 12.9087 7.24599C12.9554 7.33932 12.9647 7.45599 12.9367 7.59599C12.9087 7.73599 12.6847 8.65532 12.2647 10.354C11.742 12.3887 11.49 13.3733 11.5087 13.308C11.4154 13.6067 11.2754 13.882 11.0887 14.134C10.566 14.9273 9.87536 15.4593 9.01669 15.73C8.60603 15.87 8.20469 15.912 7.81269 15.856C7.11269 15.7533 6.67403 15.436 6.49669 14.904C6.45936 14.6987 6.46403 14.5073 6.51069 14.33C6.56669 14.0967 6.68336 13.91 6.86069 13.77C7.19669 13.5273 7.51403 13.4807 7.81269 13.63C7.92469 13.6953 8.00869 13.784 8.06469 13.896C8.16736 14.12 8.14869 14.3627 8.00869 14.624C7.91536 14.8013 7.78936 14.932 7.63069 15.016L7.54669 15.072L7.61669 15.1C7.98069 15.2587 8.36336 15.2587 8.76469 15.1C9.26869 14.8947 9.68869 14.442 10.0247 13.742C10.09 13.5927 10.1507 13.4433 10.2067 13.294C10.3187 12.9673 10.3654 12.7993 10.3467 12.79C10.3374 12.79 10.3047 12.8087 10.2487 12.846C10.1087 12.93 9.95936 13 9.80069 13.056C9.28736 13.2333 8.76003 13.2007 8.21869 12.958C8.08803 12.902 7.97136 12.832 7.86869 12.748C7.44869 12.412 7.26669 11.8987 7.32269 11.208C7.36003 10.7507 7.57936 9.98066 7.98069 8.89799C7.99003 8.86066 7.99936 8.82799 8.00869 8.79999C8.13936 8.47332 8.20936 8.27732 8.21869 8.21199C8.33069 7.89466 8.34469 7.67532 8.26069 7.55399C8.22336 7.49799 8.14869 7.46999 8.03669 7.46999C7.63536 7.50732 7.30403 7.84799 7.04269 8.49199C6.99603 8.60399 6.95403 8.72532 6.91669 8.85599C6.87003 9.00532 6.83269 9.08466 6.80469 9.09399C6.79536 9.10332 6.69736 9.10799 6.51069 9.10799H6.25869L6.21669 9.06599C6.17003 9.02866 6.17469 8.93066 6.23069 8.77199C6.40803 8.17466 6.67869 7.70332 7.04269 7.35799C7.30403 7.08732 7.59803 6.91466 7.92469 6.83999ZM15 4L15 16H16L16 4H15Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
yRelative: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7.92463 6.83998C8.1113 6.79332 8.33063 6.79798 8.58263 6.85398C9.05863 6.98465 9.36197 7.26932 9.49263 7.70798C9.52997 7.86665 9.52997 8.06732 9.49263 8.30998C9.47397 8.37532 9.40397 8.56665 9.28263 8.88399C8.94663 9.78932 8.7273 10.4847 8.62463 10.97C8.52197 11.5953 8.57797 12.0293 8.79263 12.272C8.9513 12.4587 9.1753 12.5287 9.46463 12.482C9.88463 12.426 10.2533 12.174 10.5706 11.726L10.6546 11.6L11.1586 9.54198C11.5133 8.15132 11.7046 7.43265 11.7326 7.38598C11.882 7.11532 12.106 6.97532 12.4046 6.96598C12.554 6.96598 12.68 7.00798 12.7826 7.09198C12.8293 7.11065 12.8713 7.16198 12.9086 7.24598C12.9553 7.33932 12.9646 7.45598 12.9366 7.59598C12.9086 7.73598 12.6846 8.65532 12.2646 10.354C11.742 12.3887 11.49 13.3733 11.5086 13.308C11.4153 13.6067 11.2753 13.882 11.0886 14.134C10.566 14.9273 9.8753 15.4593 9.01663 15.73C8.60597 15.87 8.20463 15.912 7.81263 15.856C7.11263 15.7533 6.67397 15.436 6.49663 14.904C6.4593 14.6987 6.46397 14.5073 6.51063 14.33C6.56663 14.0967 6.6833 13.91 6.86063 13.77C7.19663 13.5273 7.51397 13.4807 7.81263 13.63C7.92463 13.6953 8.00863 13.784 8.06463 13.896C8.1673 14.12 8.14863 14.3627 8.00863 14.624C7.9153 14.8013 7.7893 14.932 7.63063 15.016L7.54663 15.072L7.61663 15.1C7.98063 15.2587 8.3633 15.2587 8.76463 15.1C9.26863 14.8947 9.68863 14.442 10.0246 13.742C10.09 13.5927 10.1506 13.4433 10.2066 13.294C10.3186 12.9673 10.3653 12.7993 10.3466 12.79C10.3373 12.79 10.3046 12.8087 10.2486 12.846C10.1086 12.93 9.9593 13 9.80063 13.056C9.2873 13.2333 8.75997 13.2007 8.21863 12.958C8.08797 12.902 7.9713 12.832 7.86863 12.748C7.44863 12.412 7.26663 11.8987 7.32263 11.208C7.35997 10.7507 7.5793 9.98065 7.98063 8.89798C7.98997 8.86065 7.9993 8.82798 8.00863 8.79998C8.1393 8.47332 8.2093 8.27732 8.21863 8.21198C8.33063 7.89465 8.34463 7.67532 8.26063 7.55398C8.2233 7.49798 8.14863 7.46998 8.03663 7.46998C7.6353 7.50732 7.30397 7.84798 7.04263 8.49198C6.99597 8.60398 6.95397 8.72532 6.91663 8.85598C6.86997 9.00532 6.83263 9.08465 6.80463 9.09398C6.7953 9.10332 6.6973 9.10798 6.51063 9.10798H6.25863L6.21663 9.06598C6.16997 9.02865 6.17463 8.93065 6.23063 8.77198C6.40797 8.17465 6.67863 7.70332 7.04263 7.35798C7.30397 7.08732 7.59797 6.91465 7.92463 6.83998Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
} as const
|
||||
|
||||
export type CustomIconName = keyof typeof CustomIconMap
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
InterpreterFrom,
|
||||
Prop,
|
||||
StateFrom,
|
||||
assign,
|
||||
} from 'xstate'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { fileMachine } from 'machines/fileMachine'
|
||||
@ -37,7 +38,7 @@ export const FileMachineProvider = ({
|
||||
}) => {
|
||||
const navigate = useNavigate()
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const { project } = useRouteLoaderData(paths.FILE) as IndexLoaderData
|
||||
const { project, file } = useRouteLoaderData(paths.FILE) as IndexLoaderData
|
||||
|
||||
const [state, send] = useMachine(fileMachine, {
|
||||
context: {
|
||||
@ -53,8 +54,32 @@ export const FileMachineProvider = ({
|
||||
context.selectedDirectory + sep() + event.data.name
|
||||
)}`
|
||||
)
|
||||
} else if (
|
||||
event.data &&
|
||||
'path' in event.data &&
|
||||
event.data.path.endsWith(FILE_EXT)
|
||||
) {
|
||||
// Don't navigate to newly created directories
|
||||
navigate(`${paths.FILE}/${encodeURIComponent(event.data.path)}`)
|
||||
}
|
||||
},
|
||||
addFileToRenamingQueue: assign({
|
||||
itemsBeingRenamed: (context, event) => [
|
||||
...context.itemsBeingRenamed,
|
||||
event.data.path,
|
||||
],
|
||||
}),
|
||||
removeFileFromRenamingQueue: assign({
|
||||
itemsBeingRenamed: (
|
||||
context,
|
||||
event: EventFrom<typeof fileMachine, 'done.invoke.rename-file'>
|
||||
) =>
|
||||
context.itemsBeingRenamed.filter(
|
||||
(path) => path !== event.data.oldPath
|
||||
),
|
||||
}),
|
||||
renameToastSuccess: (_, event) => toast.success(event.data.message),
|
||||
createToastSuccess: (_, event) => toast.success(event.data.message),
|
||||
toastSuccess: (_, event) =>
|
||||
event.data && toast.success((event.data || '') + ''),
|
||||
toastError: (_, event) => toast.error((event.data || '') + ''),
|
||||
@ -70,37 +95,56 @@ export const FileMachineProvider = ({
|
||||
}
|
||||
},
|
||||
createFile: async (context, event) => {
|
||||
let name = event.data.name.trim() || DEFAULT_FILE_NAME
|
||||
let createdName = event.data.name.trim() || DEFAULT_FILE_NAME
|
||||
let createdPath: string
|
||||
|
||||
if (event.data.makeDir) {
|
||||
await mkdir(await join(context.selectedDirectory.path, name))
|
||||
createdPath = await join(context.selectedDirectory.path, createdName)
|
||||
await mkdir(createdPath)
|
||||
} else {
|
||||
await create(
|
||||
createdPath =
|
||||
context.selectedDirectory.path +
|
||||
sep() +
|
||||
name +
|
||||
(name.endsWith(FILE_EXT) ? '' : FILE_EXT)
|
||||
)
|
||||
createdName +
|
||||
(createdName.endsWith(FILE_EXT) ? '' : FILE_EXT)
|
||||
await create(createdPath)
|
||||
}
|
||||
|
||||
return `Successfully created "${name}"`
|
||||
return {
|
||||
message: `Successfully created "${createdName}"`,
|
||||
path: createdPath,
|
||||
}
|
||||
},
|
||||
renameFile: async (
|
||||
context: ContextFrom<typeof fileMachine>,
|
||||
event: EventFrom<typeof fileMachine, 'Rename file'>
|
||||
) => {
|
||||
const { oldName, newName, isDir } = event.data
|
||||
let name = newName ? newName : DEFAULT_FILE_NAME
|
||||
const name = newName ? newName : DEFAULT_FILE_NAME
|
||||
const oldPath = await join(context.selectedDirectory.path, oldName)
|
||||
const newDirPath = await join(context.selectedDirectory.path, name)
|
||||
const newPath =
|
||||
newDirPath + (name.endsWith(FILE_EXT) || isDir ? '' : FILE_EXT)
|
||||
|
||||
await rename(
|
||||
await join(context.selectedDirectory.path, oldName),
|
||||
(await join(context.selectedDirectory.path, name)) +
|
||||
(name.endsWith(FILE_EXT) || isDir ? '' : FILE_EXT),
|
||||
{}
|
||||
)
|
||||
return (
|
||||
oldName !== name && `Successfully renamed "${oldName}" to "${name}"`
|
||||
await rename(oldPath, newPath, {})
|
||||
|
||||
if (oldPath === file?.path && project?.path) {
|
||||
// If we just renamed the current file, navigate to the new path
|
||||
navigate(paths.FILE + '/' + encodeURIComponent(newPath))
|
||||
} else if (file?.path.includes(oldPath)) {
|
||||
// If we just renamed a directory that the current file is in, navigate to the new path
|
||||
navigate(
|
||||
paths.FILE +
|
||||
'/' +
|
||||
encodeURIComponent(file.path.replace(oldPath, newDirPath))
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
message: `Successfully renamed "${oldName}" to "${name}"`,
|
||||
newPath,
|
||||
oldPath,
|
||||
}
|
||||
},
|
||||
deleteFile: async (
|
||||
context: ContextFrom<typeof fileMachine>,
|
||||
@ -117,6 +161,17 @@ export const FileMachineProvider = ({
|
||||
console.error('Error deleting file', e)
|
||||
)
|
||||
}
|
||||
|
||||
// If we just deleted the current file or one of its parent directories,
|
||||
// navigate to the project root
|
||||
if (
|
||||
(event.data.path === file?.path ||
|
||||
file?.path.includes(event.data.path)) &&
|
||||
project?.path
|
||||
) {
|
||||
navigate(paths.FILE + '/' + encodeURIComponent(project.path))
|
||||
}
|
||||
|
||||
return `Successfully deleted ${isDir ? 'folder' : 'file'} "${
|
||||
event.data.name
|
||||
}"`
|
||||
|
@ -2,11 +2,11 @@ import type { FileEntry, IndexLoaderData } from 'lib/types'
|
||||
import { paths } from 'lib/paths'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import Tooltip from './Tooltip'
|
||||
import { Dispatch, useEffect, useRef, useState } from 'react'
|
||||
import { Dispatch, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
|
||||
import { Dialog, Disclosure } from '@headlessui/react'
|
||||
import { Disclosure } from '@headlessui/react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
|
||||
import { useFileContext } from 'hooks/useFileContext'
|
||||
import styles from './FileTree.module.css'
|
||||
import { sortProject } from 'lib/tauriFS'
|
||||
@ -16,6 +16,8 @@ import { codeManager, kclManager } from 'lib/singletons'
|
||||
import { useDocumentHasFocus } from 'hooks/useDocumentHasFocus'
|
||||
import { useLspContext } from './LspProvider'
|
||||
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { DeleteConfirmationDialog } from './ProjectCard/DeleteProjectDialog'
|
||||
|
||||
function getIndentationCSS(level: number) {
|
||||
return `calc(1rem * ${level + 1})`
|
||||
@ -23,11 +25,11 @@ function getIndentationCSS(level: number) {
|
||||
|
||||
function RenameForm({
|
||||
fileOrDir,
|
||||
setIsRenaming,
|
||||
onSubmit,
|
||||
level = 0,
|
||||
}: {
|
||||
fileOrDir: FileEntry
|
||||
setIsRenaming: Dispatch<React.SetStateAction<boolean>>
|
||||
onSubmit: () => void
|
||||
level?: number
|
||||
}) {
|
||||
const { send } = useFileContext()
|
||||
@ -35,7 +37,6 @@ function RenameForm({
|
||||
|
||||
function handleRenameSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
setIsRenaming(false)
|
||||
send({
|
||||
type: 'Rename file',
|
||||
data: {
|
||||
@ -49,7 +50,7 @@ function RenameForm({
|
||||
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
||||
if (e.key === 'Escape') {
|
||||
e.stopPropagation()
|
||||
setIsRenaming(false)
|
||||
onSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,10 +62,12 @@ function RenameForm({
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
autoFocus
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
placeholder={fileOrDir.name}
|
||||
className="w-full py-1 bg-transparent text-chalkboard-100 placeholder:text-chalkboard-70 dark:text-chalkboard-10 dark:placeholder:text-chalkboard-50 focus:outline-none focus:ring-0"
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={() => setIsRenaming(false)}
|
||||
onBlur={onSubmit}
|
||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||
/>
|
||||
</label>
|
||||
@ -75,7 +78,7 @@ function RenameForm({
|
||||
)
|
||||
}
|
||||
|
||||
function DeleteConfirmationDialog({
|
||||
function DeleteFileTreeItemDialog({
|
||||
fileOrDir,
|
||||
setIsOpen,
|
||||
}: {
|
||||
@ -84,48 +87,23 @@ function DeleteConfirmationDialog({
|
||||
}) {
|
||||
const { send } = useFileContext()
|
||||
return (
|
||||
<Dialog
|
||||
open={true}
|
||||
onClose={() => setIsOpen(false)}
|
||||
className="relative z-50"
|
||||
>
|
||||
<div className="fixed inset-0 bg-chalkboard-110/80 grid place-content-center">
|
||||
<Dialog.Panel className="rounded p-4 bg-chalkboard-10 dark:bg-chalkboard-100 border border-destroy-80 max-w-2xl">
|
||||
<Dialog.Title as="h2" className="text-2xl font-bold mb-4">
|
||||
Delete {fileOrDir.children !== undefined ? 'Folder' : 'File'}
|
||||
</Dialog.Title>
|
||||
<Dialog.Description className="my-6">
|
||||
This will permanently delete "{fileOrDir.name || 'this file'}"
|
||||
{fileOrDir.children !== undefined
|
||||
? ' and all of its contents. '
|
||||
: '. '}
|
||||
This action cannot be undone.
|
||||
</Dialog.Description>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={async () => {
|
||||
<DeleteConfirmationDialog
|
||||
title={`Delete ${fileOrDir.children !== undefined ? 'folder' : 'file'}`}
|
||||
onDismiss={() => setIsOpen(false)}
|
||||
onConfirm={() => {
|
||||
send({ type: 'Delete file', data: fileOrDir })
|
||||
setIsOpen(false)
|
||||
}}
|
||||
iconStart={{
|
||||
icon: faTrashAlt,
|
||||
bgClassName: 'bg-destroy-80',
|
||||
iconClassName:
|
||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10 dark:text-destroy-20 dark:group-hover:text-destroy-10 dark:hover:text-destroy-10',
|
||||
}}
|
||||
className="hover:border-destroy-40 dark:hover:border-destroy-40"
|
||||
>
|
||||
Delete
|
||||
</ActionButton>
|
||||
<ActionButton Element="button" onClick={() => setIsOpen(false)}>
|
||||
Cancel
|
||||
</ActionButton>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</div>
|
||||
</Dialog>
|
||||
<p className="my-4">
|
||||
This will permanently delete "{fileOrDir.name || 'this file'}"
|
||||
{fileOrDir.children !== undefined ? ' and all of its contents. ' : '. '}
|
||||
</p>
|
||||
<p className="my-4">
|
||||
Are you sure you want to delete "{fileOrDir.name || 'this file'}
|
||||
"? This action cannot be undone.
|
||||
</p>
|
||||
</DeleteConfirmationDialog>
|
||||
)
|
||||
}
|
||||
|
||||
@ -133,35 +111,57 @@ const FileTreeItem = ({
|
||||
project,
|
||||
currentFile,
|
||||
fileOrDir,
|
||||
onDoubleClick,
|
||||
onNavigateToFile,
|
||||
level = 0,
|
||||
}: {
|
||||
project?: IndexLoaderData['project']
|
||||
currentFile?: IndexLoaderData['file']
|
||||
fileOrDir: FileEntry
|
||||
onDoubleClick?: () => void
|
||||
onNavigateToFile?: () => void
|
||||
level?: number
|
||||
}) => {
|
||||
const { send, context } = useFileContext()
|
||||
const { send: fileSend, context: fileContext } = useFileContext()
|
||||
const { onFileOpen, onFileClose } = useLspContext()
|
||||
const navigate = useNavigate()
|
||||
const [isRenaming, setIsRenaming] = useState(false)
|
||||
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
||||
const isCurrentFile = fileOrDir.path === currentFile?.path
|
||||
|
||||
const isRenaming = fileContext.itemsBeingRenamed.includes(fileOrDir.path)
|
||||
const removeCurrentItemFromRenaming = useCallback(
|
||||
() =>
|
||||
fileSend({
|
||||
type: 'assign',
|
||||
data: {
|
||||
itemsBeingRenamed: fileContext.itemsBeingRenamed.filter(
|
||||
(path) => path !== fileOrDir.path
|
||||
),
|
||||
},
|
||||
}),
|
||||
[fileContext.itemsBeingRenamed, fileOrDir.path, fileSend]
|
||||
)
|
||||
|
||||
const addCurrentItemToRenaming = useCallback(() => {
|
||||
fileSend({
|
||||
type: 'assign',
|
||||
data: {
|
||||
itemsBeingRenamed: [...fileContext.itemsBeingRenamed, fileOrDir.path],
|
||||
},
|
||||
})
|
||||
}, [fileContext.itemsBeingRenamed, fileOrDir.path, fileSend])
|
||||
|
||||
function handleKeyUp(e: React.KeyboardEvent<HTMLButtonElement>) {
|
||||
if (e.metaKey && e.key === 'Backspace') {
|
||||
// Open confirmation dialog
|
||||
setIsConfirmingDelete(true)
|
||||
} else if (e.key === 'Enter') {
|
||||
// Show the renaming form
|
||||
setIsRenaming(true)
|
||||
addCurrentItemToRenaming()
|
||||
} else if (e.code === 'Space') {
|
||||
handleDoubleClick()
|
||||
handleClick()
|
||||
}
|
||||
}
|
||||
|
||||
function handleDoubleClick() {
|
||||
function handleClick() {
|
||||
if (fileOrDir.children !== undefined) return // Don't open directories
|
||||
|
||||
if (fileOrDir.name?.endsWith(FILE_EXT) === false && project?.path) {
|
||||
@ -172,7 +172,7 @@ const FileTreeItem = ({
|
||||
codeManager.code
|
||||
)
|
||||
codeManager.writeToFile()
|
||||
kclManager.executeCode(true)
|
||||
kclManager.executeCode(true, true)
|
||||
} else {
|
||||
// Let the lsp servers know we closed a file.
|
||||
onFileClose(currentFile?.path || null, project?.path || null)
|
||||
@ -181,7 +181,7 @@ const FileTreeItem = ({
|
||||
// Open kcl files
|
||||
navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`)
|
||||
}
|
||||
onDoubleClick?.()
|
||||
onNavigateToFile?.()
|
||||
}
|
||||
|
||||
return (
|
||||
@ -199,8 +199,10 @@ const FileTreeItem = ({
|
||||
<button
|
||||
className="flex gap-1 items-center py-0.5 rounded-none border-none p-0 m-0 text-sm w-full hover:!bg-transparent text-left !text-inherit"
|
||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onClick={(e) => e.currentTarget.focus()}
|
||||
onClick={(e) => {
|
||||
e.currentTarget.focus()
|
||||
handleClick()
|
||||
}}
|
||||
onKeyUp={handleKeyUp}
|
||||
>
|
||||
<CustomIcon
|
||||
@ -212,7 +214,7 @@ const FileTreeItem = ({
|
||||
) : (
|
||||
<RenameForm
|
||||
fileOrDir={fileOrDir}
|
||||
setIsRenaming={setIsRenaming}
|
||||
onSubmit={removeCurrentItemFromRenaming}
|
||||
level={level}
|
||||
/>
|
||||
)}
|
||||
@ -225,17 +227,23 @@ const FileTreeItem = ({
|
||||
<Disclosure.Button
|
||||
className={
|
||||
' group border-none text-sm rounded-none p-0 m-0 flex items-center justify-start w-full py-0.5 hover:text-primary hover:bg-primary/5 dark:hover:text-inherit dark:hover:bg-primary/10' +
|
||||
(context.selectedDirectory.path.includes(fileOrDir.path)
|
||||
(fileContext.selectedDirectory.path.includes(fileOrDir.path)
|
||||
? ' ui-open:bg-primary/10'
|
||||
: '')
|
||||
}
|
||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||
onClick={(e) => e.currentTarget.focus()}
|
||||
onClickCapture={(e) =>
|
||||
send({ type: 'Set selected directory', data: fileOrDir })
|
||||
fileSend({
|
||||
type: 'Set selected directory',
|
||||
data: fileOrDir,
|
||||
})
|
||||
}
|
||||
onFocusCapture={(e) =>
|
||||
send({ type: 'Set selected directory', data: fileOrDir })
|
||||
fileSend({
|
||||
type: 'Set selected directory',
|
||||
data: fileOrDir,
|
||||
})
|
||||
}
|
||||
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
|
||||
onKeyUp={handleKeyUp}
|
||||
@ -263,7 +271,7 @@ const FileTreeItem = ({
|
||||
/>
|
||||
<RenameForm
|
||||
fileOrDir={fileOrDir}
|
||||
setIsRenaming={setIsRenaming}
|
||||
onSubmit={removeCurrentItemFromRenaming}
|
||||
level={-1}
|
||||
/>
|
||||
</div>
|
||||
@ -279,10 +287,16 @@ const FileTreeItem = ({
|
||||
<ul
|
||||
className="m-0 p-0"
|
||||
onClickCapture={(e) => {
|
||||
send({ type: 'Set selected directory', data: fileOrDir })
|
||||
fileSend({
|
||||
type: 'Set selected directory',
|
||||
data: fileOrDir,
|
||||
})
|
||||
}}
|
||||
onFocusCapture={(e) =>
|
||||
send({ type: 'Set selected directory', data: fileOrDir })
|
||||
fileSend({
|
||||
type: 'Set selected directory',
|
||||
data: fileOrDir,
|
||||
})
|
||||
}
|
||||
>
|
||||
{fileOrDir.children?.map((child) => (
|
||||
@ -290,7 +304,7 @@ const FileTreeItem = ({
|
||||
fileOrDir={child}
|
||||
project={project}
|
||||
currentFile={currentFile}
|
||||
onDoubleClick={onDoubleClick}
|
||||
onNavigateToFile={onNavigateToFile}
|
||||
level={level + 1}
|
||||
key={level + '-' + child.path}
|
||||
/>
|
||||
@ -302,7 +316,7 @@ const FileTreeItem = ({
|
||||
</Disclosure>
|
||||
)}
|
||||
{isConfirmingDelete && (
|
||||
<DeleteConfirmationDialog
|
||||
<DeleteFileTreeItemDialog
|
||||
fileOrDir={fileOrDir}
|
||||
setIsOpen={setIsConfirmingDelete}
|
||||
/>
|
||||
@ -314,7 +328,7 @@ const FileTreeItem = ({
|
||||
interface FileTreeProps {
|
||||
className?: string
|
||||
file?: IndexLoaderData['file']
|
||||
closePanel: (
|
||||
onNavigateToFile: (
|
||||
focusableElement?:
|
||||
| HTMLElement
|
||||
| React.MutableRefObject<HTMLElement | null>
|
||||
@ -371,30 +385,34 @@ export const FileTreeMenu = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export const FileTree = ({ className = '', closePanel }: FileTreeProps) => {
|
||||
export const FileTree = ({
|
||||
className = '',
|
||||
onNavigateToFile: closePanel,
|
||||
}: FileTreeProps) => {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="flex items-center gap-1 px-4 py-1 bg-chalkboard-20/40 dark:bg-chalkboard-80/50 border-b border-b-chalkboard-30 dark:border-b-chalkboard-80">
|
||||
<h2 className="flex-1 m-0 p-0 text-sm mono">Files</h2>
|
||||
<FileTreeMenu />
|
||||
</div>
|
||||
<FileTreeInner onDoubleClick={closePanel} />
|
||||
<FileTreeInner onNavigateToFile={closePanel} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const FileTreeInner = ({
|
||||
onDoubleClick,
|
||||
onNavigateToFile,
|
||||
}: {
|
||||
onDoubleClick?: () => void
|
||||
onNavigateToFile?: () => void
|
||||
}) => {
|
||||
const loaderData = useRouteLoaderData(paths.FILE) as IndexLoaderData
|
||||
const { send, context } = useFileContext()
|
||||
const { send: fileSend, context: fileContext } = useFileContext()
|
||||
const { send: modelingSend } = useModelingContext()
|
||||
const documentHasFocus = useDocumentHasFocus()
|
||||
|
||||
// Refresh the file tree when the document gets focus
|
||||
useEffect(() => {
|
||||
send({ type: 'Refresh' })
|
||||
fileSend({ type: 'Refresh' })
|
||||
}, [documentHasFocus])
|
||||
|
||||
return (
|
||||
@ -402,15 +420,22 @@ export const FileTreeInner = ({
|
||||
<ul
|
||||
className="m-0 p-0 text-sm"
|
||||
onClickCapture={(e) => {
|
||||
send({ type: 'Set selected directory', data: context.project })
|
||||
fileSend({
|
||||
type: 'Set selected directory',
|
||||
data: fileContext.project,
|
||||
})
|
||||
}}
|
||||
>
|
||||
{sortProject(context.project.children || []).map((fileOrDir) => (
|
||||
{sortProject(fileContext.project?.children || []).map((fileOrDir) => (
|
||||
<FileTreeItem
|
||||
project={context.project}
|
||||
project={fileContext.project}
|
||||
currentFile={loaderData?.file}
|
||||
fileOrDir={fileOrDir}
|
||||
onDoubleClick={onDoubleClick}
|
||||
onNavigateToFile={() => {
|
||||
// Reset modeling state when navigating to a new file
|
||||
modelingSend({ type: 'Cancel' })
|
||||
onNavigateToFile?.()
|
||||
}}
|
||||
key={fileOrDir.path}
|
||||
/>
|
||||
))}
|
||||
|
269
src/components/Gizmo.tsx
Normal file
@ -0,0 +1,269 @@
|
||||
import { SceneInfra } from 'clientSideScene/sceneInfra'
|
||||
import { sceneInfra } from 'lib/singletons'
|
||||
import { MutableRefObject, useEffect, useRef } from 'react'
|
||||
import {
|
||||
WebGLRenderer,
|
||||
Scene,
|
||||
OrthographicCamera,
|
||||
BoxGeometry,
|
||||
SphereGeometry,
|
||||
MeshBasicMaterial,
|
||||
Color,
|
||||
Mesh,
|
||||
Clock,
|
||||
Quaternion,
|
||||
ColorRepresentation,
|
||||
Vector2,
|
||||
Raycaster,
|
||||
Camera,
|
||||
Intersection,
|
||||
Object3D,
|
||||
} from 'three'
|
||||
|
||||
const CANVAS_SIZE = 80
|
||||
const FRUSTUM_SIZE = 0.5
|
||||
const AXIS_LENGTH = 0.35
|
||||
const AXIS_WIDTH = 0.02
|
||||
enum AxisColors {
|
||||
X = '#fa6668',
|
||||
Y = '#11eb6b',
|
||||
Z = '#6689ef',
|
||||
Gray = '#c6c7c2',
|
||||
}
|
||||
enum AxisNames {
|
||||
X = 'x',
|
||||
Y = 'y',
|
||||
Z = 'z',
|
||||
NEG_X = '-x',
|
||||
NEG_Y = '-y',
|
||||
NEG_Z = '-z',
|
||||
}
|
||||
|
||||
export default function Gizmo() {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
||||
const raycasterIntersect = useRef<Intersection<Object3D> | null>(null)
|
||||
const cameraPassiveUpdateTimer = useRef(0)
|
||||
const raycasterPassiveUpdateTimer = useRef(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (!canvasRef.current) return
|
||||
|
||||
const canvas = canvasRef.current
|
||||
const renderer = new WebGLRenderer({ canvas, antialias: true, alpha: true })
|
||||
renderer.setSize(CANVAS_SIZE, CANVAS_SIZE)
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
|
||||
|
||||
const scene = new Scene()
|
||||
const camera = createCamera()
|
||||
const { gizmoAxes, gizmoAxisHeads } = createGizmo()
|
||||
scene.add(...gizmoAxes, ...gizmoAxisHeads)
|
||||
|
||||
const raycaster = new Raycaster()
|
||||
const { mouse, disposeMouseEvents } = initializeMouseEvents(
|
||||
canvas,
|
||||
raycasterIntersect,
|
||||
sceneInfra
|
||||
)
|
||||
const raycasterObjects = [...gizmoAxisHeads]
|
||||
|
||||
const clock = new Clock()
|
||||
const clientCamera = sceneInfra.camControls.camera
|
||||
let currentQuaternion = new Quaternion().copy(clientCamera.quaternion)
|
||||
|
||||
const animate = () => {
|
||||
const delta = clock.getDelta()
|
||||
updateCameraOrientation(
|
||||
camera,
|
||||
currentQuaternion,
|
||||
sceneInfra.camControls.camera.quaternion,
|
||||
delta,
|
||||
cameraPassiveUpdateTimer
|
||||
)
|
||||
updateRayCaster(
|
||||
raycasterObjects,
|
||||
raycaster,
|
||||
mouse,
|
||||
camera,
|
||||
raycasterIntersect,
|
||||
delta,
|
||||
raycasterPassiveUpdateTimer
|
||||
)
|
||||
renderer.render(scene, camera)
|
||||
requestAnimationFrame(animate)
|
||||
}
|
||||
animate()
|
||||
|
||||
return () => {
|
||||
renderer.dispose()
|
||||
disposeMouseEvents()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="grid place-content-center rounded-full overflow-hidden border border-solid border-primary/50 pointer-events-none">
|
||||
<canvas ref={canvasRef} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const createCamera = (): OrthographicCamera => {
|
||||
return new OrthographicCamera(
|
||||
-FRUSTUM_SIZE,
|
||||
FRUSTUM_SIZE,
|
||||
FRUSTUM_SIZE,
|
||||
-FRUSTUM_SIZE,
|
||||
0.5,
|
||||
3
|
||||
)
|
||||
}
|
||||
|
||||
const createGizmo = () => {
|
||||
const gizmoAxes = [
|
||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.X, 0, 'z'),
|
||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Y, Math.PI / 2, 'z'),
|
||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Z, -Math.PI / 2, 'y'),
|
||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Gray, Math.PI, 'z'),
|
||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Gray, -Math.PI / 2, 'z'),
|
||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Gray, Math.PI / 2, 'y'),
|
||||
]
|
||||
|
||||
const gizmoAxisHeads = [
|
||||
createAxisHead(AxisNames.X, AxisColors.X, [AXIS_LENGTH, 0, 0]),
|
||||
createAxisHead(AxisNames.Y, AxisColors.Y, [0, AXIS_LENGTH, 0]),
|
||||
createAxisHead(AxisNames.Z, AxisColors.Z, [0, 0, AXIS_LENGTH]),
|
||||
createAxisHead(AxisNames.NEG_X, AxisColors.Gray, [-AXIS_LENGTH, 0, 0]),
|
||||
createAxisHead(AxisNames.NEG_Y, AxisColors.Gray, [0, -AXIS_LENGTH, 0]),
|
||||
createAxisHead(AxisNames.NEG_Z, AxisColors.Gray, [0, 0, -AXIS_LENGTH]),
|
||||
]
|
||||
|
||||
return { gizmoAxes, gizmoAxisHeads }
|
||||
}
|
||||
|
||||
const createAxis = (
|
||||
length: number,
|
||||
width: number,
|
||||
color: ColorRepresentation,
|
||||
rotation = 0,
|
||||
axis = 'x'
|
||||
): Mesh => {
|
||||
const geometry = new BoxGeometry(length, width, width)
|
||||
geometry.translate(length / 2, 0, 0)
|
||||
const material = new MeshBasicMaterial({ color: new Color(color) })
|
||||
const mesh = new Mesh(geometry, material)
|
||||
mesh.rotation[axis as 'x' | 'y' | 'z'] = rotation
|
||||
return mesh
|
||||
}
|
||||
|
||||
const createAxisHead = (
|
||||
name: AxisNames,
|
||||
color: ColorRepresentation,
|
||||
position: number[]
|
||||
): Mesh => {
|
||||
const geometry = new SphereGeometry(0.065, 16, 8)
|
||||
const material = new MeshBasicMaterial({ color: new Color(color) })
|
||||
const mesh = new Mesh(geometry, material)
|
||||
|
||||
mesh.position.set(position[0], position[1], position[2])
|
||||
mesh.updateMatrixWorld()
|
||||
mesh.name = name
|
||||
return mesh
|
||||
}
|
||||
|
||||
const updateCameraOrientation = (
|
||||
camera: OrthographicCamera,
|
||||
currentQuaternion: Quaternion,
|
||||
targetQuaternion: Quaternion,
|
||||
deltaTime: number,
|
||||
cameraPassiveUpdateTimer: MutableRefObject<number>
|
||||
) => {
|
||||
cameraPassiveUpdateTimer.current += deltaTime
|
||||
if (
|
||||
!quaternionsEqual(currentQuaternion, targetQuaternion) ||
|
||||
cameraPassiveUpdateTimer.current >= 5
|
||||
) {
|
||||
const slerpFactor = 1 - Math.exp(-30 * deltaTime)
|
||||
currentQuaternion.slerp(targetQuaternion, slerpFactor).normalize()
|
||||
camera.position.set(0, 0, 1).applyQuaternion(currentQuaternion)
|
||||
camera.quaternion.copy(currentQuaternion)
|
||||
cameraPassiveUpdateTimer.current = 0
|
||||
}
|
||||
}
|
||||
|
||||
const quaternionsEqual = (
|
||||
q1: Quaternion,
|
||||
q2: Quaternion,
|
||||
tolerance: number = 0.001
|
||||
): boolean => {
|
||||
return (
|
||||
Math.abs(q1.x - q2.x) < tolerance &&
|
||||
Math.abs(q1.y - q2.y) < tolerance &&
|
||||
Math.abs(q1.z - q2.z) < tolerance &&
|
||||
Math.abs(q1.w - q2.w) < tolerance
|
||||
)
|
||||
}
|
||||
|
||||
const initializeMouseEvents = (
|
||||
canvas: HTMLCanvasElement,
|
||||
raycasterIntersect: MutableRefObject<Intersection<Object3D> | null>,
|
||||
sceneInfra: SceneInfra
|
||||
): { mouse: Vector2; disposeMouseEvents: () => void } => {
|
||||
const mouse = new Vector2()
|
||||
mouse.x = 1 // fix initial mouse position issue
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
const { left, top, width, height } = canvas.getBoundingClientRect()
|
||||
mouse.x = ((event.clientX - left) / width) * 2 - 1
|
||||
mouse.y = ((event.clientY - top) / height) * -2 + 1
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
if (raycasterIntersect.current) {
|
||||
const axisName = raycasterIntersect.current.object.name as AxisNames
|
||||
sceneInfra.camControls.updateCameraToAxis(axisName)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
window.addEventListener('click', handleClick)
|
||||
|
||||
const disposeMouseEvents = () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
window.removeEventListener('click', handleClick)
|
||||
}
|
||||
|
||||
return { mouse, disposeMouseEvents }
|
||||
}
|
||||
|
||||
const updateRayCaster = (
|
||||
objects: Object3D[],
|
||||
raycaster: Raycaster,
|
||||
mouse: Vector2,
|
||||
camera: Camera,
|
||||
raycasterIntersect: MutableRefObject<Intersection<Object3D> | null>,
|
||||
deltaTime: number,
|
||||
raycasterPassiveUpdateTimer: MutableRefObject<number>
|
||||
) => {
|
||||
raycasterPassiveUpdateTimer.current += deltaTime
|
||||
|
||||
// check if mouse is outside the canvas bounds and stop raycaster
|
||||
if (raycasterPassiveUpdateTimer.current < 2) {
|
||||
if (mouse.x < -1 || mouse.x > 1 || mouse.y < -1 || mouse.y > 1) {
|
||||
raycasterIntersect.current = null
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
raycaster.setFromCamera(mouse, camera)
|
||||
const intersects = raycaster.intersectObjects(objects)
|
||||
|
||||
objects.forEach((object) => object.scale.set(1, 1, 1))
|
||||
if (intersects.length) {
|
||||
intersects[0].object.scale.set(1.5, 1.5, 1.5)
|
||||
raycasterIntersect.current = intersects[0] // filter first object
|
||||
} else {
|
||||
raycasterIntersect.current = null
|
||||
}
|
||||
if (raycasterPassiveUpdateTimer.current > 2) {
|
||||
raycasterPassiveUpdateTimer.current = 0
|
||||
}
|
||||
}
|
@ -81,6 +81,12 @@ export function HelpMenu(props: React.PropsWithChildren) {
|
||||
>
|
||||
Release notes
|
||||
</HelpMenuItem>
|
||||
<HelpMenuItem
|
||||
as="button"
|
||||
onClick={() => navigate('settings?tab=keybindings')}
|
||||
>
|
||||
Keyboard shortcuts
|
||||
</HelpMenuItem>
|
||||
<HelpMenuItem
|
||||
as="button"
|
||||
onClick={() => {
|
||||
|
@ -1,14 +1,65 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import {
|
||||
EngineConnectionStateType,
|
||||
DisconnectingType,
|
||||
EngineCommandManagerEvents,
|
||||
EngineConnectionEvents,
|
||||
ConnectionError,
|
||||
CONNECTION_ERROR_TEXT,
|
||||
} from '../lang/std/engineConnection'
|
||||
|
||||
import { engineCommandManager } from '../lib/singletons'
|
||||
|
||||
const Loading = ({ children }: React.PropsWithChildren) => {
|
||||
const [hasLongLoadTime, setHasLongLoadTime] = useState(false)
|
||||
const [error, setError] = useState<ConnectionError>(ConnectionError.Unset)
|
||||
|
||||
useEffect(() => {
|
||||
const onConnectionStateChange = ({ detail: state }: CustomEvent) => {
|
||||
if (
|
||||
(state.type !== EngineConnectionStateType.Disconnected ||
|
||||
state.type !== EngineConnectionStateType.Disconnecting) &&
|
||||
state.value?.type !== DisconnectingType.Error
|
||||
)
|
||||
return
|
||||
setError(state.value.value.error)
|
||||
}
|
||||
|
||||
const onEngineAvailable = ({ detail: engineConnection }: CustomEvent) => {
|
||||
engineConnection.addEventListener(
|
||||
EngineConnectionEvents.ConnectionStateChanged,
|
||||
onConnectionStateChange as EventListener
|
||||
)
|
||||
}
|
||||
|
||||
engineCommandManager.addEventListener(
|
||||
EngineCommandManagerEvents.EngineAvailable,
|
||||
onEngineAvailable as EventListener
|
||||
)
|
||||
|
||||
return () => {
|
||||
engineCommandManager.removeEventListener(
|
||||
EngineCommandManagerEvents.EngineAvailable,
|
||||
onEngineAvailable as EventListener
|
||||
)
|
||||
engineCommandManager.engineConnection?.removeEventListener(
|
||||
EngineConnectionEvents.ConnectionStateChanged,
|
||||
onConnectionStateChange as EventListener
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// Don't set long loading time if there's a more severe error
|
||||
if (error > ConnectionError.LongLoadingTime) return
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setHasLongLoadTime(true)
|
||||
setError(ConnectionError.LongLoadingTime)
|
||||
}, 4000)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [setHasLongLoadTime])
|
||||
}, [error, setError])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="body-bg flex flex-col items-center justify-center h-screen"
|
||||
@ -29,10 +80,10 @@ const Loading = ({ children }: React.PropsWithChildren) => {
|
||||
<p
|
||||
className={
|
||||
'text-sm mt-4 text-primary/60 transition-opacity duration-500' +
|
||||
(hasLongLoadTime ? ' opacity-100' : ' opacity-0')
|
||||
(error !== ConnectionError.Unset ? ' opacity-100' : ' opacity-0')
|
||||
}
|
||||
>
|
||||
Loading is taking longer than expected.
|
||||
{CONNECTION_ERROR_TEXT[error]}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
@ -13,17 +13,19 @@ export function LowerRightControls(props: React.PropsWithChildren) {
|
||||
const linkOverrideClassName =
|
||||
'!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30'
|
||||
|
||||
const isPlayWright = window?.localStorage.getItem('playwright') === 'true'
|
||||
|
||||
return (
|
||||
<section className="fixed bottom-2 right-2">
|
||||
<section className="fixed bottom-2 right-2 flex flex-col items-end gap-3 pointer-events-none">
|
||||
{props.children}
|
||||
<menu className="flex items-center justify-end gap-3">
|
||||
<menu className="flex items-center justify-end gap-3 pointer-events-auto">
|
||||
<a
|
||||
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={'!no-underline font-mono text-xs ' + linkOverrideClassName}
|
||||
>
|
||||
v{APP_VERSION}
|
||||
v{isPlayWright ? '11.22.33' : APP_VERSION}
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/KittyCAD/modeling-app/issues/new/choose"
|
||||
|
@ -13,7 +13,6 @@ import { LanguageSupport } from '@codemirror/language'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { paths } from 'lib/paths'
|
||||
import { FileEntry } from 'lib/types'
|
||||
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
|
||||
import Worker from 'editor/plugins/lsp/worker.ts?worker'
|
||||
import {
|
||||
LspWorkerEventType,
|
||||
@ -23,6 +22,8 @@ import {
|
||||
} from 'editor/plugins/lsp/types'
|
||||
import { wasmUrl } from 'lang/wasm'
|
||||
import { PROJECT_ENTRYPOINT } from 'lib/constants'
|
||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||
|
||||
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
|
||||
return []
|
||||
@ -86,7 +87,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
} = useSettingsAuthContext()
|
||||
const token = auth?.context.token
|
||||
const navigate = useNavigate()
|
||||
const { overallState } = useNetworkStatus()
|
||||
const { overallState } = useNetworkContext()
|
||||
const isNetworkOkay = overallState === NetworkHealthState.Ok
|
||||
|
||||
// So this is a bit weird, we need to initialize the lsp server and client.
|
||||
|
@ -11,13 +11,17 @@ import {
|
||||
import { SetSelections, modelingMachine } from 'machines/modelingMachine'
|
||||
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { isCursorInSketchCommandRange } from 'lang/util'
|
||||
import {
|
||||
isCursorInSketchCommandRange,
|
||||
updatePathToNodeFromMap,
|
||||
} from 'lang/util'
|
||||
import {
|
||||
kclManager,
|
||||
sceneInfra,
|
||||
engineCommandManager,
|
||||
codeManager,
|
||||
editorManager,
|
||||
sceneEntitiesManager,
|
||||
} from 'lib/singletons'
|
||||
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
|
||||
import {
|
||||
@ -39,11 +43,24 @@ import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
||||
import useStateMachineCommands from 'hooks/useStateMachineCommands'
|
||||
import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConfig'
|
||||
import {
|
||||
STRAIGHT_SEGMENT,
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
getParentGroup,
|
||||
getSketchOrientationDetails,
|
||||
getSketchQuaternion,
|
||||
} from 'clientSideScene/sceneEntities'
|
||||
import { sketchOnExtrudedFace, startSketchOnDefault } from 'lang/modifyAst'
|
||||
import { Program, VariableDeclaration, coreDump } from 'lang/wasm'
|
||||
import {
|
||||
moveValueIntoNewVariablePath,
|
||||
sketchOnExtrudedFace,
|
||||
startSketchOnDefault,
|
||||
} from 'lang/modifyAst'
|
||||
import {
|
||||
Program,
|
||||
VariableDeclaration,
|
||||
coreDump,
|
||||
parse,
|
||||
recast,
|
||||
} from 'lang/wasm'
|
||||
import {
|
||||
getNodeFromPath,
|
||||
getNodePathFromSourceRange,
|
||||
@ -57,6 +74,7 @@ import { EditorSelection } from '@uiw/react-codemirror'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
||||
import { getVarNameModal } from 'hooks/useToolbarGuards'
|
||||
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
@ -127,10 +145,84 @@ export const ModelingMachineProvider = ({
|
||||
},
|
||||
'Set mouse state': assign({
|
||||
mouseState: (_, event) => event.data,
|
||||
segmentHoverMap: ({ mouseState, segmentHoverMap }, event) => {
|
||||
if (event.data.type === 'isHovering') {
|
||||
const parent = getParentGroup(event.data.on, [
|
||||
STRAIGHT_SEGMENT,
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
])
|
||||
const pathToNode = parent?.userData?.pathToNode
|
||||
const pathToNodeString = JSON.stringify(pathToNode)
|
||||
if (!parent || !pathToNode) return segmentHoverMap
|
||||
if (segmentHoverMap[pathToNodeString] !== undefined)
|
||||
clearTimeout(segmentHoverMap[JSON.stringify(pathToNode)])
|
||||
return {
|
||||
...segmentHoverMap,
|
||||
[pathToNodeString]: 0,
|
||||
}
|
||||
} else if (
|
||||
event.data.type === 'idle' &&
|
||||
mouseState.type === 'isHovering'
|
||||
) {
|
||||
const mouseOnParent = getParentGroup(mouseState.on, [
|
||||
STRAIGHT_SEGMENT,
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
])
|
||||
if (!mouseOnParent || !mouseOnParent?.userData?.pathToNode)
|
||||
return segmentHoverMap
|
||||
const pathToNodeString = JSON.stringify(
|
||||
mouseOnParent?.userData?.pathToNode
|
||||
)
|
||||
const timeoutId = setTimeout(() => {
|
||||
sceneInfra.modelingSend({
|
||||
type: 'Set mouse state',
|
||||
data: {
|
||||
type: 'timeoutEnd',
|
||||
pathToNodeString,
|
||||
},
|
||||
})
|
||||
}, 800) as unknown as number
|
||||
return {
|
||||
...segmentHoverMap,
|
||||
[pathToNodeString]: timeoutId,
|
||||
}
|
||||
} else if (event.data.type === 'timeoutEnd') {
|
||||
const copy = { ...segmentHoverMap }
|
||||
delete copy[event.data.pathToNodeString]
|
||||
return copy
|
||||
}
|
||||
return {}
|
||||
},
|
||||
}),
|
||||
'Set selection': assign(({ selectionRanges }, event) => {
|
||||
if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
|
||||
const setSelections = event.data
|
||||
'Set Segment Overlays': assign({
|
||||
segmentOverlays: ({ segmentOverlays }, { data }) => {
|
||||
if (data.type === 'set-many') return data.overlays
|
||||
if (data.type === 'set-one')
|
||||
return {
|
||||
...segmentOverlays,
|
||||
[data.pathToNodeString]: data.seg,
|
||||
}
|
||||
if (data.type === 'delete-one') {
|
||||
const copy = { ...segmentOverlays }
|
||||
delete copy[data.pathToNodeString]
|
||||
return copy
|
||||
}
|
||||
// data.type === 'clear'
|
||||
return {}
|
||||
},
|
||||
}),
|
||||
'Set sketchDetails': assign(({ sketchDetails }, event) =>
|
||||
sketchDetails
|
||||
? {
|
||||
sketchDetails: {
|
||||
...sketchDetails,
|
||||
sketchPathToNode: event.data,
|
||||
},
|
||||
}
|
||||
: {}
|
||||
),
|
||||
'Set selection': assign(({ selectionRanges, sketchDetails }, event) => {
|
||||
const setSelections = event.data as SetSelections // this was needed for ts after adding 'Set selection' action to on done modal events
|
||||
if (!editorManager.editorView) return {}
|
||||
const dispatchSelection = (selection?: EditorSelection) => {
|
||||
if (!selection) return // TODO less of hack for the below please
|
||||
@ -217,11 +309,29 @@ export const ModelingMachineProvider = ({
|
||||
selectionRanges: selections,
|
||||
}
|
||||
}
|
||||
if (setSelections.selectionType === 'completeSelection') {
|
||||
editorManager.selectRange(setSelections.selection)
|
||||
if (!sketchDetails)
|
||||
return {
|
||||
selectionRanges: setSelections.selection,
|
||||
}
|
||||
return {
|
||||
selectionRanges: setSelections.selection,
|
||||
sketchDetails: {
|
||||
...sketchDetails,
|
||||
sketchPathToNode:
|
||||
setSelections.updatedPathToNode ||
|
||||
sketchDetails?.sketchPathToNode ||
|
||||
[],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
}),
|
||||
'Engine export': (_, event) => {
|
||||
'Engine export': async (_, event) => {
|
||||
if (event.type !== 'Export' || TEST) return
|
||||
console.log('exporting', event.data)
|
||||
const format = {
|
||||
...event.data,
|
||||
} as Partial<Models['OutputFormat_type']>
|
||||
@ -265,9 +375,16 @@ export const ModelingMachineProvider = ({
|
||||
format.selection = { type: 'default_scene' }
|
||||
}
|
||||
|
||||
toast.promise(
|
||||
exportFromEngine({
|
||||
format: format as Models['OutputFormat_type'],
|
||||
}).catch((e) => toast.error('Error while exporting', e)) // TODO I think we need to throw the error from engineCommandManager
|
||||
}),
|
||||
{
|
||||
loading: 'Exporting...',
|
||||
success: 'Exported successfully',
|
||||
error: 'Error while exporting',
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
guards: {
|
||||
@ -300,8 +417,23 @@ export const ModelingMachineProvider = ({
|
||||
selectionRanges
|
||||
)
|
||||
},
|
||||
'Has exportable geometry': () =>
|
||||
kclManager.kclErrors.length === 0 && kclManager.ast.body.length > 0,
|
||||
'Has exportable geometry': () => {
|
||||
if (
|
||||
kclManager.kclErrors.length === 0 &&
|
||||
kclManager.ast.body.length > 0
|
||||
)
|
||||
return true
|
||||
else {
|
||||
let errorMessage = 'Unable to Export '
|
||||
if (kclManager.kclErrors.length > 0)
|
||||
errorMessage += 'due to KCL Errors'
|
||||
else if (kclManager.ast.body.length === 0)
|
||||
errorMessage += 'due to Empty Scene'
|
||||
console.error(errorMessage)
|
||||
toast.error(errorMessage)
|
||||
return false
|
||||
}
|
||||
},
|
||||
},
|
||||
services: {
|
||||
'AST-undo-startSketchOn': async ({ sketchDetails }) => {
|
||||
@ -321,7 +453,8 @@ export const ModelingMachineProvider = ({
|
||||
const { modifiedAst, pathToNode: pathToNewSketchNode } =
|
||||
sketchOnExtrudedFace(
|
||||
kclManager.ast,
|
||||
data.extrudeSegmentPathToNode,
|
||||
data.sketchPathToNode,
|
||||
data.extrudePathToNode,
|
||||
kclManager.programMemory,
|
||||
data.cap
|
||||
)
|
||||
@ -376,13 +509,26 @@ export const ModelingMachineProvider = ({
|
||||
},
|
||||
'Get horizontal info': async ({
|
||||
selectionRanges,
|
||||
sketchDetails,
|
||||
}): Promise<SetSelections> => {
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
await applyConstraintHorzVertDistance({
|
||||
constraint: 'setHorzDistance',
|
||||
selectionRanges,
|
||||
})
|
||||
await kclManager.updateAst(modifiedAst, true)
|
||||
const _modifiedAst = parse(recast(modifiedAst))
|
||||
if (!sketchDetails) throw new Error('No sketch details')
|
||||
const updatedPathToNode = updatePathToNodeFromMap(
|
||||
sketchDetails.sketchPathToNode,
|
||||
pathToNodeMap
|
||||
)
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
updatedPathToNode,
|
||||
_modifiedAst,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
sketchDetails.origin
|
||||
)
|
||||
return {
|
||||
selectionType: 'completeSelection',
|
||||
selection: pathMapToSelections(
|
||||
@ -390,17 +536,31 @@ export const ModelingMachineProvider = ({
|
||||
selectionRanges,
|
||||
pathToNodeMap
|
||||
),
|
||||
updatedPathToNode,
|
||||
}
|
||||
},
|
||||
'Get vertical info': async ({
|
||||
selectionRanges,
|
||||
sketchDetails,
|
||||
}): Promise<SetSelections> => {
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
await applyConstraintHorzVertDistance({
|
||||
constraint: 'setVertDistance',
|
||||
selectionRanges,
|
||||
})
|
||||
await kclManager.updateAst(modifiedAst, true)
|
||||
const _modifiedAst = parse(recast(modifiedAst))
|
||||
if (!sketchDetails) throw new Error('No sketch details')
|
||||
const updatedPathToNode = updatePathToNodeFromMap(
|
||||
sketchDetails.sketchPathToNode,
|
||||
pathToNodeMap
|
||||
)
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
updatedPathToNode,
|
||||
_modifiedAst,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
sketchDetails.origin
|
||||
)
|
||||
return {
|
||||
selectionType: 'completeSelection',
|
||||
selection: pathMapToSelections(
|
||||
@ -408,10 +568,12 @@ export const ModelingMachineProvider = ({
|
||||
selectionRanges,
|
||||
pathToNodeMap
|
||||
),
|
||||
updatedPathToNode,
|
||||
}
|
||||
},
|
||||
'Get angle info': async ({
|
||||
selectionRanges,
|
||||
sketchDetails,
|
||||
}): Promise<SetSelections> => {
|
||||
const { modifiedAst, pathToNodeMap } = await (angleBetweenInfo({
|
||||
selectionRanges,
|
||||
@ -423,22 +585,48 @@ export const ModelingMachineProvider = ({
|
||||
selectionRanges,
|
||||
angleOrLength: 'setAngle',
|
||||
}))
|
||||
await kclManager.updateAst(modifiedAst, true)
|
||||
const _modifiedAst = parse(recast(modifiedAst))
|
||||
if (!sketchDetails) throw new Error('No sketch details')
|
||||
const updatedPathToNode = updatePathToNodeFromMap(
|
||||
sketchDetails.sketchPathToNode,
|
||||
pathToNodeMap
|
||||
)
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
updatedPathToNode,
|
||||
_modifiedAst,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
sketchDetails.origin
|
||||
)
|
||||
return {
|
||||
selectionType: 'completeSelection',
|
||||
selection: pathMapToSelections(
|
||||
kclManager.ast,
|
||||
_modifiedAst,
|
||||
selectionRanges,
|
||||
pathToNodeMap
|
||||
),
|
||||
updatedPathToNode,
|
||||
}
|
||||
},
|
||||
'Get length info': async ({
|
||||
selectionRanges,
|
||||
sketchDetails,
|
||||
}): Promise<SetSelections> => {
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
await applyConstraintAngleLength({ selectionRanges })
|
||||
await kclManager.updateAst(modifiedAst, true)
|
||||
const _modifiedAst = parse(recast(modifiedAst))
|
||||
if (!sketchDetails) throw new Error('No sketch details')
|
||||
const updatedPathToNode = updatePathToNodeFromMap(
|
||||
sketchDetails.sketchPathToNode,
|
||||
pathToNodeMap
|
||||
)
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
updatedPathToNode,
|
||||
_modifiedAst,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
sketchDetails.origin
|
||||
)
|
||||
return {
|
||||
selectionType: 'completeSelection',
|
||||
selection: pathMapToSelections(
|
||||
@ -446,17 +634,31 @@ export const ModelingMachineProvider = ({
|
||||
selectionRanges,
|
||||
pathToNodeMap
|
||||
),
|
||||
updatedPathToNode,
|
||||
}
|
||||
},
|
||||
'Get perpendicular distance info': async ({
|
||||
selectionRanges,
|
||||
sketchDetails,
|
||||
}): Promise<SetSelections> => {
|
||||
const { modifiedAst, pathToNodeMap } = await applyConstraintIntersect(
|
||||
{
|
||||
selectionRanges,
|
||||
}
|
||||
)
|
||||
await kclManager.updateAst(modifiedAst, true)
|
||||
const _modifiedAst = parse(recast(modifiedAst))
|
||||
if (!sketchDetails) throw new Error('No sketch details')
|
||||
const updatedPathToNode = updatePathToNodeFromMap(
|
||||
sketchDetails.sketchPathToNode,
|
||||
pathToNodeMap
|
||||
)
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
updatedPathToNode,
|
||||
_modifiedAst,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
sketchDetails.origin
|
||||
)
|
||||
return {
|
||||
selectionType: 'completeSelection',
|
||||
selection: pathMapToSelections(
|
||||
@ -464,17 +666,31 @@ export const ModelingMachineProvider = ({
|
||||
selectionRanges,
|
||||
pathToNodeMap
|
||||
),
|
||||
updatedPathToNode,
|
||||
}
|
||||
},
|
||||
'Get ABS X info': async ({
|
||||
selectionRanges,
|
||||
sketchDetails,
|
||||
}): Promise<SetSelections> => {
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
await applyConstraintAbsDistance({
|
||||
constraint: 'xAbs',
|
||||
selectionRanges,
|
||||
})
|
||||
await kclManager.updateAst(modifiedAst, true)
|
||||
const _modifiedAst = parse(recast(modifiedAst))
|
||||
if (!sketchDetails) throw new Error('No sketch details')
|
||||
const updatedPathToNode = updatePathToNodeFromMap(
|
||||
sketchDetails.sketchPathToNode,
|
||||
pathToNodeMap
|
||||
)
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
updatedPathToNode,
|
||||
_modifiedAst,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
sketchDetails.origin
|
||||
)
|
||||
return {
|
||||
selectionType: 'completeSelection',
|
||||
selection: pathMapToSelections(
|
||||
@ -482,17 +698,31 @@ export const ModelingMachineProvider = ({
|
||||
selectionRanges,
|
||||
pathToNodeMap
|
||||
),
|
||||
updatedPathToNode,
|
||||
}
|
||||
},
|
||||
'Get ABS Y info': async ({
|
||||
selectionRanges,
|
||||
sketchDetails,
|
||||
}): Promise<SetSelections> => {
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
await applyConstraintAbsDistance({
|
||||
constraint: 'yAbs',
|
||||
selectionRanges,
|
||||
})
|
||||
await kclManager.updateAst(modifiedAst, true)
|
||||
const _modifiedAst = parse(recast(modifiedAst))
|
||||
if (!sketchDetails) throw new Error('No sketch details')
|
||||
const updatedPathToNode = updatePathToNodeFromMap(
|
||||
sketchDetails.sketchPathToNode,
|
||||
pathToNodeMap
|
||||
)
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
updatedPathToNode,
|
||||
_modifiedAst,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
sketchDetails.origin
|
||||
)
|
||||
return {
|
||||
selectionType: 'completeSelection',
|
||||
selection: pathMapToSelections(
|
||||
@ -500,8 +730,30 @@ export const ModelingMachineProvider = ({
|
||||
selectionRanges,
|
||||
pathToNodeMap
|
||||
),
|
||||
updatedPathToNode,
|
||||
}
|
||||
},
|
||||
'Get convert to variable info': async ({ sketchDetails }, { data }) => {
|
||||
if (!sketchDetails) return []
|
||||
const { variableName } = await getVarNameModal({
|
||||
valueName: data.variableName || 'var',
|
||||
})
|
||||
const { modifiedAst: _modifiedAst, pathToReplacedNode } =
|
||||
moveValueIntoNewVariablePath(
|
||||
parse(recast(kclManager.ast)),
|
||||
kclManager.programMemory,
|
||||
data.pathToNode,
|
||||
variableName
|
||||
)
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
pathToReplacedNode || [],
|
||||
parse(recast(_modifiedAst)),
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
sketchDetails.origin
|
||||
)
|
||||
return pathToReplacedNode || sketchDetails.sketchPathToNode
|
||||
},
|
||||
},
|
||||
devTools: true,
|
||||
}
|
||||
@ -511,6 +763,12 @@ export const ModelingMachineProvider = ({
|
||||
kclManager.registerExecuteCallback(() => {
|
||||
modelingSend({ type: 'Re-execute' })
|
||||
})
|
||||
|
||||
// Before this component unmounts, call the 'Cancel'
|
||||
// event to clean up any state in the modeling machine.
|
||||
return () => {
|
||||
modelingSend({ type: 'Cancel' })
|
||||
}
|
||||
}, [modelingSend])
|
||||
|
||||
// Give the state back to the editorManager.
|
||||
@ -526,6 +784,19 @@ export const ModelingMachineProvider = ({
|
||||
editorManager.selectionRanges = modelingState.context.selectionRanges
|
||||
}, [modelingState.context.selectionRanges])
|
||||
|
||||
useEffect(() => {
|
||||
const offlineCallback = () => {
|
||||
// If we are in sketch mode we need to exit it.
|
||||
// TODO: how do i check if we are in a sketch mode, I only want to call
|
||||
// this then.
|
||||
modelingSend({ type: 'Cancel' })
|
||||
}
|
||||
window.addEventListener('offline', offlineCallback)
|
||||
return () => {
|
||||
window.removeEventListener('offline', offlineCallback)
|
||||
}
|
||||
}, [modelingSend])
|
||||
|
||||
useStateMachineCommands({
|
||||
machineId: 'modeling',
|
||||
state: modelingState,
|
||||
@ -533,6 +804,10 @@ export const ModelingMachineProvider = ({
|
||||
actor: modelingActor,
|
||||
commandBarConfig: modelingMachineConfig,
|
||||
allCommandsRequireNetwork: true,
|
||||
// TODO for when sketch tools are in the toolbar: This was added when we used one "Cancel" event,
|
||||
// but we need to support "SketchCancel" and basically
|
||||
// make this function take the actor or state so it
|
||||
// can call the correct event.
|
||||
onCancel: () => modelingSend({ type: 'Cancel' }),
|
||||
})
|
||||
|
||||
|
@ -48,7 +48,7 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
|
||||
{convertToVarEnabled && (
|
||||
<Menu.Item>
|
||||
<button
|
||||
onClick={handleConvertToVarClick}
|
||||
onClick={() => handleConvertToVarClick()}
|
||||
className={styles.button}
|
||||
>
|
||||
<span>Convert to Variable</span>
|
||||
|