From e6641e68f3c50e3b811cc2f70ed7df2a86f830ed Mon Sep 17 00:00:00 2001 From: Frank Noirot Date: Wed, 29 May 2024 18:04:27 -0400 Subject: [PATCH 1/2] 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 --- e2e/playwright/flow-tests.spec.ts | 74 ++++++- e2e/playwright/snapshot-tests.spec.ts | 228 +++++++++------------ e2e/playwright/test-utils.ts | 78 ++++++- src-tauri/Cargo.lock | 15 +- src/components/ModelingMachineProvider.tsx | 16 +- src/lang/std/engineConnection.ts | 14 +- 6 files changed, 276 insertions(+), 149 deletions(-) diff --git a/e2e/playwright/flow-tests.spec.ts b/e2e/playwright/flow-tests.spec.ts index 260d7b770..fd558ddf1 100644 --- a/e2e/playwright/flow-tests.spec.ts +++ b/e2e/playwright/flow-tests.spec.ts @@ -1,5 +1,5 @@ import { test, expect, Page } from '@playwright/test' -import { makeTemplate, getUtils } from './test-utils' +import { makeTemplate, getUtils, doExport } from './test-utils' import waitOn from 'wait-on' import { roundOff, uuidv4 } from 'lib/utils' import { SaveSettingsPayload } from 'lib/settings/settingsTypes' @@ -3811,3 +3811,75 @@ test('Engine disconnect & reconnect in sketch mode', async ({ page }) => { page.getByRole('button', { name: 'Exit Sketch' }) ).not.toBeVisible() }) + +test('Successful export shows a success toast', async ({ page }) => { + // FYI this test doesn't work with only engine running locally + // And you will need to have the KittyCAD CLI installed + const u = await getUtils(page) + await page.addInitScript(async () => { + ;(window as any).playwrightSkipFilePicker = true + localStorage.setItem( + 'persistCode', + `const topAng = 25 +const bottomAng = 35 +const baseLen = 3.5 +const baseHeight = 1 +const totalHeightHalf = 2 +const armThick = 0.5 +const totalLen = 9.5 +const part001 = startSketchOn('-XZ') + |> startProfileAt([0, 0], %) + |> yLine(baseHeight, %) + |> xLine(baseLen, %) + |> angledLineToY({ + angle: topAng, + to: totalHeightHalf, + }, %, 'seg04') + |> xLineTo(totalLen, %, 'seg03') + |> yLine(-armThick, %, 'seg01') + |> angledLineThatIntersects({ + angle: HALF_TURN, + offset: -armThick, + intersectTag: 'seg04' + }, %) + |> angledLineToY([segAng('seg04', %) + 180, ZERO], %) + |> angledLineToY({ + angle: -bottomAng, + to: -totalHeightHalf - armThick, + }, %, 'seg02') + |> xLineTo(segEndX('seg03', %) + 0, %) + |> yLine(-segLen('seg01', %), %) + |> angledLineThatIntersects({ + angle: HALF_TURN, + offset: -armThick, + intersectTag: 'seg02' + }, %) + |> angledLineToY([segAng('seg02', %) + 180, -baseHeight], %) + |> xLineTo(ZERO, %) + |> close(%) + |> extrude(4, %)` + ) + }) + await page.setViewportSize({ width: 1200, height: 500 }) + await page.goto('/') + await u.waitForAuthSkipAppStart() + await u.openDebugPanel() + await u.expectCmdLog('[data-message-type="execution-done"]') + await u.waitForCmdReceive('extrude') + await page.waitForTimeout(1000) + await u.clearAndCloseDebugPanel() + + await doExport( + { + type: 'gltf', + storage: 'embedded', + presentation: 'pretty', + }, + page + ) + + // This is the main thing we're testing, + // We test the export functionality across all + // file types in snapshot-tests.spec.ts + await expect(page.getByText('Exported successfully')).toBeVisible() +}) diff --git a/e2e/playwright/snapshot-tests.spec.ts b/e2e/playwright/snapshot-tests.spec.ts index 949e35a25..8f32fb688 100644 --- a/e2e/playwright/snapshot-tests.spec.ts +++ b/e2e/playwright/snapshot-tests.spec.ts @@ -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' @@ -99,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 => { - 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((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', @@ -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 // the rest are only there to make typescript happy exportLocations.push( - await doExport({ - type: 'step', - coords: sysType, - }) + await doExport( + { + type: 'step', + coords: sysType, + }, + page + ) ) exportLocations.push( - await doExport({ - type: 'ply', - coords: sysType, - selection: { type: 'default_scene' }, - storage: 'ascii', - units: 'in', - }) + await doExport( + { + type: 'ply', + coords: sysType, + selection: { type: 'default_scene' }, + storage: 'ascii', + units: 'in', + }, + page + ) ) exportLocations.push( - await doExport({ - type: 'ply', - storage: 'binary_little_endian', - coords: sysType, - selection: { type: 'default_scene' }, - units: 'in', - }) + await doExport( + { + type: 'ply', + storage: 'binary_little_endian', + coords: sysType, + selection: { type: 'default_scene' }, + units: 'in', + }, + page + ) ) exportLocations.push( - await doExport({ - type: 'ply', - storage: 'binary_big_endian', - coords: sysType, - selection: { type: 'default_scene' }, - units: 'in', - }) + await doExport( + { + type: 'ply', + storage: 'binary_big_endian', + coords: sysType, + selection: { type: 'default_scene' }, + units: 'in', + }, + page + ) ) exportLocations.push( - await doExport({ - type: 'stl', - storage: 'ascii', - coords: sysType, - units: 'in', - selection: { type: 'default_scene' }, - }) + await doExport( + { + type: 'stl', + storage: 'ascii', + coords: sysType, + units: 'in', + selection: { type: 'default_scene' }, + }, + page + ) ) exportLocations.push( - await doExport({ - type: 'stl', - storage: 'binary', - coords: sysType, - units: 'in', - selection: { type: 'default_scene' }, - }) + await doExport( + { + type: 'stl', + storage: 'binary', + coords: sysType, + units: 'in', + selection: { type: 'default_scene' }, + }, + page + ) ) exportLocations.push( - await doExport({ - // obj seems to be a little flaky, times out tests sometimes - type: 'obj', - coords: sysType, - units: 'in', - }) + await doExport( + { + // obj seems to be a little flaky, times out tests sometimes + type: 'obj', + coords: sysType, + units: 'in', + }, + page + ) ) exportLocations.push( - await doExport({ - type: 'gltf', - storage: 'embedded', - presentation: 'pretty', - }) + await doExport( + { + type: 'gltf', + storage: 'embedded', + presentation: 'pretty', + }, + page + ) ) exportLocations.push( - await doExport({ - type: 'gltf', - storage: 'binary', - presentation: 'pretty', - }) + await doExport( + { + type: 'gltf', + storage: 'binary', + presentation: 'pretty', + }, + page + ) ) exportLocations.push( - await doExport({ - type: 'gltf', - storage: 'standard', - presentation: 'pretty', - }) + await doExport( + { + type: 'gltf', + storage: 'standard', + presentation: 'pretty', + }, + page + ) ) // close page to disconnect websocket since we can only have one open atm diff --git a/e2e/playwright/test-utils.ts b/e2e/playwright/test-utils.ts index fde96423a..578502426 100644 --- a/e2e/playwright/test-utils.ts +++ b/e2e/playwright/test-utils.ts @@ -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 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 @@ -277,3 +279,77 @@ export const makeTemplate: ( ), } } + +export interface Paths { + modelPath: string + imagePath: string + outputType: string +} + +export const doExport = async ( + output: Models['OutputFormat_type'], + page: Page +): Promise => { + 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((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, + } +} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ae240a9c5..d85261ff0 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2602,7 +2602,7 @@ dependencies = [ "wasm-bindgen-futures", "web-sys", "winnow 0.5.40", - "zip 1.3.0", + "zip 2.1.1", ] [[package]] @@ -4500,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", ] @@ -4518,9 +4518,9 @@ 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", @@ -7106,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", ] diff --git a/src/components/ModelingMachineProvider.tsx b/src/components/ModelingMachineProvider.tsx index 2663209a1..8a355acf4 100644 --- a/src/components/ModelingMachineProvider.tsx +++ b/src/components/ModelingMachineProvider.tsx @@ -314,8 +314,9 @@ export const ModelingMachineProvider = ({ 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 @@ -359,9 +360,16 @@ export const ModelingMachineProvider = ({ format.selection = { type: 'default_scene' } } - 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 + toast.promise( + exportFromEngine({ + format: format as Models['OutputFormat_type'], + }), + { + loading: 'Exporting...', + success: 'Exported successfully', + error: 'Error while exporting', + } + ) }, }, guards: { diff --git a/src/lang/std/engineConnection.ts b/src/lang/std/engineConnection.ts index b8bccac92..99f30b517 100644 --- a/src/lang/std/engineConnection.ts +++ b/src/lang/std/engineConnection.ts @@ -994,6 +994,10 @@ export class EngineCommandManager { engineConnection?: EngineConnection defaultPlanes: DefaultPlanes | null = null commandLogs: CommandLog[] = [] + pendingExport?: { + resolve: (filename?: string) => void + reject: (reason: any) => void + } _commandLogCallBack: (command: CommandLog[]) => void = () => {} private resolveReady = () => {} /** 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 // export we send a binary blob. // Pass this to our export function. - void exportSave(event.data) + exportSave(event.data).then(() => { + this.pendingExport?.resolve() + }, this.pendingExport?.reject) } else { const message: Models['WebSocketResponse_type'] = JSON.parse( event.data @@ -1548,6 +1554,12 @@ export class EngineCommandManager { this.outSequence++ this.engineConnection?.unreliableSend(command) 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 ( command.cmd.type === 'default_camera_look_at' || From 4b676d47dad4ade75af5ea3bcf52e42780ebe35b Mon Sep 17 00:00:00 2001 From: Kurt Hutten Date: Thu, 30 May 2024 13:28:29 +1000 Subject: [PATCH 2/2] 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 --- e2e/playwright/flow-tests.spec.ts | 152 ++++++++++ playwright.config.ts | 2 +- src/components/ModelingMachineProvider.tsx | 16 +- src/components/Toolbar/setAngleLength.tsx | 2 +- src/lib/selections.ts | 22 +- src/machines/modelingMachine.ts | 325 ++++++++++++++------- 6 files changed, 407 insertions(+), 112 deletions(-) diff --git a/e2e/playwright/flow-tests.spec.ts b/e2e/playwright/flow-tests.spec.ts index fd558ddf1..53dfd6335 100644 --- a/e2e/playwright/flow-tests.spec.ts +++ b/e2e/playwright/flow-tests.spec.ts @@ -2439,6 +2439,158 @@ test('Extrude from command bar selects extrude line after', async ({ ) }) +test.describe('Testing constraints', () => { + test.describe('Two segment - no modal constraints', () => { + const cases = [ + { + codeAfter: `|> angledLine([83, segLen('seg01', %)], %)`, + constraintName: 'Equal Length', + }, + { + codeAfter: `|> angledLine([segAng('seg01', %), 78.33], %)`, + constraintName: 'Parallel', + }, + { + codeAfter: `|> lineTo([segEndX('seg01', %), 61.34], %)`, + constraintName: 'Vertically Align', + }, + { + codeAfter: `|> lineTo([154.9, segEndY('seg01', %)], %)`, + constraintName: 'Horizontally Align', + }, + ] as const + for (const { codeAfter, constraintName } of cases) { + test(`${constraintName}`, async ({ page }) => { + await page.addInitScript(async () => { + localStorage.setItem( + 'persistCode', + `const yo = 5 +const part001 = startSketchOn('XZ') + |> startProfileAt([-7.54, -26.74], %) + |> line([74.36, 130.4], %) + |> line([78.92, -120.11], %) + |> line([9.16, 77.79], %) +const part002 = startSketchOn('XZ') + |> startProfileAt([299.05, 231.45], %) + |> xLine(-425.34, %, 'seg-what') + |> yLine(-264.06, %) + |> xLine(segLen('seg-what', %), %) + |> lineTo([profileStartX(%), profileStartY(%)], %)` + ) + }) + const u = await getUtils(page) + await page.setViewportSize({ width: 1200, height: 500 }) + await page.goto('/') + await u.waitForAuthSkipAppStart() + + await page.getByText('line([74.36, 130.4], %)').click() + await page.getByRole('button', { name: 'Edit Sketch' }).click() + + const line1 = await u.getBoundingBox(`[data-overlay-index="${0}"]`) + const line3 = await u.getBoundingBox(`[data-overlay-index="${2}"]`) + + // select two segments by holding down shift + await page.mouse.click(line1.x - 20, line1.y + 20) + await page.keyboard.down('Shift') + await page.mouse.click(line3.x - 3, line3.y + 20) + await page.keyboard.up('Shift') + const constraintMenuButton = page.getByRole('button', { + name: 'Constrain', + }) + const constraintButton = page.getByRole('button', { + name: constraintName, + }) + + // apply the constraint + await constraintMenuButton.click() + await constraintButton.click() + + await expect(page.locator('.cm-content')).toContainText(codeAfter) + // expect the string 'seg01' to appear twice in '.cm-content' the tag segment and referencing the tag + const content = await page.locator('.cm-content').innerText() + await expect(content.match(/seg01/g)).toHaveLength(2) + // check there are still 2 cursors (they should stay on the same lines as before constraint was applied) + await expect(page.locator('.cm-cursor')).toHaveCount(2) + // check actives lines + const activeLinesContent = await page.locator('.cm-activeLine').all() + await expect(activeLinesContent).toHaveLength(2) + + // check both cursors are where they should be after constraint is applied + await expect(activeLinesContent[0]).toHaveText( + "|> line([74.36, 130.4], %, 'seg01')" + ) + await expect(activeLinesContent[1]).toHaveText(codeAfter) + }) + } + }) + test.describe('Axis & segment - no modal constraints', () => { + const cases = [ + { + codeAfter: `|> lineTo([154.9, ZERO], %)`, + axisClick: { x: 950, y: 250 }, + constraintName: 'Snap To X', + }, + { + codeAfter: `|> lineTo([ZERO, 61.34], %)`, + axisClick: { x: 600, y: 150 }, + constraintName: 'Snap To Y', + }, + ] as const + for (const { codeAfter, constraintName, axisClick } of cases) { + test(`${constraintName}`, async ({ page }) => { + await page.addInitScript(async () => { + localStorage.setItem( + 'persistCode', + `const yo = 5 +const part001 = startSketchOn('XZ') + |> startProfileAt([-7.54, -26.74], %) + |> line([74.36, 130.4], %) + |> line([78.92, -120.11], %) + |> line([9.16, 77.79], %) +const part002 = startSketchOn('XZ') + |> startProfileAt([299.05, 231.45], %) + |> xLine(-425.34, %, 'seg-what') + |> yLine(-264.06, %) + |> xLine(segLen('seg-what', %), %) + |> lineTo([profileStartX(%), profileStartY(%)], %)` + ) + }) + const u = await getUtils(page) + await page.setViewportSize({ width: 1200, height: 500 }) + await page.goto('/') + await u.waitForAuthSkipAppStart() + + await page.getByText('line([74.36, 130.4], %)').click() + await page.getByRole('button', { name: 'Edit Sketch' }).click() + + const line3 = await u.getBoundingBox(`[data-overlay-index="${2}"]`) + + // select segment and axis by holding down shift + await page.mouse.click(line3.x - 3, line3.y + 20) + await page.keyboard.down('Shift') + await page.waitForTimeout(100) + await page.mouse.click(axisClick.x, axisClick.y) + await page.keyboard.up('Shift') + const constraintMenuButton = page.getByRole('button', { + name: 'Constrain', + }) + const constraintButton = page.getByRole('button', { + name: constraintName, + }) + + // apply the constraint + await constraintMenuButton.click() + await expect(constraintButton).toBeVisible() + await constraintButton.click() + + // check the cursor is where is should be after constraint is applied + await expect(page.locator('.cm-content')).toContainText(codeAfter) + await expect(page.locator('.cm-activeLine')).toHaveText(codeAfter) + }) + } + }) +}) + test.describe('Testing segment overlays', () => { test.describe('Hover over a segment should show its overlay, hovering over the input overlays should show its popover, clicking the input overlay should constrain/unconstrain it:\nfor the following segments', () => { /** diff --git a/playwright.config.ts b/playwright.config.ts index ff419f63c..f4b708cdc 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -18,7 +18,7 @@ export default defineConfig({ /* Retry on CI only */ retries: process.env.CI ? 3 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 2 : 1, + workers: process.env.CI ? 1 : 1, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ diff --git a/src/components/ModelingMachineProvider.tsx b/src/components/ModelingMachineProvider.tsx index 8a355acf4..f44b80325 100644 --- a/src/components/ModelingMachineProvider.tsx +++ b/src/components/ModelingMachineProvider.tsx @@ -34,6 +34,7 @@ import { handleSelectionBatch, isSelectionLastLine, isSketchPipe, + updateSelections, } from 'lib/selections' import { applyConstraintIntersect } from './Toolbar/Intersect' import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance' @@ -53,6 +54,7 @@ import { } from 'lang/modifyAst' import { Program, + Value, VariableDeclaration, coreDump, parse, @@ -73,10 +75,7 @@ import { useSearchParams } from 'react-router-dom' import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls' import { getVarNameModal } from 'hooks/useToolbarGuards' import useHotkeyWrapper from 'lib/hotkeyWrapper' -import { - EngineConnectionState, - EngineConnectionStateType, -} from 'lang/std/engineConnection' +import { applyConstraintEqualAngle } from './Toolbar/EqualAngle' type MachineContext = { state: StateFrom @@ -223,8 +222,7 @@ export const ModelingMachineProvider = ({ : {} ), '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 + 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 @@ -311,6 +309,12 @@ export const ModelingMachineProvider = ({ selectionRanges: selections, } } + if (setSelections.selectionType === 'completeSelection') { + editorManager.selectRange(setSelections.selection) + return { + selectionRanges: setSelections.selection, + } + } return {} }), diff --git a/src/components/Toolbar/setAngleLength.tsx b/src/components/Toolbar/setAngleLength.tsx index 95984bc9c..3137ed4c5 100644 --- a/src/components/Toolbar/setAngleLength.tsx +++ b/src/components/Toolbar/setAngleLength.tsx @@ -144,7 +144,7 @@ export async function applyConstraintAngleLength({ pathToNodeMap, } } catch (e) { - console.log('erorr', e) + console.log('error', e) throw e } } diff --git a/src/lib/selections.ts b/src/lib/selections.ts index 218d768ed..b261480ed 100644 --- a/src/lib/selections.ts +++ b/src/lib/selections.ts @@ -5,7 +5,7 @@ import { kclManager, sceneEntitiesManager, } 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 { uuidv4 } from 'lib/utils' import { EditorSelection } from '@codemirror/state' @@ -27,6 +27,7 @@ import { } from 'clientSideScene/sceneEntities' import { Mesh, Object3D, Object3DEventMap } from 'three' 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 Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01' @@ -564,3 +565,22 @@ export function sendSelectEventToEngine( .then((res) => res.data.data) 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(ast, pathToNode).node + return { + range: [node.start, node.end], + type: prevSelectionRanges.codeBasedSelections[Number(index)]?.type, + } + } + ), + } +} diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index a098d8177..cd94be44d 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -1,5 +1,5 @@ import { PathToNode, VariableDeclarator, parse, recast } from 'lang/wasm' -import { Axis, Selection, Selections } from 'lib/selections' +import { Axis, Selection, Selections, updateSelections } from 'lib/selections' import { assign, createMachine } from 'xstate' import { isNodeSafeToReplacePath, @@ -150,7 +150,10 @@ export type ModelingMachineEvent = } ) } - | { type: 'Set selection'; data: SetSelections } + | { + type: 'Set selection' + data: SetSelections + } | { type: 'Sketch no face' } | { type: 'Toggle gui mode' } | { type: 'Cancel' } @@ -214,7 +217,7 @@ export type MoveDesc = { line: number; snippet: string } export const modelingMachine = createMachine( { - /** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0AdgCsAZgB04gEyjhADnEA2GgoUAWJQBoQAT0QBGGuICckmoZkbTM42YWKAvk91oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBEFTU2VJYWFxDUNDbPFxCQ1dAwQZO0lDW3EcuQzlBRoNFzd0LDxCXwCAW1QAVyDA9hJ2MAjeGI4ueNBE5JTJVUNNBUNlLQ05YsQsjQWNLVE5Yw0aZXFmkHc2r07-XygusFwAgHkANzAAJ0wSPVgxqITOK8RI0LYIGgXK6eKCSbAQTBgAgAUWeX0CAGs-OQABYA5isSbcEFCGTKYQVMmpLRWHLkor6RAyUy7DRZWR5GimBTKUxQ1owuEIpGokafTHYvGGSIE2JTElJaw0STZRaiNTiNQ0YTgk4ySS2YTKURWeqGcQrZT8jztIWIlF8difXoYfHRQnAhJCUTySTMmiiXmiDSiFbgnLqv08hQpdU2faGa3XWHw+3IvgsT7sN1A+VexXqKNKsq5Fmh8EZQwLMRZY0NGTCENJwXeLHsXHEMiUTCtyU5j15mZGJZpYyNrRasyycNLKs5dQ5TVLUPN2299s4ggAEWCI0CYAeT2z9HGA+J+bBjIhq-wknXHfIrX8kA4-ggvU+7QlG-7cvPQ4Qc0QwqfZjQtMpA32cEWTSDIyhUMl6gUCQb1he8cTvNtcQASWFAhkBILF90PZ5-A+LNsHIEhMF-Ilpn4IxmTnBQZE0I1NBoBtwzyaQaD4uQJAbJQmlcS4BTXLCMPQ3D7QIoighIgIcVQT8AC9uCGGiT0BM96MSKxGwWNRVBNAzyRkcNqirYN5EsM5jB9GRUMwyUXI3GSkSIbhYCdEg8H8ZS1I06i32wHyu1GbTZTohUrGDCwNk1bkslMJYFHBGQzDSAMfT4kNUvNZz0LcnC8K83AfM+PzcDIr5OCozBQvCihIpld0-z0xj6gNQ5kNWbI1A2DKGykQ5sjYuDjRElobVvYrpLK7zfP8gBBAAhbx-AADVoz0AKsZCVXEMRS2qZlMoy0x4qu8kGk40RTEyc5ROhCTXIW+1ysq6r-HWzaAE1dsHBjANY0Q-Q2VYjkDQMLKvTVC2EASTVURx9iKySSpxDziCWqr-LIKBESB-8QaAqQLXA5DFEMYRTAy9UKTkamMkDOnHox97JJxr7lpqxF8HYPEovamL83NY7pGNVRUtkapRAy5QlnSXlsjp-YlHJTmNyxnm8Z+pgvkN3AIEo3ofnFU3msoEnOtB1iVRSM6ThHU4MpWXZhEqGM4P2DQxG13FdcWiq+YClTsHU55qMwPR-Go7AoFwW3YrJfVkOWQpg1OcRwxRv0romhtA3yQOpO5kPvv88j6pjuOE6TlPxYbcw2VYumUbME1w2UORJFDR7uXqZlHGembkyx4PPv1-zYFwEgmH8dhUG2pv9uqKQyQtNRHpYswMsafUIOUE+TMcMoy6nzyZ5queF6XlfAZF3NSf0jeLCNaoMmO44MtyClgznwerTDQawx5iVmmhTGH1r6h3xjVMAABHXoIUBZQCFmvMmzJRr-zpsIAMchjrhkyMqNkJg+IBmlnyF64k5rQIrtPOBBsSBVUwIiLSbUX523NA2CwZR7IPSyCA8MphJYmGqErFI2o1CiEvjA3GTD-KfDAD0D4-hHyKOeP8Z+ulU5aAWFYEMvdNCGFDLnK8SxqgWFWLYRo+RjJORoZAye8iABKYBBBgD4CEXoIxMH6V6v3M4yECgnxLDqCxJhwZkNEYUUBORhByIYSKZB2BF4ABk8BgAfqgDhp4OqxRjGkCQKgbBXSyOocEShwaM2sBBUBpkkmShxsiVJi8hj4CPNgEKLDyAP38UyOKKouQBnKbWcxJRjAxj9D6FQpwh4BnAa9OhXNml4Vab0NJ-hlGhEJoiHJeSdIFPzGUMQfopkbBZCGEw4YZCWBVCPes2DMhNPcnhR8GBnymwCO+T8+Bvy4gGYBLI4NcghjEGsVicMSjcgUNIIRVkJDe2mhAie81kkKJrg-MiLDukACNiY6OOevQ41YljrCVlyBkJRZDahVGOGMthuRXUvitAA7n5JSEco6aSah0yg-g8AADNUAEAgNwMAcJcBvFQFiSQMB2CCECpHYKmBBDCtQECzKjQVSgJOI9Mwj1MgZWyOYZCUKJG2DZqyjlr5lU8pClbfl2SNUEC+J8FSkgmA-HYCKz4XR5V+CVdy1V6rcAiq1ZqUFYLFwhlYo9DK4EEqnD4gUBsSsbWctqhRBqfKIqCvDaK8VuBJV4BlXKhVgga6UWomGiNRKxYAUylYCox0VgmHJJULiV4yhKEkI4Q4hwJa00cePFsmN2VZurbmp1+bXXus9d64YfqA2VunbWjVkaW0NHkMAsQmhhobAqNLTiFoWLqCWbQqBrlJ2vj+ttAtIqxUSqleWyVlaSB4tgIIPgdbNUNr2iDZtsFe78UIeWHtqR9S5EaP1WQvcXlOLRRO21AR71bUfaKhdnwvU+pXYGxVn7v2-s3QB4GiRm0UjZGegopgAxaEuqsaQoYwLHEbNQsdb0da3rQxtfw-1MPPpLa+2V76g1EcEHoP9W60gFF3Q9Z5IZwSanJI7GM6ozH0czXevjAn52fA9ThpdvqVKrvE1+yT0myOvyZF-Phn8kYnyEcoZT-ULC8jCUcY6o7UXjpvah+O+B9muuLaW6VomCOCD2R40jnDdEnLs0uc0g7YyGAZoGA0UbLCmJYxkbTARouCew7h5dpnIvRas3F4lQHqhwvwdkViVgd60wZrTCoyX8FyAIVrJDfnuMBbQULQToWRMVqDYNnElX8mNpq6Yo6mUWQgLud2koZJDo5y-ioWmVh8v+Am0Vgzi68NlcrRNqbRyZsUYkRUVNoZVhsWhUySFCxe5aBZGIVmVpetcaDjx-whtPjG1NuQc2LC80tWGy+stEXK0A6B2bC2ghZ0tXO9FQDFG049SUNYXua3Ng9pYuDH+oDjSxkIbtuHTxgeg8tmFZ1B3DMlZM-6yLlOTYI5YUjunEVUei3R0yTHhcOS4+HgrHtGQKTE5Pg9K65PvsrJ1pkktByURtKXoTLpPTPh9OXlqumaRuT-zsaIumEzEC72GdULQxpHNLEvkr7Jy9cmq82YvHZHSiaO9QLkrVxdzlciyF7U904LGmNgkjYw2p1AmjkPbrJBzJDYVwBwAgvuVD9otCaBoLI5D0yvGaP0FpRF0eOnTTKcfldO8wIn5P7BU-Smm-z0oDZ9Tqka2sJrvIKx0xVGcCQVreR8VkfL69iv49V8kAAORXgABVQHgdgsACArQgBAQYLCAgsAX1qk+uw6Mwxzw0NY4IT6tzMHgqPSgFAV697kqfs-59aM7C1Q5aPyNGE0OYEwmUWJUozulK8fBKsGwSRfIA6M4S+JPFPIFYwJjA4fBOZQ4KlW5fIXvI0MkFNMkbISA2vevKrS7IwAMKsGMEZUBWlSwalRiVKdIEJeJLA0RL7TjBXIOAAFQ12eG6Ual6Qfhdy2QdwOR332COijwenVF7mglAXSCRkylslOFAUSRH0nl6BNhXnCizHQheFwCEzCzfUkBWm8BYMEGUPFUEDUPYA0OTms24XyDhUOEyAKn2AKDUHBFDGVFSEqEGkyCUGOkvncV2SC1v2rx4y-AjigDwGX1X22RCA932VCLwEwUmWyHBgXBWFYmqD4gTSA0cGKU1CMRSC9hjEvnIERDIECB9SRBgOyF2BMh9HNVyBKRnHNAsFOGMF3kNR20UOKj+w0SxWXhxU-E-XtGK2M3w0rR6LqkEGXirVxUGJi0LV90cGsXNTAK9h9DFxpU1AphsRSFYlRjUDLn8FwBXiFRIEoB8GCFCDfDABOPNk3x+BLSBUEDsGYhZFL2t3Y3WMQF7jSFSPZHZiNBsGcjIGwC6GGC-D6KXRLW0NG0lWBNBJGEmNQEEBOJtisIVB9GYij0aDA21BPwdjOnsXjUbHUCBOT3hPBJXi8Tpy-FgEkmhOhzlThOGA8SmNpL7DRPzF7nBk4gQ0N2DG81czhRPi8NSHUHe2v06JQyzXGKzGxTeBmIJSRBGwZLE0VRlMVSmPlIGMVN5y4QVFOWjUaBN3bSVk+IQFkEqAqFSBlje0MWH1EiOIwHgCiGWSgEb3fySAaCrFkHkE1jUGWDN09IAQoTWFPlEQKBRVdLtDAHdJsySDWFsPJANU0FDH9iqRWH7gRlMQJMsA418x+xxFjLtlFJVGpg7QIWQJ7RMANCVnUADCINMQlKYNHyDhgSLNihOE3ken9lsBTLpgAMmUqF2FyCsBU3yAQMvWcS6IC3tVVXBwFQ1XbPFmjF1S5EUC9gbE60TVsGkBDE1FOF5AbETElP8ynTqhrUamRwXMLSXP2nUGg1UEsAKAQlSmGkjE4nGlyGlxNF23Q0w1vLJhjCkA1CsAYLsAyMuisSMUUEWDPh8yjOnKzXvT0xvIuybyWB3N6jZCWEPmU1OHBggh9DEFhWZF20K0XLQo9IwvBjODynyG5EOFEQZjUH7R-x9FUHQIKF232worfzjIwtISiTrEqBUBPkVnTzpm9iNHVhLgpyNipw51p2thdVQr4u4WQlglSHVA4hAPyAyhpnSG9mASItSBvwOQAoo3sA-hsODFhXqBcIzOL0cEGnqGkLMonygPYAssGRNGsszxsHUHsvzx3NWDOFAjMEaF5Hcu92ryOP9Wom8tKBsCo2-n9AaFkEewQCAMdlALOh5FkGirv2n38DnwX2dLUv1L3KjCt3bTljOArF7ijByHNCWABJWBwI4ESocnMDWGDCVEplSkoMAlnHawXHNEsAzkvjYM6Q4K1x11QESuZgpBMBYg2FATJC-PDHwr2EegemcvZngqvSUJUPX3UMkk0K6vqHMAkEARLjMWZBcMkPiVsBDDZg0t8OiMKyr0WokDSF6sbF3VqzdgsUa2kBWCUFOEtADhPJ1j8JiMCL0NQxpJCG4DX0fE+BLU+B+s0D2HkHBSRSPzzjoyOjWBOhaqUDzIQsxjhq+pisRs5RCM-DCPoj1JOSVjSDyCpGSowu4jmxSIOnSLXKKJKNvnKK6qnH7kuUemsFkFsC2rOBrIIUZhsV23VLlIVMRC6o0v7TyjMXlm7nhjKGVGfNlzLEkSbPzOYJxEOOONOJjMorjKeNQLrGWw2EhU4l1EyBmV7igzqCnFJJBLBP+QhPuPtoqvFgVsekHxsHkE7zxOyjPSNEAQRgUObMkCZM4GDspL4GpP+TZI3C6senSEDBt04lyGqDloJ2VBPh3g-IcQlJcCAA */ + /** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0AdgCsAZgB04gEyjhADnEA2GgoUAWJQBoQAT0QBGGuICckmoZkbTM42YWKAvk91oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBEFTU2VJYWFxDUNDbPFxCQ1dAwQZO0lDW3EcuQzlBRoNFzd0LDxCXwCAW1QAVyDA9hJ2MAjeGI4ueNBE5JTJVUNNBUNlLQ05YsQsjQWNLVE5Yw0aZXFmkHc2r07-XygusFwAgHkANzAAJ0wSPVgxqITOK8RI0LYIGgXK6eKCSbAQTBgAgAUWeX0CAGs-OQABYA5isSbcEFCGTKYQVMmpLRWHLkor6RAyUy7DRZWR5GimBTKUxQ1owuEIpGokafTHYvGGSIE2JTElJaw0STZRaiNTiNQ0YTgk4ySS2YTKURWeqGcQrZT8jztIWIlF8difXoYfHRQnAhJCUTySTMmiiXmiDSiFbgnLqv08hQpdU2faGa3XWHw+3IvgsT7sN1A+VexXqKNKsq5Fmh8EZQwLMRZY0NGTCENJwXeLHsXHEMiUTCtyU5j15mZGJZpYyNrRasyycNLKs5dQ5TVLUPN2299s4ggAEWCI0CYAeT2z9HGA+J+bBjIhq-wknXHfIrX8kA4-ggvU+7QlG-7cvPQ4Qc0QwqfZjQtMpA32cEWTSDIyhUMl6gUCQb1he8cTvNtcQASWFAhkBILF90PZ5-A+LNsHIEhMF-Ilpn4IxmTnBQZE0I1NBoBtwzyaQaD4uQJAbJQmlcS4BTXLCMPQ3D7QIoighIgIcVQT8AC9uCGGiT0BM96MSKxGwWNRVBNAzyRkcNqirYN5EsM5jB9GRUMwyUXI3GSkSIbhYCdEg8H8ZS1I06i32wHyu1GbTZTohUrGDCwNk1bkslMJYFHBGQzDSAMfT4kNUvNZz0LcnC8K83AfM+PzcDIr5OCozBQvCihIpld0-z0xj6gNQ5kNWbI1A2DKGykQ5sjYuDjRElobVvYrpLK7zfP8gBBAAhbx-AADVoz0AKsZCVXEMRS2qZlMoy0x4qu8kGk40RTEyc5ROhCTXIW+1ysq6r-HWzaAE1dsHBjANY0Q-Q2VYjkDQMLKvTVC2EASTVURx9iKySSpxDziCWqr-LIKBESB-8QaAqQLXA5DFEMYRTAy9UKTkamMkDOnHox97JJxr7lpqxF8HYPEovamL83NY7pGNVRUtkapRAy5QlnSXlsjp-YlHJTmNyxnm8Z+pgvkN3AIEo3ofnFU3msoEnOtB1iVRSM6ThHU4MpWXZhEqGM4P2DQxG13FdcWiq+YClTsHU55qMwPR-Go7AoFwW3YrJfVkOWQpg1OcRwxRv0romhtA3yQOpO5kPvv88j6pjuOE6TlPxYbcw2VYumUbME1w2UORJFDR7uXqZlHGembkyx4PPv1-zYFwEgmH8dhUG2pv9uqKQyQtNRHpYswMsafUIOUE+TMcMoy6nzyZ5queF6XlfAZF3NSf0jeLCNaoMmO44MtyClgznwerTDQawx5iVmmhTGH1r6h3xjVMAABHXoIUBZQCFmvMmzJRr-zpsIAMchjrhkyMqNkJg+IBmlnyF64k5rQIrtPOBBsSBVUwIiLSbUX523NA2CwiElhlENHYcMLIKRKz4oYUM6gd6XxgbjJh-lPhgB6B8fwj4FHPH+M-XSqctALCsCGXumhJE6CvEsaoFhVi2EaPkYyTkaGQMnnIgASmAQQYA+AhF6CMTB+ler9zOMhAoJ8Sw6jMSYcGZDTAFB2ErWmsiGEimQdgReAAZPAYAH6oA4aeDqsUYxpAkCoGwV0sjqHBEocGjNrAQVAaZBJkocbImSYvIY+AjzYBCiw8gD9fFMjiiqLkAZSm1lzuEmMfofQqFOEPAM4DXp0K5o0vCzTegpP8Eo0IhNERZJyTpPJ+YyhiD9MYTQzJ9jqjGSUKwlgVQj3rNgzIDT3J4UfBgZ8psAjvk-Pgb8uI+mASyODXIIYxBrFYnDEo3IFDSCyOaCM1RuTTQgRPeaiT5E1wfmRFhnSABGxNtEHPXocasSx1jiJZOCWQ2oVRjhjLYbkV1L4rQAO5+SUhHKOmkmptMoP4PAAAzVABAIDcDAHCXAbxUBYkkDAdgghAqR2CpgQQgrUAAsyo0FUoCTiPTMI9TIGVsjmGQhC6ovI5BawcaizGrL2XhyCtHRqVteWZLVQQL4nwVKSCYD8dgQrPhdFlX4BVnLlWqtwEKjVmpgUgsXCGVij0MrgQSqcPiBQGxK2ZWy18NdKIhRdRFflkbhWitwOKvAUqZVysEHmhqEao2ErFgBTKVgKjHRWCYcklQuJXjKEoSQjhDiHAlrTex48Wy2pzQEOtBawquuLUKj1nwvWfB9X6gNQaa2zpVWq6NbaGjyGAWITQw0NgVGlpxC0LF1DzNoVA1ydrXx-W2ou0tYqJVVvFTWkguLYCCD4A29VTa9og1bbBXu-FCHlj7akfUuRGj9VkL3J51rJ2Punb9Dar73Weu9b64Ym7g3yt-f+wDe6QPA0SK2ikbJr0FFMAGLQl1VjSFDGBY4jZqETrejrJ9AQX3-TfSKj9lbpXfpDaRwQeggP7rSAUI9D1HkhipacWjjL1ShhjcihZD6+OYcE8JvDa6CP+pUluyTf7pOyco6-JkX8+GfyRifOFygqX9QsLyEJRxjrjpReh-T9rtlupLSJ8tn7xPEcEMFmznCdGHIc0uc0w7YyGAZoGA0MbLCSPYxkbNQX8A7Nwyu-DG7zNRZixRuLRKwPVBhfg7IrEbkxlpgzWmFRkv4LkAQq1PHFmBdfGgoWwmy0VslZFmtQ2cSxdyc22rkijqZRZCAmQnEwklDJIdHOX8VC0ysPlwbTx0E4iMyVkzZXA1RamzN-Zc3qPmoqOm0Mqw2KQqZOChYvctCiPkGCg7ARDafGNqbcg5sWE8qLe60bEXq0hsB8Ds2FtBCFpajd6KoHqNpx6koawvdNubD7SxcGP9QHGljIQ-7-h4dPBB2Dy287IeheM+uwj5Wa3U5NojlhyOGeo6q7NjHTIseFw5Hj4eCs+0ZApCTk+D0roU7Q7xoO6Ty27JRC0pehMOldM+D05eGq6ZpG5P-Gx0S6ZXMQLvQZ1QtDGmc0sS+KvMnL2yertZi9NltKJs71A2SNXFxOVyLIXsr3TjMZI2CSNjDanUCaOQjuMm7MkNhXAHACD+5UIOi0JoGgsjkPTK8Zo-QWmiYx46dNMoJ9Vy7zAyfU-sHT9KAXVGhdI37pxS0jWZkVjpiqM4EhbCBkYwGKvPvsmSAAHIrwAAqoDwOwWABAVoQAgIMFhAO5-PA1SfXYjGYZ54aGscEJ9W5mDwTHpQChR9J6n-4Wf8-F+kBans9HLfAKaHMCYTKLEuQHHKVefBKsGwJWaJM6HkO9RxYqFPNPAFYwVjA4fBaZQ4X-cMTKKQUBI0MkNNMkbIS+aAhvKgJvW7QXQCAMKsGMIZUBalSwBka5JidIIJUBU0GwFQS+AAFS12eE6Uam6Qfjd3WSd12W332COhjwenVF7mglAXSCRkylslOAwMvl6BNhXnCizHQheFwDCzGy-UkBWm8DYMEGUNFUEDUPYA0OTls24XyBhUOEyAKn2AKDUHBFDGVFSEqEGkyCUGOkvlcS2UKzH1r34y-AjigDwCXxXw2RCC9x2VCLwEwWuWyHBgXBWFYmqD4iTTA0cEKU1EMRSC9hjEvnIERDIECD9SRFgOyF2BMh9FNVyCKRnHNAsFOGMF3n1X20V36yDn4zUW4ExWXmxU-F-XtGhzE1h3lXURrkEGXlrRxWGLcX52ILfyOVjUaDN07SVglxKFkEqAqFSBlm+wMVEEp0mLqixTeDmPxSRGZ1MyIxrVOKzGmNQFmKGKuLR1FhIP7QpEsFNXyD2x9C2O2E1ApisRSFYlRjUBOJvgdSVSdVjnjhwCTm0Jh3FRMPUSrlwFDUdU0ljmi0RMsOqzuyF1sH7hy0tS7S9jzjUANFAmOkAVSKv06L026Mw3RLDh3XhIbi0NGPGxlTRJvlrTqnzTYRky5P90MhDFVBOD4jWDSzMVUF2BUHyGNGsEbBayhI0UCHnlaRXi2mRLGNROeLZPgVMO1KeIA2jT4gWAKC9gKAMhUz7QiUpGsEMSnAIQ1IxK1PvgGP+n1N5MNMEGNOqlNIXnNL0EtLSH9iWHwQaFrDlI21NGkDKHOhpH2I9LDiQRQUaimz9N0P5I0XcWQWokECm391SgNFVJNBsEUGQjczMS9lHAkLBlyHbggJtQw3tSDP8iYBYRjiwFzMi3zIxMEB7NYXYUtJhUY1umRgyHSjMWiRhUyi8MbBy2XDLn8FwBXgFRIEoB8GCFCDfDAG3PNgBx+HLQBUEDsGYhZHL1ty40BIQF7jSFSPZHZiNBsGcjIGwC6GGC-AGII3LQHJlS-J-JGHNO3JtisIVB9GYhj0aCg21GPwdjOlsUTTVMZL61hBAt-N+QGI8XnS-FgEkiAvFWwrApmKIr7CgvzF7nBk4hQ2N2DF83cxhRPi8NSHUFEUZNEk3IwHgCiF02bzsySAaCrFkHkE1jUGWAtxEoAQoUwN5BDwKGclTDACErtkEDWFsPJD1U0FDH9gqRWH7gRkkRQssG438yVxxHUoVA4pVGpi7QIRQMdKkHcJZG1ApUkWEGeVKkRBsvFhOE3ken9lsD0rpjnOuUqF2FyCsE1DOGOhMDbICxZPtUVS5TnWthCyFX8v2mjG1S5EUC9gbC62TRJKBU1FOAtS9kpx3QhxajfRyrJnUHg1UEsBiV7lSmGkjE4nGlyFlxNEpxfS2gaqWOEqWGiQsCUCsGiV7mjwLw22iSALZh5DsgDBYkGuwyEzVUav0hjH1F6jZCWEPlU2ND9BnIemaost00nh6OCxGtfzGpjHBjODynyG5EOGiQZmpL3mQkDA8rkMpym3uo+Lf3GtIQiTrEqBUBPkVkzzpm9iNHVhLkpw51pwtjqr5W2tGu4VrPSFSHVA4mAPyAyhpnSG9mAR9EN2vxrx2v6RMA-hsODGhXqBcKMtL1nMY0yGZGOKZMnkEJrzrw4FptKEkSrG1EZpsHUBZsLxJNWDOFpMY1ASuvvT5sTwFs3MDWomFusEehVG-n9FjLKArFuWAKhrALOD82uuKn5t91r1v3v00W1pDE3imppC-yVIrF7ijByHhWyJpDwPr2FocnMDWGDCVEplSloOHCMvnBWHNB+JXF5uKg4PaS4J1z11QG1tDG+PAg2FATJD6vDFOGeurIuvwXZktpVuKmMNUKGHUMkk0KDvqHMAkEARLi02ZBcOkKYNsBDGWsDF8OiLuppuxoVEITSFDsbCPTqzdjMSa2kBWEbD4k1kAMHv8O9yT2CN+SCHUVX0fE+HLU+Czs0D2HkFBQkCOrrOuQelIWKROl9rMDXpiMCL0OnRCM-DCPoi4THqVkjJ4VVkenGu4gWxSIOnSIKqKJKNvnKKDqnH7g2HyOsFkFsCLrOANBMm6wbOVsgKnU7L6LOIGIuNeL8tHvFlxruhDG8NXKpTKGVBiXlzLBAIwssq6Iwh6K7JqjSuVU5PxKDs0CkAKQY1VAcCvqMGrIqC9lNEtUZm8qTtwdfA4dqgogah4cTi-vi1yrZEHVSCEdWAcAiqMCU37hjCCSuhzwtHTPgS9J1O2j4YyzugbDQojyjtKCvWkG+3ZH9ksZ+jvhsf+jsefP4iRh9mcb-mQp2PwWznUGQm8f8kzNQSOyFj4a0cEYemEaHh7l1rIQU1kGyJ9FiZqlHL7MwDseBUcKKUHnjKMFsEjLWCvUUEaFrPXM3P8AgrUtIYAkvPyAWFDFWxqXBU4l1EyEmQ6pQ0-miU-NT1Ar-JXgAvaYeu4TQcel5DWPkBuVEcfLJAsGvSNEAQRlkcwskDIpmefD4AIu3skiDt1rBTt04lyERTe1KAaE+x3h6rsW4qcCAA */ id: 'Modeling', tsTypes: {} as import('./modelingMachine.typegen').Typegen0, @@ -331,44 +334,32 @@ export const modelingMachine = createMachine( 'Constrain horizontally align': { cond: 'Can constrain horizontally align', - target: 'SketchIdle', - internal: true, - actions: ['Constrain horizontally align'], + target: 'Await constrain horizontally align', }, 'Constrain vertically align': { cond: 'Can constrain vertically align', - target: 'SketchIdle', - internal: true, - actions: ['Constrain vertically align'], + target: 'Await constrain vertically align', }, 'Constrain snap to X': { cond: 'Can constrain snap to X', - target: 'SketchIdle', - internal: true, - actions: ['Constrain snap to X'], + target: 'Await constrain snap to X', }, 'Constrain snap to Y': { cond: 'Can constrain snap to Y', - target: 'SketchIdle', - internal: true, - actions: ['Constrain snap to Y'], + target: 'Await constrain snap to Y', }, 'Constrain equal length': { cond: 'Can constrain equal length', - target: 'SketchIdle', - internal: true, - actions: ['Constrain equal length'], + target: 'Await constrain equal length', }, 'Constrain parallel': { - target: 'SketchIdle', - internal: true, + target: 'Await constrain parallel', cond: 'Can canstrain parallel', - actions: ['Constrain parallel'], }, 'Constrain remove constraints': { @@ -597,6 +588,68 @@ export const modelingMachine = createMachine( }, }, }, + + 'Await constrain horizontally align': { + invoke: { + src: 'do-constrain-horizontally-align', + id: 'do-constrain-horizontally-align', + onDone: { + target: 'SketchIdle', + actions: 'Set selection', + }, + }, + }, + 'Await constrain vertically align': { + invoke: { + src: 'do-constrain-vertically-align', + id: 'do-constrain-vertically-align', + onDone: { + target: 'SketchIdle', + actions: 'Set selection', + }, + }, + }, + 'Await constrain snap to X': { + invoke: { + src: 'do-constrain-snap-to-x', + id: 'do-constrain-snap-to-x', + onDone: { + target: 'SketchIdle', + actions: 'Set selection', + }, + }, + }, + 'Await constrain snap to Y': { + invoke: { + src: 'do-constrain-snap-to-y', + id: 'do-constrain-snap-to-y', + onDone: { + target: 'SketchIdle', + actions: 'Set selection', + }, + }, + }, + + 'Await constrain equal length': { + invoke: { + src: 'do-constrain-equal-length', + id: 'do-constrain-equal-length', + onDone: { + target: 'SketchIdle', + actions: 'Set selection', + }, + }, + }, + 'Await constrain parallel': { + invoke: { + src: 'do-constrain-parallel', + id: 'do-constrain-parallel', + onDone: { + target: 'SketchIdle', + actions: 'Set selection', + }, + }, + }, }, initial: 'Init', @@ -812,88 +865,6 @@ export const modelingMachine = createMachine( sketchDetails.origin ) }, - 'Constrain horizontally align': ({ selectionRanges, sketchDetails }) => { - const { modifiedAst } = applyConstraintHorzVertAlign({ - selectionRanges, - constraint: 'setVertDistance', - }) - if (!sketchDetails) return - sceneEntitiesManager.updateAstAndRejigSketch( - sketchDetails?.sketchPathToNode || [], - modifiedAst, - sketchDetails.zAxis, - sketchDetails.yAxis, - sketchDetails.origin - ) - }, - 'Constrain vertically align': ({ selectionRanges, sketchDetails }) => { - const { modifiedAst } = applyConstraintHorzVertAlign({ - selectionRanges, - constraint: 'setHorzDistance', - }) - if (!sketchDetails) return - sceneEntitiesManager.updateAstAndRejigSketch( - sketchDetails?.sketchPathToNode || [], - modifiedAst, - sketchDetails.zAxis, - sketchDetails.yAxis, - sketchDetails.origin - ) - }, - 'Constrain snap to X': ({ selectionRanges, sketchDetails }) => { - const { modifiedAst } = applyConstraintAxisAlign({ - selectionRanges, - constraint: 'snapToXAxis', - }) - if (!sketchDetails) return - sceneEntitiesManager.updateAstAndRejigSketch( - sketchDetails?.sketchPathToNode || [], - modifiedAst, - sketchDetails.zAxis, - sketchDetails.yAxis, - sketchDetails.origin - ) - }, - 'Constrain snap to Y': ({ selectionRanges, sketchDetails }) => { - const { modifiedAst } = applyConstraintAxisAlign({ - selectionRanges, - constraint: 'snapToYAxis', - }) - if (!sketchDetails) return - sceneEntitiesManager.updateAstAndRejigSketch( - sketchDetails?.sketchPathToNode || [], - modifiedAst, - sketchDetails.zAxis, - sketchDetails.yAxis, - sketchDetails.origin - ) - }, - 'Constrain equal length': ({ selectionRanges, sketchDetails }) => { - const { modifiedAst } = applyConstraintEqualLength({ - selectionRanges, - }) - if (!sketchDetails) return - sceneEntitiesManager.updateAstAndRejigSketch( - sketchDetails?.sketchPathToNode || [], - modifiedAst, - sketchDetails.zAxis, - sketchDetails.yAxis, - sketchDetails.origin - ) - }, - 'Constrain parallel': ({ selectionRanges, sketchDetails }) => { - const { modifiedAst } = applyConstraintEqualAngle({ - selectionRanges, - }) - if (!sketchDetails) return - sceneEntitiesManager.updateAstAndRejigSketch( - sketchDetails?.sketchPathToNode || [], - modifiedAst, - sketchDetails.zAxis, - sketchDetails.yAxis, - sketchDetails.origin - ) - }, 'Constrain remove constraints': ({ selectionRanges, sketchDetails }) => { const { modifiedAst } = applyRemoveConstrainingValues({ selectionRanges, @@ -1152,5 +1123,153 @@ export const modelingMachine = createMachine( 'Reset Segment Overlays': () => sceneEntitiesManager.resetOverlays(), }, // end actions + services: { + 'do-constrain-horizontally-align': async ({ + selectionRanges, + sketchDetails, + }) => { + const { modifiedAst, pathToNodeMap } = applyConstraintHorzVertAlign({ + selectionRanges, + constraint: 'setVertDistance', + }) + if (!sketchDetails) return + await sceneEntitiesManager.updateAstAndRejigSketch( + sketchDetails?.sketchPathToNode || [], + modifiedAst, + sketchDetails.zAxis, + sketchDetails.yAxis, + sketchDetails.origin + ) + const updatedSelectionRanges = updateSelections( + pathToNodeMap, + selectionRanges, + parse(recast(modifiedAst)) + ) + return { + selectionType: 'completeSelection', + selection: updatedSelectionRanges, + } + }, + 'do-constrain-vertically-align': async ({ + selectionRanges, + sketchDetails, + }) => { + const { modifiedAst, pathToNodeMap } = applyConstraintHorzVertAlign({ + selectionRanges, + constraint: 'setHorzDistance', + }) + if (!sketchDetails) return + await sceneEntitiesManager.updateAstAndRejigSketch( + sketchDetails?.sketchPathToNode || [], + modifiedAst, + sketchDetails.zAxis, + sketchDetails.yAxis, + sketchDetails.origin + ) + const updatedSelectionRanges = updateSelections( + pathToNodeMap, + selectionRanges, + parse(recast(modifiedAst)) + ) + return { + selectionType: 'completeSelection', + selection: updatedSelectionRanges, + } + }, + 'do-constrain-snap-to-x': async ({ selectionRanges, sketchDetails }) => { + const { modifiedAst, pathToNodeMap } = applyConstraintAxisAlign({ + selectionRanges, + constraint: 'snapToXAxis', + }) + if (!sketchDetails) return + await sceneEntitiesManager.updateAstAndRejigSketch( + sketchDetails?.sketchPathToNode || [], + modifiedAst, + sketchDetails.zAxis, + sketchDetails.yAxis, + sketchDetails.origin + ) + const updatedSelectionRanges = updateSelections( + pathToNodeMap, + selectionRanges, + parse(recast(modifiedAst)) + ) + return { + selectionType: 'completeSelection', + selection: updatedSelectionRanges, + } + }, + 'do-constrain-snap-to-y': async ({ selectionRanges, sketchDetails }) => { + const { modifiedAst, pathToNodeMap } = applyConstraintAxisAlign({ + selectionRanges, + constraint: 'snapToYAxis', + }) + if (!sketchDetails) return + await sceneEntitiesManager.updateAstAndRejigSketch( + sketchDetails?.sketchPathToNode || [], + modifiedAst, + sketchDetails.zAxis, + sketchDetails.yAxis, + sketchDetails.origin + ) + const updatedSelectionRanges = updateSelections( + pathToNodeMap, + selectionRanges, + parse(recast(modifiedAst)) + ) + return { + selectionType: 'completeSelection', + selection: updatedSelectionRanges, + } + }, + 'do-constrain-parallel': async ({ selectionRanges, sketchDetails }) => { + const { modifiedAst, pathToNodeMap } = applyConstraintEqualAngle({ + selectionRanges, + }) + if (!sketchDetails) throw new Error('No sketch details') + await sceneEntitiesManager.updateAstAndRejigSketch( + sketchDetails?.sketchPathToNode || [], + parse(recast(modifiedAst)), + sketchDetails.zAxis, + sketchDetails.yAxis, + sketchDetails.origin + ) + const updatedSelectionRanges = updateSelections( + pathToNodeMap, + selectionRanges, + parse(recast(modifiedAst)) + ) + return { + selectionType: 'completeSelection', + selection: updatedSelectionRanges, + } + }, + 'do-constrain-equal-length': async ({ + selectionRanges, + sketchDetails, + }) => { + const { modifiedAst, pathToNodeMap } = applyConstraintEqualLength({ + selectionRanges, + }) + if (!sketchDetails) return + await sceneEntitiesManager.updateAstAndRejigSketch( + sketchDetails?.sketchPathToNode || [], + modifiedAst, + sketchDetails.zAxis, + sketchDetails.yAxis, + sketchDetails.origin + ) + const updatedSelectionRanges = updateSelections( + pathToNodeMap, + selectionRanges, + parse(recast(modifiedAst)) + ) + return { + selectionType: 'completeSelection', + selection: updatedSelectionRanges, + } + }, + }, + // end services } )