Compare commits

...

20 Commits

Author SHA1 Message Date
8eee3e1c58 Cut release v0.22.0 (#2582) 2024-06-03 21:53:39 -04:00
b02529cae0 perpendicular distance & remove constraint - constraint fixes (#2579)
* perpendicular distance constraint

* remove constraints fix
2024-06-03 12:40:59 +00:00
cf03021366 length constraint fix (#2578)
length constraint
2024-06-03 08:30:30 +00:00
f52d2d55f1 fix horz vert distance contraint (#2572)
fix hor vert distance contraint
2024-06-03 15:37:23 +10:00
59b1319e50 Update rectangle code gen to use profileStart to close shape (#2565)
* Update rectangle code gen to use profileStart to close shape

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Re-run CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-05-31 14:02:46 -04:00
b07bbda20b Remove FileTree from ProjectSiderbarMenu (#2544)
* Remove FileTree from ProjectSiderbarMenu

* Remove tests sidebar menu component tests that are no longer relevant
2024-05-31 06:42:20 -04:00
3c01924184 fix ABS XY constraint (#2560)
* fix source rangen for abs x y constraints

* fix abs bug

* add e2e test
2024-05-31 04:00:32 +00:00
bd16902f02 fix single selection angle constraint (#2555)
* fix single selection angle constraint

* fix angle for multi selections

* make test more robust for makos
2024-05-31 11:36:08 +10:00
8c3af1a72a Small refactor and renames (#2548)
Stuff that came up while working on multiple profiles per sketchgroup
2024-05-30 17:48:59 -05:00
33f5d7740d Enable Windows Tauri e2e tests in CI (#2554)
* Renable windows tauri e2e ci, no cache, manual debug build

* Cleanup and add workaround from https://github.com/actions/runner-images/issues/9538

* Added comment
2024-05-30 11:26:56 -07:00
b388f60648 hovering over axis should not remove overlays (#2553) 2024-05-30 11:25:20 +00:00
8f4380be74 Get existing tauri e2e tests to work on Windows (#2394)
* WIP: Get existing tauri e2e tests to work on Windows
Will fix #2393

* Enable windows stage (will fail)

* WIP msedge version sync

* Move setup edge before build

* Manual debug build (no action)

* Specify v119 for npm package

* Fixes on auth test

* Working test on win10

* Clean up

* Disable yarn cache to help debug the mismatch issue

* Revert "Disable yarn cache to help debug the mismatch issue"

This reverts commit e6abc7db42.

* Explicit webviewOptions and remove tauri driver fork

* Double \\ workaround for windows

* Clean up

* Clean up and readme

* Quick fix

* Lint

* Clippy fix

* Back to tauri-action and disable windows CI tests for early merge

* Back to 10sec delay

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Timer reset

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Trigger CI

* Back to 1 pw worker

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-05-30 06:23:56 -04:00
9ae8042a57 Update selections after constraint is applied [horizontal and vertical] (#2551)
* source range for vert horz constraints

* remove commented out code
2024-05-30 09:43:35 +00:00
4b676d47da Update selections after constraint is applied [equal length, parallel, snap to x or y] (#2543)
* migrate one constraint

* typo

* update snap to y, snap to x, horz align, vert align, equal length

* add some e2e tests

* add e2e test for snap to axis contsraits

* remove works for now
2024-05-30 13:28:29 +10:00
e6641e68f3 Add a promise-based toast when exporting (#2541)
* Add loading and success toasts to export engine command

* Move doExport out to a test utility, test visibility of loading spinner

* Add playwright test for export success toast

* Update Cargo.lock

* Remove loading assertion, it flashes too quickly for Playwright to pick up
2024-05-29 18:04:27 -04:00
450afb1605 increase playwright workers (#2518) 2024-05-28 05:32:02 +00:00
04433fecad Bump zip from 1.3.0 to 2.0.0 in /src/wasm-lib (#2536)
Bumps [zip](https://github.com/zip-rs/zip2) from 1.3.0 to 2.0.0.
- [Release notes](https://github.com/zip-rs/zip2/releases)
- [Changelog](https://github.com/zip-rs/zip2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zip-rs/zip2/compare/v1.3.0...v2.0.0)

---
updated-dependencies:
- dependency-name: zip
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-28 00:31:28 +00:00
6567e2ff92 Bump serde from 1.0.202 to 1.0.203 in /src/wasm-lib (#2537)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.202 to 1.0.203.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.202...v1.0.203)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-27 21:31:18 +00:00
91c32a7fe2 Bump proc-macro2 from 1.0.83 to 1.0.84 in /src/wasm-lib (#2538)
Bumps [proc-macro2](https://github.com/dtolnay/proc-macro2) from 1.0.83 to 1.0.84.
- [Release notes](https://github.com/dtolnay/proc-macro2/releases)
- [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.83...1.0.84)

---
updated-dependencies:
- dependency-name: proc-macro2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-27 19:38:53 +00:00
f735cdc22e fix and simulate engine disconnect when in sketch mode (#2524)
* fix and simulate engine disconnect when in sketch mode

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* Update e2e/playwright/test-utils.ts

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-05-24 23:11:49 +00:00
37 changed files with 1939 additions and 502 deletions

View File

@ -147,6 +147,14 @@ jobs:
cp artifact/src-tauri/tauri.conf.json src-tauri/tauri.conf.json cp artifact/src-tauri/tauri.conf.json src-tauri/tauri.conf.json
cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json
- name: 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 - name: Install ubuntu system dependencies
if: matrix.os == 'ubuntu-latest' if: matrix.os == 'ubuntu-latest'
run: | run: |
@ -364,6 +372,17 @@ jobs:
E2E_APPLICATION: "./src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/zoo-modeling-app" 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 }} 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: publish-apps-release:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -309,6 +309,25 @@ PS: for the debug panel, the following JSON is useful for snapping the camera
</details> </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 ## KCL
For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl). For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl).

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,10 @@
import { test, expect, Download } from '@playwright/test' import { test, expect } from '@playwright/test'
import { secrets } from './secrets' import { secrets } from './secrets'
import { getUtils } from './test-utils' import { Paths, doExport, getUtils } from './test-utils'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import fsp from 'fs/promises' import fsp from 'fs/promises'
import { spawn } from 'child_process' 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 JSZip from 'jszip'
import path from 'path' import path from 'path'
import { TEST_SETTINGS, TEST_SETTINGS_KEY } from './storageStates' import { TEST_SETTINGS, TEST_SETTINGS_KEY } from './storageStates'
@ -99,78 +99,6 @@ const part001 = startSketchOn('-XZ')
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
await u.clearAndCloseDebugPanel() 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'] = { const axisDirectionPair: Models['AxisDirectionPair_type'] = {
axis: 'z', axis: 'z',
direction: 'positive', direction: 'positive',
@ -186,84 +114,114 @@ const part001 = startSketchOn('-XZ')
// just note that only `type` and `storage` are used for selecting the drop downs is the app // 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 // the rest are only there to make typescript happy
exportLocations.push( exportLocations.push(
await doExport({ await doExport(
type: 'step', {
coords: sysType, type: 'step',
}) coords: sysType,
},
page
)
) )
exportLocations.push( exportLocations.push(
await doExport({ await doExport(
type: 'ply', {
coords: sysType, type: 'ply',
selection: { type: 'default_scene' }, coords: sysType,
storage: 'ascii', selection: { type: 'default_scene' },
units: 'in', storage: 'ascii',
}) units: 'in',
},
page
)
) )
exportLocations.push( exportLocations.push(
await doExport({ await doExport(
type: 'ply', {
storage: 'binary_little_endian', type: 'ply',
coords: sysType, storage: 'binary_little_endian',
selection: { type: 'default_scene' }, coords: sysType,
units: 'in', selection: { type: 'default_scene' },
}) units: 'in',
},
page
)
) )
exportLocations.push( exportLocations.push(
await doExport({ await doExport(
type: 'ply', {
storage: 'binary_big_endian', type: 'ply',
coords: sysType, storage: 'binary_big_endian',
selection: { type: 'default_scene' }, coords: sysType,
units: 'in', selection: { type: 'default_scene' },
}) units: 'in',
},
page
)
) )
exportLocations.push( exportLocations.push(
await doExport({ await doExport(
type: 'stl', {
storage: 'ascii', type: 'stl',
coords: sysType, storage: 'ascii',
units: 'in', coords: sysType,
selection: { type: 'default_scene' }, units: 'in',
}) selection: { type: 'default_scene' },
},
page
)
) )
exportLocations.push( exportLocations.push(
await doExport({ await doExport(
type: 'stl', {
storage: 'binary', type: 'stl',
coords: sysType, storage: 'binary',
units: 'in', coords: sysType,
selection: { type: 'default_scene' }, units: 'in',
}) selection: { type: 'default_scene' },
},
page
)
) )
exportLocations.push( exportLocations.push(
await doExport({ await doExport(
// obj seems to be a little flaky, times out tests sometimes {
type: 'obj', // obj seems to be a little flaky, times out tests sometimes
coords: sysType, type: 'obj',
units: 'in', coords: sysType,
}) units: 'in',
},
page
)
) )
exportLocations.push( exportLocations.push(
await doExport({ await doExport(
type: 'gltf', {
storage: 'embedded', type: 'gltf',
presentation: 'pretty', storage: 'embedded',
}) presentation: 'pretty',
},
page
)
) )
exportLocations.push( exportLocations.push(
await doExport({ await doExport(
type: 'gltf', {
storage: 'binary', type: 'gltf',
presentation: 'pretty', storage: 'binary',
}) presentation: 'pretty',
},
page
)
) )
exportLocations.push( exportLocations.push(
await doExport({ await doExport(
type: 'gltf', {
storage: 'standard', type: 'gltf',
presentation: 'pretty', storage: 'standard',
}) presentation: 'pretty',
},
page
)
) )
// close page to disconnect websocket since we can only have one open atm // close page to disconnect websocket since we can only have one open atm

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -1,9 +1,11 @@
import { test, expect, Page } from '@playwright/test' import { test, expect, Page, Download } from '@playwright/test'
import { EngineCommand } from '../../src/lang/std/engineConnection' import { EngineCommand } from '../../src/lang/std/engineConnection'
import fsp from 'fs/promises' import fsp from 'fs/promises'
import pixelMatch from 'pixelmatch' import pixelMatch from 'pixelmatch'
import { PNG } from 'pngjs' import { PNG } from 'pngjs'
import { Protocol } from 'playwright-core/types/protocol' import { Protocol } from 'playwright-core/types/protocol'
import type { Models } from '@kittycad/lib'
import { APP_NAME } from 'lib/constants'
async function waitForPageLoad(page: Page) { async function waitForPageLoad(page: Page) {
// wait for 'Loading stream...' spinner // wait for 'Loading stream...' spinner
@ -95,10 +97,10 @@ async function waitForCmdReceive(page: Page, commandType: string) {
} }
export async function getUtils(page: Page) { export async function getUtils(page: Page) {
// Chrome devtools protocol session only works in Chromium
const browserType = page.context().browser()?.browserType().name()
const cdpSession = const cdpSession =
process.platform === 'darwin' browserType !== 'chromium' ? null : await page.context().newCDPSession(page)
? null
: await page.context().newCDPSession(page)
return { return {
waitForAuthSkipAppStart: () => waitForPageLoad(page), waitForAuthSkipAppStart: () => waitForPageLoad(page),
@ -130,6 +132,19 @@ export async function getUtils(page: Page) {
}, },
waitForCmdReceive: (commandType: string) => waitForCmdReceive: (commandType: string) =>
waitForCmdReceive(page, commandType), 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: bbox.x + angleXOffset,
y: bbox.y - angleYOffset,
}
},
getBoundingBox: async (locator: string) => getBoundingBox: async (locator: string) =>
page page
.locator(locator) .locator(locator)
@ -277,3 +292,77 @@ 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,
}
}

View File

@ -1,11 +1,19 @@
import { browser, $, expect } from '@wdio/globals' import { browser, $, expect } from '@wdio/globals'
import fs from 'fs/promises' import fs from 'fs/promises'
import path from 'path'
import os from 'os'
const documentsDir = `${process.env.HOME}/Documents` const isWin32 = os.platform() === 'win32'
const userSettingsDir = `${process.env.HOME}/.config/dev.zoo.modeling-app` const documentsDir = path.join(os.homedir(), 'Documents')
const defaultProjectDir = `${documentsDir}/zoo-modeling-app-projects` const userSettingsDir = path.join(
const newProjectDir = `${documentsDir}/a-different-directory` os.homedir(),
const userCodeDir = '/tmp/kittycad_user_code' '.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> { async function click(element: WebdriverIO.Element): Promise<void> {
// Workaround for .click(), see https://github.com/tauri-apps/tauri/issues/6541 // 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) 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 () => { it('opens the auth page and signs in', async () => {
// Clean up filesystem from previous tests // Clean up filesystem from previous tests
await new Promise((resolve) => setTimeout(resolve, 100)) await new Promise((resolve) => setTimeout(resolve, 100))
@ -42,9 +50,7 @@ describe('ZMA (Tauri, Linux)', () => {
await new Promise((resolve) => setTimeout(resolve, 2000)) await new Promise((resolve) => setTimeout(resolve, 2000))
// Get from main.rs // Get from main.rs
const userCode = await ( const userCode = await (await fs.readFile(userCodeDir)).toString()
await fs.readFile('/tmp/kittycad_user_code')
).toString()
console.log(`Found user code ${userCode}`) console.log(`Found user code ${userCode}`)
// Device flow: verify // Device flow: verify
@ -92,7 +98,12 @@ describe('ZMA (Tauri, Linux)', () => {
* to be able to skip the folder selection dialog if data-testValue * to be able to skip the folder selection dialog if data-testValue
* has a value, allowing us to test the input otherwise works. * 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"]') const projectDirButton = await $('[data-testid="project-directory-button"]')
await click(projectDirButton) await click(projectDirButton)
await new Promise((resolve) => setTimeout(resolve, 500)) await new Promise((resolve) => setTimeout(resolve, 500))
@ -102,6 +113,15 @@ describe('ZMA (Tauri, Linux)', () => {
const nameInput = await $('[data-testid="projects-defaultProjectName"]') const nameInput = await $('[data-testid="projects-defaultProjectName"]')
expect(await nameInput.getValue()).toEqual('project-$nnn') 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"]') const closeButton = await $('[data-testid="settings-close-button"]')
await click(closeButton) await click(closeButton)
}) })
@ -120,9 +140,15 @@ describe('ZMA (Tauri, Linux)', () => {
it('opens the new file and expects a loading stream', async () => { it('opens the new file and expects a loading stream', async () => {
const projectLink = await $('[data-testid="project-link"]') const projectLink = await $('[data-testid="project-link"]')
await click(projectLink) await click(projectLink)
const errorText = await $('[data-testid="unexpected-error"]') if (isWin32) {
expect(await errorText.getText()).toContain('unexpected error') // TODO: actually do something to check that the stream is up
await browser.execute('window.location.href = "tauri://localhost/home"') await new Promise((resolve) => setTimeout(resolve, 5000))
} else {
const errorText = await $('[data-testid="unexpected-error"]')
expect(await errorText.getText()).toContain('unexpected error')
}
const base = isWin32 ? 'http://tauri.localhost' : 'tauri://localhost'
await browser.execute(`window.location.href = "${base}/home"`)
}) })
it('signs out', async () => { it('signs out', async () => {

View File

@ -1,6 +1,6 @@
{ {
"name": "untitled-app", "name": "untitled-app",
"version": "0.21.9", "version": "0.22.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.16.0", "@codemirror/autocomplete": "^6.16.0",

15
src-tauri/Cargo.lock generated
View File

@ -2602,7 +2602,7 @@ dependencies = [
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"winnow 0.5.40", "winnow 0.5.40",
"zip 1.3.0", "zip 2.1.1",
] ]
[[package]] [[package]]
@ -4500,9 +4500,9 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.202" version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
@ -4518,9 +4518,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.202" version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -7106,15 +7106,16 @@ dependencies = [
[[package]] [[package]]
name = "zip" name = "zip"
version = "1.3.0" version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1f4a27345eb6f7aa7bd015ba7eb4175fa4e1b462a29874b779e0bbcf96c6ac7" checksum = "1dd56a4d5921bc2f99947ac5b3abe5f510b1be7376fdc5e9fce4a23c6a93e87c"
dependencies = [ dependencies = [
"arbitrary", "arbitrary",
"crc32fast", "crc32fast",
"crossbeam-utils", "crossbeam-utils",
"displaydoc", "displaydoc",
"indexmap 2.2.6", "indexmap 2.2.6",
"memchr",
"thiserror", "thiserror",
] ]

View File

@ -267,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(); let e2e_tauri_enabled = env::var("E2E_TAURI_ENABLED").is_ok();
if e2e_tauri_enabled { if e2e_tauri_enabled {
log::warn!("E2E_TAURI_ENABLED is set, won't open {} externally", auth_uri.secret()); 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 .await
.map_err(|e| InvokeError::from_anyhow(e.into()))?; .map_err(|e| InvokeError::from_anyhow(e.into()))?;
} else { } else {

View File

@ -74,5 +74,5 @@
} }
}, },
"productName": "Zoo Modeling App", "productName": "Zoo Modeling App",
"version": "0.21.9" "version": "0.22.0"
} }

View File

@ -531,8 +531,7 @@ const ConstraintSymbol = ({
varNameMap[_type as LineInputsType]?.implicitConstraintDesc varNameMap[_type as LineInputsType]?.implicitConstraintDesc
const node = useMemo( const node = useMemo(
() => () => getNodeFromPath<Value>(kclManager.ast, pathToNode).node,
getNodeFromPath<Value>(parse(recast(kclManager.ast)), pathToNode).node,
[kclManager.ast, pathToNode] [kclManager.ast, pathToNode]
) )
const range: SourceRange = node ? [node.start, node.end] : [0, 0] const range: SourceRange = node ? [node.start, node.end] : [0, 0]

View File

@ -39,6 +39,7 @@ export function ActionButtonDropdown({
onClick={item.onClick} 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" 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} disabled={item.disabled}
data-testid={item.label}
> >
<span className="capitalize">{item.label}</span> <span className="capitalize">{item.label}</span>
{item.shortcut && ( {item.shortcut && (

View File

@ -214,13 +214,17 @@ export const CreateNewVariable = ({
}) => { }) => {
return ( return (
<> <>
<label htmlFor="create-new-variable" className="block mt-3 font-mono"> <label
htmlFor="create-new-variable"
className="block mt-3 font-mono text-chalkboard-90"
>
Create new variable Create new variable
</label> </label>
<div className="mt-1 flex gap-2 items-center"> <div className="mt-1 flex gap-2 items-center">
{showCheckbox && ( {showCheckbox && (
<input <input
type="checkbox" type="checkbox"
data-testid="create-new-variable-checkbox"
checked={shouldCreateVariable} checked={shouldCreateVariable}
onChange={(e) => { onChange={(e) => {
setShouldCreateVariable(e.target.checked) setShouldCreateVariable(e.target.checked)

View File

@ -11,7 +11,10 @@ import {
import { SetSelections, modelingMachine } from 'machines/modelingMachine' import { SetSelections, modelingMachine } from 'machines/modelingMachine'
import { useSetupEngineManager } from 'hooks/useSetupEngineManager' import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { isCursorInSketchCommandRange } from 'lang/util' import {
isCursorInSketchCommandRange,
updatePathToNodeFromMap,
} from 'lang/util'
import { import {
kclManager, kclManager,
sceneInfra, sceneInfra,
@ -150,7 +153,7 @@ export const ModelingMachineProvider = ({
]) ])
const pathToNode = parent?.userData?.pathToNode const pathToNode = parent?.userData?.pathToNode
const pathToNodeString = JSON.stringify(pathToNode) const pathToNodeString = JSON.stringify(pathToNode)
if (!parent || !pathToNode) return {} if (!parent || !pathToNode) return segmentHoverMap
if (segmentHoverMap[pathToNodeString] !== undefined) if (segmentHoverMap[pathToNodeString] !== undefined)
clearTimeout(segmentHoverMap[JSON.stringify(pathToNode)]) clearTimeout(segmentHoverMap[JSON.stringify(pathToNode)])
return { return {
@ -218,9 +221,8 @@ export const ModelingMachineProvider = ({
} }
: {} : {}
), ),
'Set selection': assign(({ selectionRanges }, event) => { 'Set selection': assign(({ selectionRanges, sketchDetails }, 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 as SetSelections // this was needed for ts after adding 'Set selection' action to on done modal events
const setSelections = event.data
if (!editorManager.editorView) return {} if (!editorManager.editorView) return {}
const dispatchSelection = (selection?: EditorSelection) => { const dispatchSelection = (selection?: EditorSelection) => {
if (!selection) return // TODO less of hack for the below please if (!selection) return // TODO less of hack for the below please
@ -307,11 +309,29 @@ export const ModelingMachineProvider = ({
selectionRanges: selections, 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 {} return {}
}), }),
'Engine export': (_, event) => { 'Engine export': async (_, event) => {
if (event.type !== 'Export' || TEST) return if (event.type !== 'Export' || TEST) return
console.log('exporting', event.data)
const format = { const format = {
...event.data, ...event.data,
} as Partial<Models['OutputFormat_type']> } as Partial<Models['OutputFormat_type']>
@ -355,9 +375,16 @@ export const ModelingMachineProvider = ({
format.selection = { type: 'default_scene' } format.selection = { type: 'default_scene' }
} }
exportFromEngine({ toast.promise(
format: format as Models['OutputFormat_type'], exportFromEngine({
}).catch((e) => toast.error('Error while exporting', e)) // TODO I think we need to throw the error from engineCommandManager format: format as Models['OutputFormat_type'],
}),
{
loading: 'Exporting...',
success: 'Exported successfully',
error: 'Error while exporting',
}
)
}, },
}, },
guards: { guards: {
@ -481,13 +508,26 @@ export const ModelingMachineProvider = ({
}, },
'Get horizontal info': async ({ 'Get horizontal info': async ({
selectionRanges, selectionRanges,
sketchDetails,
}): Promise<SetSelections> => { }): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } = const { modifiedAst, pathToNodeMap } =
await applyConstraintHorzVertDistance({ await applyConstraintHorzVertDistance({
constraint: 'setHorzDistance', constraint: 'setHorzDistance',
selectionRanges, 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 { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection: pathMapToSelections( selection: pathMapToSelections(
@ -495,17 +535,31 @@ export const ModelingMachineProvider = ({
selectionRanges, selectionRanges,
pathToNodeMap pathToNodeMap
), ),
updatedPathToNode,
} }
}, },
'Get vertical info': async ({ 'Get vertical info': async ({
selectionRanges, selectionRanges,
sketchDetails,
}): Promise<SetSelections> => { }): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } = const { modifiedAst, pathToNodeMap } =
await applyConstraintHorzVertDistance({ await applyConstraintHorzVertDistance({
constraint: 'setVertDistance', constraint: 'setVertDistance',
selectionRanges, 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 { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection: pathMapToSelections( selection: pathMapToSelections(
@ -513,10 +567,12 @@ export const ModelingMachineProvider = ({
selectionRanges, selectionRanges,
pathToNodeMap pathToNodeMap
), ),
updatedPathToNode,
} }
}, },
'Get angle info': async ({ 'Get angle info': async ({
selectionRanges, selectionRanges,
sketchDetails,
}): Promise<SetSelections> => { }): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } = await (angleBetweenInfo({ const { modifiedAst, pathToNodeMap } = await (angleBetweenInfo({
selectionRanges, selectionRanges,
@ -528,22 +584,48 @@ export const ModelingMachineProvider = ({
selectionRanges, selectionRanges,
angleOrLength: 'setAngle', 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 { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection: pathMapToSelections( selection: pathMapToSelections(
kclManager.ast, _modifiedAst,
selectionRanges, selectionRanges,
pathToNodeMap pathToNodeMap
), ),
updatedPathToNode,
} }
}, },
'Get length info': async ({ 'Get length info': async ({
selectionRanges, selectionRanges,
sketchDetails,
}): Promise<SetSelections> => { }): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } = const { modifiedAst, pathToNodeMap } =
await applyConstraintAngleLength({ selectionRanges }) 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 { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection: pathMapToSelections( selection: pathMapToSelections(
@ -551,17 +633,31 @@ export const ModelingMachineProvider = ({
selectionRanges, selectionRanges,
pathToNodeMap pathToNodeMap
), ),
updatedPathToNode,
} }
}, },
'Get perpendicular distance info': async ({ 'Get perpendicular distance info': async ({
selectionRanges, selectionRanges,
sketchDetails,
}): Promise<SetSelections> => { }): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } = await applyConstraintIntersect( const { modifiedAst, pathToNodeMap } = await applyConstraintIntersect(
{ {
selectionRanges, 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 { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection: pathMapToSelections( selection: pathMapToSelections(
@ -569,17 +665,31 @@ export const ModelingMachineProvider = ({
selectionRanges, selectionRanges,
pathToNodeMap pathToNodeMap
), ),
updatedPathToNode,
} }
}, },
'Get ABS X info': async ({ 'Get ABS X info': async ({
selectionRanges, selectionRanges,
sketchDetails,
}): Promise<SetSelections> => { }): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } = const { modifiedAst, pathToNodeMap } =
await applyConstraintAbsDistance({ await applyConstraintAbsDistance({
constraint: 'xAbs', constraint: 'xAbs',
selectionRanges, 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 { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection: pathMapToSelections( selection: pathMapToSelections(
@ -587,17 +697,31 @@ export const ModelingMachineProvider = ({
selectionRanges, selectionRanges,
pathToNodeMap pathToNodeMap
), ),
updatedPathToNode,
} }
}, },
'Get ABS Y info': async ({ 'Get ABS Y info': async ({
selectionRanges, selectionRanges,
sketchDetails,
}): Promise<SetSelections> => { }): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } = const { modifiedAst, pathToNodeMap } =
await applyConstraintAbsDistance({ await applyConstraintAbsDistance({
constraint: 'yAbs', constraint: 'yAbs',
selectionRanges, 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 { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection: pathMapToSelections( selection: pathMapToSelections(
@ -605,6 +729,7 @@ export const ModelingMachineProvider = ({
selectionRanges, selectionRanges,
pathToNodeMap pathToNodeMap
), ),
updatedPathToNode,
} }
}, },
'Get convert to variable info': async ({ sketchDetails }, { data }) => { 'Get convert to variable info': async ({ sketchDetails }, { data }) => {
@ -658,6 +783,19 @@ export const ModelingMachineProvider = ({
editorManager.selectionRanges = modelingState.context.selectionRanges editorManager.selectionRanges = modelingState.context.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({ useStateMachineCommands({
machineId: 'modeling', machineId: 'modeling',
state: modelingState, state: modelingState,

View File

@ -31,43 +31,6 @@ const projectWellFormed = {
} satisfies Project } satisfies Project
describe('ProjectSidebarMenu tests', () => { describe('ProjectSidebarMenu tests', () => {
test('Renders the project name', () => {
render(
<BrowserRouter>
<CommandBarProvider>
<SettingsAuthProviderJest>
<ProjectSidebarMenu project={projectWellFormed} enableMenu={true} />
</SettingsAuthProviderJest>
</CommandBarProvider>
</BrowserRouter>
)
fireEvent.click(screen.getByTestId('project-sidebar-toggle'))
expect(screen.getByTestId('projectName')).toHaveTextContent(
projectWellFormed.name
)
expect(screen.getByTestId('createdAt')).toHaveTextContent(
`Created ${now.toLocaleDateString()}`
)
})
test('Renders app name if given no project', () => {
render(
<BrowserRouter>
<CommandBarProvider>
<SettingsAuthProviderJest>
<ProjectSidebarMenu enableMenu={true} />
</SettingsAuthProviderJest>
</CommandBarProvider>
</BrowserRouter>
)
fireEvent.click(screen.getByTestId('project-sidebar-toggle'))
expect(screen.getByTestId('projectName')).toHaveTextContent(APP_NAME)
})
test('Disables popover menu by default', () => { test('Disables popover menu by default', () => {
render( render(
<BrowserRouter> <BrowserRouter>

View File

@ -138,41 +138,7 @@ function ProjectMenuPopover({
> >
{({ close }) => ( {({ close }) => (
<> <>
<div className="flex items-center gap-4 px-4 py-3"> <div className="flex flex-col gap-2 p-4">
<div>
<p className="m-0 text-mono" data-testid="projectName">
{project?.name ? project.name : APP_NAME}
</p>
{project?.metadata && project.metadata.created && (
<p
className="m-0 text-xs text-chalkboard-80 dark:text-chalkboard-40"
data-testid="createdAt"
>
Created{' '}
{new Date(project.metadata.created).toLocaleDateString()}
</p>
)}
</div>
</div>
{isTauri() ? (
<FileTree
file={file}
className="overflow-hidden border-0 border-y border-chalkboard-30 dark:border-chalkboard-80"
onNavigateToFile={close}
/>
) : (
<div className="flex-1 p-4 text-sm overflow-hidden">
<p>
In the browser version of Modeling App you can only have one
part, and the code is stored in your browser's storage.
</p>
<p className="my-6">
Please save any code you want to keep more permanently, as
your browser's storage is not guaranteed to be permanent.
</p>
</div>
)}
<div className="flex flex-col gap-2 p-4 dark:bg-chalkboard-90">
<ActionButton <ActionButton
Element="button" Element="button"
iconStart={{ icon: 'exportFile', className: 'p-1' }} iconStart={{ icon: 'exportFile', className: 'p-1' }}

View File

@ -140,7 +140,11 @@ export async function applyConstraintIntersect({
value: valueUsedInTransform, value: valueUsedInTransform,
initialVariableName: 'offset', initialVariableName: 'offset',
}) })
if (segName === tagInfo?.tag && Number(value) === valueUsedInTransform) { if (
!variableName &&
segName === tagInfo?.tag &&
Number(value) === valueUsedInTransform
) {
return { return {
modifiedAst, modifiedAst,
pathToNodeMap, pathToNodeMap,
@ -169,6 +173,10 @@ export async function applyConstraintIntersect({
createVariableDeclaration(variableName, valueNode) createVariableDeclaration(variableName, valueNode)
) )
_modifiedAst.body = newBody _modifiedAst.body = newBody
Object.values(_pathToNodeMap).forEach((pathToNode) => {
const index = pathToNode.findIndex((a) => a[0] === 'body') + 1
pathToNode[index][0] = Number(pathToNode[index][0]) + 1
})
} }
return { return {
modifiedAst: _modifiedAst, modifiedAst: _modifiedAst,

View File

@ -120,6 +120,10 @@ export async function applyConstraintAbsDistance({
createVariableDeclaration(variableName, valueNode) createVariableDeclaration(variableName, valueNode)
) )
_modifiedAst.body = newBody _modifiedAst.body = newBody
Object.values(pathToNodeMap).forEach((pathToNode) => {
const index = pathToNode.findIndex((a) => a[0] === 'body') + 1
pathToNode[index][0] = Number(pathToNode[index][0]) + 1
})
} }
return { modifiedAst: _modifiedAst, pathToNodeMap } return { modifiedAst: _modifiedAst, pathToNodeMap }
} }

View File

@ -98,7 +98,11 @@ export async function applyConstraintAngleBetween({
value: valueUsedInTransform, value: valueUsedInTransform,
initialVariableName: 'angle', initialVariableName: 'angle',
} as any) } as any)
if (segName === tagInfo?.tag && Number(value) === valueUsedInTransform) { if (
segName === tagInfo?.tag &&
Number(value) === valueUsedInTransform &&
!variableName
) {
return { return {
modifiedAst, modifiedAst,
pathToNodeMap, pathToNodeMap,
@ -128,6 +132,10 @@ export async function applyConstraintAngleBetween({
createVariableDeclaration(variableName, valueNode) createVariableDeclaration(variableName, valueNode)
) )
_modifiedAst.body = newBody _modifiedAst.body = newBody
Object.values(_pathToNodeMap).forEach((pathToNode) => {
const index = pathToNode.findIndex((a) => a[0] === 'body') + 1
pathToNode[index][0] = Number(pathToNode[index][0]) + 1
})
} }
return { return {
modifiedAst: _modifiedAst, modifiedAst: _modifiedAst,

View File

@ -106,7 +106,11 @@ export async function applyConstraintHorzVertDistance({
value: valueUsedInTransform, value: valueUsedInTransform,
initialVariableName: constraint === 'setHorzDistance' ? 'xDis' : 'yDis', initialVariableName: constraint === 'setHorzDistance' ? 'xDis' : 'yDis',
} as any) } as any)
if (segName === tagInfo?.tag && Number(value) === valueUsedInTransform) { if (
!variableName &&
segName === tagInfo?.tag &&
Number(value) === valueUsedInTransform
) {
return { return {
modifiedAst, modifiedAst,
pathToNodeMap, pathToNodeMap,
@ -133,6 +137,10 @@ export async function applyConstraintHorzVertDistance({
createVariableDeclaration(variableName, valueNode) createVariableDeclaration(variableName, valueNode)
) )
_modifiedAst.body = newBody _modifiedAst.body = newBody
Object.values(pathToNodeMap).forEach((pathToNode) => {
const index = pathToNode.findIndex((a) => a[0] === 'body') + 1
pathToNode[index][0] = Number(pathToNode[index][0]) + 1
})
} }
return { return {
modifiedAst: _modifiedAst, modifiedAst: _modifiedAst,

View File

@ -138,13 +138,17 @@ export async function applyConstraintAngleLength({
createVariableDeclaration(variableName, valueNode) createVariableDeclaration(variableName, valueNode)
) )
_modifiedAst.body = newBody _modifiedAst.body = newBody
Object.values(pathToNodeMap).forEach((pathToNode) => {
const index = pathToNode.findIndex((a) => a[0] === 'body') + 1
pathToNode[index][0] = Number(pathToNode[index][0]) + 1
})
} }
return { return {
modifiedAst: _modifiedAst, modifiedAst: _modifiedAst,
pathToNodeMap, pathToNodeMap,
} }
} catch (e) { } catch (e) {
console.log('erorr', e) console.log('error', e)
throw e throw e
} }
} }

View File

@ -994,6 +994,10 @@ export class EngineCommandManager {
engineConnection?: EngineConnection engineConnection?: EngineConnection
defaultPlanes: DefaultPlanes | null = null defaultPlanes: DefaultPlanes | null = null
commandLogs: CommandLog[] = [] commandLogs: CommandLog[] = []
pendingExport?: {
resolve: (filename?: string) => void
reject: (reason: any) => void
}
_commandLogCallBack: (command: CommandLog[]) => void = () => {} _commandLogCallBack: (command: CommandLog[]) => void = () => {}
private resolveReady = () => {} private resolveReady = () => {}
/** Folks should realize that wait for ready does not get called _everytime_ /** Folks should realize that wait for ready does not get called _everytime_
@ -1150,7 +1154,9 @@ export class EngineCommandManager {
// because in all other cases we send JSON strings. But in the case of // because in all other cases we send JSON strings. But in the case of
// export we send a binary blob. // export we send a binary blob.
// Pass this to our export function. // Pass this to our export function.
void exportSave(event.data) exportSave(event.data).then(() => {
this.pendingExport?.resolve()
}, this.pendingExport?.reject)
} else { } else {
const message: Models['WebSocketResponse_type'] = JSON.parse( const message: Models['WebSocketResponse_type'] = JSON.parse(
event.data event.data
@ -1548,6 +1554,12 @@ export class EngineCommandManager {
this.outSequence++ this.outSequence++
this.engineConnection?.unreliableSend(command) this.engineConnection?.unreliableSend(command)
return Promise.resolve() return Promise.resolve()
} else if (cmd.type === 'export') {
const promise = new Promise((resolve, reject) => {
this.pendingExport = { resolve, reject }
})
this.engineConnection?.send(command)
return promise
} }
if ( if (
command.cmd.type === 'default_camera_look_at' || command.cmd.type === 'default_camera_look_at' ||

View File

@ -341,36 +341,29 @@ const setAbsDistanceCreateNode =
isXOrYLine = false, isXOrYLine = false,
index = xOrY === 'x' ? 0 : 1 index = xOrY === 'x' ? 0 : 1
): TransformInfo['createNode'] => ): TransformInfo['createNode'] =>
({ tag, forceValueUsedInTransform }) => { ({ tag, forceValueUsedInTransform }) =>
return (args, _, referencedSegment) => { (args) => {
const valueUsedInTransform = roundOff( const valueUsedInTransform = roundOff(getArgLiteralVal(args?.[index]), 2)
getArgLiteralVal(args?.[index]) - (referencedSegment?.to?.[index] || 0), const val =
2 (forceValueUsedInTransform as BinaryPart) ||
) createLiteral(valueUsedInTransform)
const val = if (isXOrYLine) {
(forceValueUsedInTransform as BinaryPart) ||
createLiteral(valueUsedInTransform)
if (isXOrYLine) {
return createCallWrapper(
xOrY === 'x' ? 'xLineTo' : 'yLineTo',
val,
tag,
valueUsedInTransform
)
}
return createCallWrapper( return createCallWrapper(
'lineTo', xOrY === 'x' ? 'xLineTo' : 'yLineTo',
!index ? [val, args[1]] : [args[0], val], val,
tag, tag,
valueUsedInTransform valueUsedInTransform
) )
} }
return createCallWrapper(
'lineTo',
!index ? [val, args[1]] : [args[0], val],
tag,
valueUsedInTransform
)
} }
const setAbsDistanceForAngleLineCreateNode = const setAbsDistanceForAngleLineCreateNode =
( (xOrY: 'x' | 'y'): TransformInfo['createNode'] =>
xOrY: 'x' | 'y',
index = xOrY === 'x' ? 0 : 1
): TransformInfo['createNode'] =>
({ tag, forceValueUsedInTransform, varValA }) => { ({ tag, forceValueUsedInTransform, varValA }) => {
return (args) => { return (args) => {
const valueUsedInTransform = roundOff(getArgLiteralVal(args?.[1]), 2) const valueUsedInTransform = roundOff(getArgLiteralVal(args?.[1]), 2)

View File

@ -26,6 +26,22 @@ export function pathMapToSelections(
return newSelections return newSelections
} }
export function updatePathToNodeFromMap(
oldPath: PathToNode,
pathToNodeMap: { [key: number]: PathToNode }
): PathToNode {
const updatedPathToNode = JSON.parse(JSON.stringify(oldPath))
let max = 0
Object.values(pathToNodeMap).forEach((path) => {
const index = Number(path[1][0])
if (index > max) {
max = index
}
})
updatedPathToNode[1][0] = max
return updatedPathToNode
}
export function isCursorInSketchCommandRange( export function isCursorInSketchCommandRange(
artifactMap: ArtifactMap, artifactMap: ArtifactMap,
selectionRanges: Selections selectionRanges: Selections

View File

@ -69,6 +69,13 @@ export const getRectangleCallExpressions = (
createPipeSubstitution(), createPipeSubstitution(),
createLiteral(tags[2]), createLiteral(tags[2]),
]), ]),
createCallExpressionStdLib('lineTo', [
createArrayExpression([
createCallExpressionStdLib('profileStartX', [createPipeSubstitution()]),
createCallExpressionStdLib('profileStartY', [createPipeSubstitution()]),
]),
createPipeSubstitution(),
]), // close the rectangle
createCallExpressionStdLib('close', [createPipeSubstitution()]), createCallExpressionStdLib('close', [createPipeSubstitution()]),
] ]

View File

@ -5,7 +5,7 @@ import {
kclManager, kclManager,
sceneEntitiesManager, sceneEntitiesManager,
} from 'lib/singletons' } from 'lib/singletons'
import { CallExpression, SourceRange, parse, recast } from 'lang/wasm' import { CallExpression, SourceRange, Value, parse, recast } from 'lang/wasm'
import { ModelingMachineEvent } from 'machines/modelingMachine' import { ModelingMachineEvent } from 'machines/modelingMachine'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { EditorSelection } from '@codemirror/state' import { EditorSelection } from '@codemirror/state'
@ -27,6 +27,7 @@ import {
} from 'clientSideScene/sceneEntities' } from 'clientSideScene/sceneEntities'
import { Mesh, Object3D, Object3DEventMap } from 'three' import { Mesh, Object3D, Object3DEventMap } from 'three'
import { AXIS_GROUP, X_AXIS } from 'clientSideScene/sceneInfra' import { AXIS_GROUP, X_AXIS } from 'clientSideScene/sceneInfra'
import { PathToNodeMap } from 'lang/std/sketchcombos'
export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b' export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b'
export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01' export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
@ -564,3 +565,22 @@ export function sendSelectEventToEngine(
.then((res) => res.data.data) .then((res) => res.data.data)
return result return result
} }
export function updateSelections(
pathToNodeMap: PathToNodeMap,
prevSelectionRanges: Selections,
ast: Program
): Selections {
return {
...prevSelectionRanges,
codeBasedSelections: Object.entries(pathToNodeMap).map(
([index, pathToNode]): Selection => {
const node = getNodeFromPath<Value>(ast, pathToNode).node
return {
range: [node.start, node.end],
type: prevSelectionRanges.codeBasedSelections[Number(index)]?.type,
}
}
),
}
}

View File

@ -127,3 +127,7 @@ export function isReducedMotion(): boolean {
window.matchMedia('(prefers-reduced-motion)').matches window.matchMedia('(prefers-reduced-motion)').matches
) )
} }
export function XOR(bool1: boolean, bool2: boolean): boolean {
return (bool1 || bool2) && !(bool1 && bool2)
}

File diff suppressed because one or more lines are too long

View File

@ -2861,9 +2861,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.83" version = "1.0.84"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -3539,9 +3539,9 @@ checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.202" version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
@ -3557,9 +3557,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.202" version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -5311,9 +5311,9 @@ dependencies = [
[[package]] [[package]]
name = "zip" name = "zip"
version = "1.3.0" version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1f4a27345eb6f7aa7bd015ba7eb4175fa4e1b462a29874b779e0bbcf96c6ac7" checksum = "fccb210625924ecbbe92f9bb497d04b167b64fe5540cec75f10b16e0c51ee92b"
dependencies = [ dependencies = [
"arbitrary", "arbitrary",
"crc32fast", "crc32fast",

View File

@ -18,7 +18,7 @@ once_cell = "1.19.0"
proc-macro2 = "1" proc-macro2 = "1"
quote = "1" quote = "1"
regex = "1.10" regex = "1.10"
serde = { version = "1.0.202", features = ["derive"] } serde = { version = "1.0.203", features = ["derive"] }
serde_tokenstream = "0.2" serde_tokenstream = "0.2"
syn = { version = "2.0.66", features = ["full"] } syn = { version = "2.0.66", features = ["full"] }

View File

@ -33,7 +33,7 @@ parse-display = "0.9.0"
reqwest = { version = "0.11.26", default-features = false, features = ["stream", "rustls-tls"] } reqwest = { version = "0.11.26", default-features = false, features = ["stream", "rustls-tls"] }
ropey = "1.6.1" ropey = "1.6.1"
schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1"] } schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1"] }
serde = { version = "1.0.202", features = ["derive"] } serde = { version = "1.0.203", features = ["derive"] }
serde_json = "1.0.116" serde_json = "1.0.116"
sha2 = "0.10.8" sha2 = "0.10.8"
thiserror = "1.0.61" thiserror = "1.0.61"
@ -44,7 +44,7 @@ url = { version = "2.5.0", features = ["serde"] }
uuid = { version = "1.8.0", features = ["v4", "js", "serde"] } uuid = { version = "1.8.0", features = ["v4", "js", "serde"] }
validator = { version = "0.18.1", features = ["derive"] } validator = { version = "0.18.1", features = ["derive"] }
winnow = "0.5.40" winnow = "0.5.40"
zip = { version = "1.3.0", default-features = false } zip = { version = "2.0.0", default-features = false }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.69" } js-sys = { version = "0.3.69" }

View File

@ -504,51 +504,45 @@ impl SketchGroup {
} }
} }
pub fn get_coords_from_paths(&self) -> Result<Point2d, KclError> { /// Get the path most recently sketched.
if self.value.is_empty() { pub fn latest_path(&self) -> Option<&Path> {
return Ok(self.start.to.into()); self.value.last()
} }
let index = self.value.len() - 1; /// The "pen" is an imaginary pen drawing the path.
if let Some(path) = self.value.get(index) { /// This gets the current point the pen is hovering over, i.e. the point
let base = path.get_base(); /// where the last path segment ends, and the next path segment will begin.
Ok(base.to.into()) pub fn current_pen_position(&self) -> Result<Point2d, KclError> {
} else { let Some(path) = self.latest_path() else {
Ok(self.start.to.into()) return Ok(self.start.to.into());
} };
let base = path.get_base();
Ok(base.to.into())
} }
pub fn get_tangential_info_from_paths(&self) -> GetTangentialInfoFromPathsResult { pub fn get_tangential_info_from_paths(&self) -> GetTangentialInfoFromPathsResult {
if self.value.is_empty() { let Some(path) = self.latest_path() else {
return GetTangentialInfoFromPathsResult { return GetTangentialInfoFromPathsResult {
center_or_tangent_point: self.start.to, center_or_tangent_point: self.start.to,
is_center: false, is_center: false,
ccw: false, ccw: false,
}; };
} };
let index = self.value.len() - 1; match path {
if let Some(path) = self.value.get(index) { Path::TangentialArcTo { center, ccw, .. } => GetTangentialInfoFromPathsResult {
match path { center_or_tangent_point: *center,
Path::TangentialArcTo { center, ccw, .. } => GetTangentialInfoFromPathsResult { is_center: true,
center_or_tangent_point: *center, ccw: *ccw,
is_center: true, },
ccw: *ccw, _ => {
}, let base = path.get_base();
_ => { GetTangentialInfoFromPathsResult {
let base = path.get_base(); center_or_tangent_point: base.from,
GetTangentialInfoFromPathsResult { is_center: false,
center_or_tangent_point: base.from, ccw: false,
is_center: false,
ccw: false,
}
} }
} }
} else {
GetTangentialInfoFromPathsResult {
center_or_tangent_point: self.start.to,
is_center: false,
ccw: false,
}
} }
} }
} }

View File

@ -53,7 +53,7 @@ async fn inner_line_to(
tag: Option<String>, tag: Option<String>,
args: Args, args: Args,
) -> Result<Box<SketchGroup>, KclError> { ) -> Result<Box<SketchGroup>, KclError> {
let from = sketch_group.get_coords_from_paths()?; let from = sketch_group.current_pen_position()?;
let id = uuid::Uuid::new_v4(); let id = uuid::Uuid::new_v4();
args.send_modeling_cmd( args.send_modeling_cmd(
@ -128,7 +128,7 @@ async fn inner_x_line_to(
tag: Option<String>, tag: Option<String>,
args: Args, args: Args,
) -> Result<Box<SketchGroup>, KclError> { ) -> Result<Box<SketchGroup>, KclError> {
let from = sketch_group.get_coords_from_paths()?; let from = sketch_group.current_pen_position()?;
let new_sketch_group = inner_line_to([to, from.y], sketch_group, tag, args).await?; let new_sketch_group = inner_line_to([to, from.y], sketch_group, tag, args).await?;
@ -166,7 +166,7 @@ async fn inner_y_line_to(
tag: Option<String>, tag: Option<String>,
args: Args, args: Args,
) -> Result<Box<SketchGroup>, KclError> { ) -> Result<Box<SketchGroup>, KclError> {
let from = sketch_group.get_coords_from_paths()?; let from = sketch_group.current_pen_position()?;
let new_sketch_group = inner_line_to([from.x, to], sketch_group, tag, args).await?; let new_sketch_group = inner_line_to([from.x, to], sketch_group, tag, args).await?;
Ok(new_sketch_group) Ok(new_sketch_group)
@ -213,7 +213,7 @@ async fn inner_line(
tag: Option<String>, tag: Option<String>,
args: Args, args: Args,
) -> Result<Box<SketchGroup>, KclError> { ) -> Result<Box<SketchGroup>, KclError> {
let from = sketch_group.get_coords_from_paths()?; let from = sketch_group.current_pen_position()?;
let to = [from.x + delta[0], from.y + delta[1]]; let to = [from.x + delta[0], from.y + delta[1]];
let id = uuid::Uuid::new_v4(); let id = uuid::Uuid::new_v4();
@ -381,7 +381,7 @@ async fn inner_angled_line(
tag: Option<String>, tag: Option<String>,
args: Args, args: Args,
) -> Result<Box<SketchGroup>, KclError> { ) -> Result<Box<SketchGroup>, KclError> {
let from = sketch_group.get_coords_from_paths()?; let from = sketch_group.current_pen_position()?;
let (angle, length) = match data { let (angle, length) = match data {
AngledLineData::AngleAndLengthNamed { angle, length } => (angle, length), AngledLineData::AngleAndLengthNamed { angle, length } => (angle, length),
AngledLineData::AngleAndLengthPair(pair) => (pair[0], pair[1]), AngledLineData::AngleAndLengthPair(pair) => (pair[0], pair[1]),
@ -514,7 +514,7 @@ async fn inner_angled_line_to_x(
tag: Option<String>, tag: Option<String>,
args: Args, args: Args,
) -> Result<Box<SketchGroup>, KclError> { ) -> Result<Box<SketchGroup>, KclError> {
let from = sketch_group.get_coords_from_paths()?; let from = sketch_group.current_pen_position()?;
let AngledLineToData { angle, to: x_to } = data; let AngledLineToData { angle, to: x_to } = data;
let x_component = x_to - from.x; let x_component = x_to - from.x;
@ -600,7 +600,7 @@ async fn inner_angled_line_to_y(
tag: Option<String>, tag: Option<String>,
args: Args, args: Args,
) -> Result<Box<SketchGroup>, KclError> { ) -> Result<Box<SketchGroup>, KclError> {
let from = sketch_group.get_coords_from_paths()?; let from = sketch_group.current_pen_position()?;
let AngledLineToData { angle, to: y_to } = data; let AngledLineToData { angle, to: y_to } = data;
let y_component = y_to - from.y; let y_component = y_to - from.y;
@ -672,7 +672,7 @@ async fn inner_angled_line_that_intersects(
})? })?
.get_base(); .get_base();
let from = sketch_group.get_coords_from_paths()?; let from = sketch_group.current_pen_position()?;
let to = intersection_with_parallel_line( let to = intersection_with_parallel_line(
&[intersect_path.from.into(), intersect_path.to.into()], &[intersect_path.from.into(), intersect_path.to.into()],
data.offset.unwrap_or_default(), data.offset.unwrap_or_default(),
@ -1355,7 +1355,7 @@ pub(crate) async fn inner_close(
tag: Option<String>, tag: Option<String>,
args: Args, args: Args,
) -> Result<Box<SketchGroup>, KclError> { ) -> Result<Box<SketchGroup>, KclError> {
let from = sketch_group.get_coords_from_paths()?; let from = sketch_group.current_pen_position()?;
let to: Point2d = sketch_group.start.from.into(); let to: Point2d = sketch_group.start.from.into();
let id = uuid::Uuid::new_v4(); let id = uuid::Uuid::new_v4();
@ -1449,7 +1449,7 @@ pub(crate) async fn inner_arc(
tag: Option<String>, tag: Option<String>,
args: Args, args: Args,
) -> Result<Box<SketchGroup>, KclError> { ) -> Result<Box<SketchGroup>, KclError> {
let from: Point2d = sketch_group.get_coords_from_paths()?; let from: Point2d = sketch_group.current_pen_position()?;
let (center, angle_start, angle_end, radius, end) = match &data { let (center, angle_start, angle_end, radius, end) = match &data {
ArcData::AnglesAndRadius { ArcData::AnglesAndRadius {
@ -1555,7 +1555,7 @@ async fn inner_tangential_arc(
tag: Option<String>, tag: Option<String>,
args: Args, args: Args,
) -> Result<Box<SketchGroup>, KclError> { ) -> Result<Box<SketchGroup>, KclError> {
let from: Point2d = sketch_group.get_coords_from_paths()?; let from: Point2d = sketch_group.current_pen_position()?;
let id = uuid::Uuid::new_v4(); let id = uuid::Uuid::new_v4();
@ -1676,7 +1676,7 @@ async fn inner_tangential_arc_to(
tag: Option<String>, tag: Option<String>,
args: Args, args: Args,
) -> Result<Box<SketchGroup>, KclError> { ) -> Result<Box<SketchGroup>, KclError> {
let from: Point2d = sketch_group.get_coords_from_paths()?; let from: Point2d = sketch_group.current_pen_position()?;
let tangent_info = sketch_group.get_tangential_info_from_paths(); let tangent_info = sketch_group.get_tangential_info_from_paths();
let tan_previous_point = if tangent_info.is_center { let tan_previous_point = if tangent_info.is_center {
get_tangent_point_from_previous_arc(tangent_info.center_or_tangent_point, tangent_info.ccw, from.into()) get_tangent_point_from_previous_arc(tangent_info.center_or_tangent_point, tangent_info.ccw, from.into())
@ -1762,7 +1762,7 @@ async fn inner_bezier_curve(
tag: Option<String>, tag: Option<String>,
args: Args, args: Args,
) -> Result<Box<SketchGroup>, KclError> { ) -> Result<Box<SketchGroup>, KclError> {
let from = sketch_group.get_coords_from_paths()?; let from = sketch_group.current_pen_position()?;
let relative = true; let relative = true;
let delta = data.to; let delta = data.to;

View File

@ -8,6 +8,7 @@ const application =
process.env.E2E_APPLICATION || `./src-tauri/target/release/zoo-modeling-app` process.env.E2E_APPLICATION || `./src-tauri/target/release/zoo-modeling-app`
export const config = { export const config = {
hostname: '127.0.0.1',
port: 4444, port: 4444,
specs: ['./e2e/tauri/specs/**/*.ts'], specs: ['./e2e/tauri/specs/**/*.ts'],
maxInstances: 1, maxInstances: 1,
@ -16,6 +17,7 @@ export const config = {
maxInstances: 1, maxInstances: 1,
'tauri:options': { 'tauri:options': {
application, application,
webviewOptions: {}, // Windows only
}, },
}, },
], ],