Compare commits
14 Commits
paultag/co
...
achalmers/
Author | SHA1 | Date | |
---|---|---|---|
36a5461de5 | |||
6a9a0a8bd7 | |||
90e432b10e | |||
90499e086f | |||
8b398a8dd5 | |||
23d2dc8dc8 | |||
764a73ec8b | |||
b69451d2fe | |||
173d50517c | |||
3b63632005 | |||
2bd3b06178 | |||
9c58cde35f | |||
3eb92bb0c4 | |||
f3083eb59d |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -340,7 +340,7 @@ jobs:
|
||||
cat last_download.json
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: 'google-github-actions/auth@v1.2.0'
|
||||
uses: 'google-github-actions/auth@v2.0.0'
|
||||
with:
|
||||
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
|
||||
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -35,6 +35,8 @@ public/wasm_lib_bg.wasm
|
||||
src/wasm-lib/lcov.info
|
||||
|
||||
e2e/playwright/playwright-secrets.env
|
||||
e2e/playwright/temp1.png
|
||||
e2e/playwright/temp2.png
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
|
2459
e2e/playwright/export-snapshots/gltf-standard-2.gltf
Normal file
2459
e2e/playwright/export-snapshots/gltf-standard-2.gltf
Normal file
File diff suppressed because it is too large
Load Diff
@ -4,6 +4,8 @@ import { EngineCommand } from '../../src/lang/std/engineConnection'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { getUtils } from './test-utils'
|
||||
import waitOn from 'wait-on'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import fsp from 'fs/promises'
|
||||
|
||||
/*
|
||||
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
|
||||
@ -58,8 +60,13 @@ test('Basic sketch', async ({ page }) => {
|
||||
|
||||
// click on "Start Sketch" button
|
||||
await u.clearCommandLogs()
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
await u.waitForDefaultPlanesVisibilityChange()
|
||||
await Promise.all([
|
||||
u.doAndWaitForImageDiff(
|
||||
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
||||
200
|
||||
),
|
||||
u.waitForDefaultPlanesVisibilityChange(),
|
||||
])
|
||||
|
||||
// select a plane
|
||||
await u.doAndWaitForCmd(() => page.mouse.click(700, 200), 'edit_mode_enter')
|
||||
@ -79,8 +86,8 @@ test('Basic sketch', async ({ page }) => {
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
|
||||
const startAt = '[9.94, -13.41]'
|
||||
const tenish = '10.03'
|
||||
const startAt = '[10.97, -14.79]'
|
||||
const tenish = '11.07'
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
@ -98,7 +105,7 @@ test('Basic sketch', async ({ page }) => {
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${tenish}, 0], %)
|
||||
|> line([0, ${tenish}], %)
|
||||
|> line([-19.97, 0], %)`)
|
||||
|> line([-22.04, 0], %)`)
|
||||
|
||||
// deselect line tool
|
||||
await u.doAndWaitForCmd(
|
||||
@ -126,7 +133,7 @@ test('Basic sketch', async ({ page }) => {
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line({ to: [10.03, 0], tag: 'seg01' }, %)
|
||||
|> line({ to: [${tenish}, 0], tag: 'seg01' }, %)
|
||||
|> line([0, ${tenish}], %)
|
||||
|> angledLine([180, segLen('seg01', %)], %)`)
|
||||
})
|
||||
@ -309,8 +316,8 @@ test('Can create sketches on all planes and their back sides', async ({
|
||||
plane = 'XY',
|
||||
sign = ''
|
||||
) => `const part001 = startSketchOn('${plane}')
|
||||
|> startProfileAt([${sign}3.97, -5.36], %)
|
||||
|> line([${sign}4.01, 0], %)`
|
||||
|> startProfileAt([${sign}6.88, -9.29], %)
|
||||
|> line([${sign}6.95, 0], %)`
|
||||
await TestSinglePlane({
|
||||
viewCmd: camCmd,
|
||||
expectedCode: codeTemplate('XY'),
|
||||
@ -454,6 +461,11 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
await u.openDebugPanel()
|
||||
await u.waitForDefaultPlanesVisibilityChange()
|
||||
|
||||
const xAxisClick = () => page.mouse.click(700, 250)
|
||||
const emptySpaceClick = () => page.mouse.click(700, 300)
|
||||
const topHorzSegmentClick = () => page.mouse.click(700, 285)
|
||||
const bottomHorzSegmentClick = () => page.mouse.click(750, 393)
|
||||
|
||||
await u.clearCommandLogs()
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
await u.waitForDefaultPlanesVisibilityChange()
|
||||
@ -476,8 +488,9 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
|
||||
const startAt = '[9.94, -13.41]'
|
||||
const tenish = '10.03'
|
||||
const startAt = '[10.97, -14.79]'
|
||||
const tenish = '11.07'
|
||||
const twentyish = '22.04'
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
@ -495,7 +508,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${tenish}, 0], %)
|
||||
|> line([0, ${tenish}], %)
|
||||
|> line([-19.97, 0], %)`)
|
||||
|> line([-${twentyish}, 0], %)`)
|
||||
|
||||
// deselect line tool
|
||||
await u.doAndWaitForCmd(
|
||||
@ -504,7 +517,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
)
|
||||
|
||||
await u.closeDebugPanel()
|
||||
const hoverSequency = async () => {
|
||||
const selectionSequence = async () => {
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
|
||||
await page.mouse.move(startXPx + PUR * 15, 500 - PUR * 10)
|
||||
@ -519,20 +532,76 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 20) // mouse onto another line
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
|
||||
// now check clicking works including axis
|
||||
|
||||
// click a segment hold shift and click an axis, see that a relevant constraint is enabled
|
||||
await u.doAndWaitForCmd(topHorzSegmentClick, 'select_with_point', false)
|
||||
await page.keyboard.down('Shift')
|
||||
const absYButton = page.getByRole('button', { name: 'ABS Y' })
|
||||
await expect(absYButton).toBeDisabled()
|
||||
await u.doAndWaitForCmd(xAxisClick, 'select_with_point', false)
|
||||
await page.keyboard.up('Shift')
|
||||
await absYButton.and(page.locator(':not([disabled])')).waitFor()
|
||||
await expect(absYButton).not.toBeDisabled()
|
||||
|
||||
// clear selection by clicking on nothing
|
||||
await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false)
|
||||
|
||||
// same selection but click the axis first
|
||||
await u.doAndWaitForCmd(xAxisClick, 'select_with_point', false)
|
||||
await expect(absYButton).toBeDisabled()
|
||||
await page.keyboard.down('Shift')
|
||||
await u.doAndWaitForCmd(topHorzSegmentClick, 'select_with_point', false)
|
||||
await page.keyboard.up('Shift')
|
||||
await expect(absYButton).not.toBeDisabled()
|
||||
|
||||
// clear selection by clicking on nothing
|
||||
await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false)
|
||||
|
||||
// check the same selection again by putting cursor in code first then selecting axis
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.getByText(` |> line([-${twentyish}, 0], %)`).click(),
|
||||
'select_clear',
|
||||
false
|
||||
)
|
||||
await page.keyboard.down('Shift')
|
||||
await expect(absYButton).toBeDisabled()
|
||||
await u.doAndWaitForCmd(xAxisClick, 'select_with_point', false)
|
||||
await page.keyboard.up('Shift')
|
||||
await expect(absYButton).not.toBeDisabled()
|
||||
|
||||
// clear selection by clicking on nothing
|
||||
await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false)
|
||||
|
||||
// select segment in editor than another segment in scene and check there are two cursors
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.getByText(` |> line([-${twentyish}, 0], %)`).click(),
|
||||
'select_clear',
|
||||
false
|
||||
)
|
||||
await page.keyboard.down('Shift')
|
||||
await expect(page.locator('.cm-cursor')).toHaveCount(1)
|
||||
await u.doAndWaitForCmd(bottomHorzSegmentClick, 'select_with_point', false) // another segment, bottom one
|
||||
await page.keyboard.up('Shift')
|
||||
await expect(page.locator('.cm-cursor')).toHaveCount(2)
|
||||
|
||||
// clear selection by clicking on nothing
|
||||
await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false)
|
||||
}
|
||||
await hoverSequency()
|
||||
|
||||
await selectionSequence()
|
||||
|
||||
// hovering in fresh sketch worked, lets try exiting and re-entering
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.getByRole('button', { name: 'Exit Sketch' }).click(),
|
||||
'edit_mode_exit'
|
||||
)
|
||||
// wait for execution done
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
|
||||
// select a line
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.mouse.click(startXPx + PUR * 10, 500 - PUR * 20),
|
||||
'select_with_point'
|
||||
)
|
||||
await u.doAndWaitForCmd(topHorzSegmentClick, 'select_clear', false)
|
||||
|
||||
// enter sketch again
|
||||
await u.doAndWaitForCmd(
|
||||
@ -542,5 +611,5 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
)
|
||||
|
||||
// hover again and check it works
|
||||
await hoverSequency()
|
||||
await selectionSequence()
|
||||
})
|
||||
|
@ -210,11 +210,36 @@ const part001 = startSketchOn('-XZ')
|
||||
const downloadPromise = page.waitForEvent('download')
|
||||
await page.getByRole('button', { name: 'Export', exact: true }).click()
|
||||
const download = await downloadPromise
|
||||
const downloadLocationer = (extra = '') =>
|
||||
`./e2e/playwright/export-snapshots/${output.type}-${
|
||||
'storage' in output ? output.storage : ''
|
||||
}${extra}.${output.type}`
|
||||
const downloadLocation = downloadLocationer()
|
||||
const downloadLocation2 = downloadLocationer('-2')
|
||||
|
||||
if (output.type === 'gltf' && output.storage === 'standard') {
|
||||
// wait for second download
|
||||
const download2 = await page.waitForEvent('download')
|
||||
await download.saveAs(downloadLocation)
|
||||
await download2.saveAs(downloadLocation2)
|
||||
|
||||
// rewrite uri to reference our file name
|
||||
const fileContents = await fsp.readFile(downloadLocation, 'utf-8')
|
||||
const isJson = fileContents.includes('buffers')
|
||||
let contents = fileContents
|
||||
let reWriteLocation = downloadLocation
|
||||
let uri = downloadLocation2.split('/').pop()
|
||||
if (!isJson) {
|
||||
contents = await fsp.readFile(downloadLocation2, 'utf-8')
|
||||
reWriteLocation = downloadLocation2
|
||||
uri = downloadLocation.split('/').pop()
|
||||
}
|
||||
contents = contents.replace(/"uri": ".*"/g, `"uri": "${uri}"`)
|
||||
await fsp.writeFile(reWriteLocation, contents)
|
||||
} else {
|
||||
await download.saveAs(downloadLocation)
|
||||
}
|
||||
|
||||
const downloadLocation = `./e2e/playwright/export-snapshots/${
|
||||
output.type
|
||||
}-${'storage' in output ? output.storage : ''}.${output.type}`
|
||||
await download.saveAs(downloadLocation)
|
||||
if (output.type === 'step') {
|
||||
// stable timestamps for step files
|
||||
const fileContents = await fsp.readFile(downloadLocation, 'utf-8')
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { expect, Page } from '@playwright/test'
|
||||
import { EngineCommand } from '../../src/lang/std/engineConnection'
|
||||
import fsp from 'fs/promises'
|
||||
import pixelMatch from 'pixelmatch'
|
||||
import { PNG } from 'pngjs'
|
||||
|
||||
async function waitForPageLoad(page: Page) {
|
||||
// wait for 'Loading stream...' spinner
|
||||
@ -108,5 +111,46 @@ export function getUtils(page: Page) {
|
||||
await closeDebugPanel(page)
|
||||
}
|
||||
},
|
||||
doAndWaitForImageDiff: (fn: () => Promise<any>, diffCount = 200) =>
|
||||
new Promise(async (resolve) => {
|
||||
await page.screenshot({
|
||||
path: './e2e/playwright/temp1.png',
|
||||
fullPage: true,
|
||||
})
|
||||
await fn()
|
||||
const isImageDiff = async () => {
|
||||
await page.screenshot({
|
||||
path: './e2e/playwright/temp2.png',
|
||||
fullPage: true,
|
||||
})
|
||||
const screenshot1 = PNG.sync.read(
|
||||
await fsp.readFile('./e2e/playwright/temp1.png')
|
||||
)
|
||||
const screenshot2 = PNG.sync.read(
|
||||
await fsp.readFile('./e2e/playwright/temp2.png')
|
||||
)
|
||||
const actualDiffCount = pixelMatch(
|
||||
screenshot1.data,
|
||||
screenshot2.data,
|
||||
null,
|
||||
screenshot1.width,
|
||||
screenshot2.height
|
||||
)
|
||||
return actualDiffCount > diffCount
|
||||
}
|
||||
|
||||
// run isImageDiff every 50ms until it returns true or 5 seconds have passed (100 times)
|
||||
let count = 0
|
||||
const interval = setInterval(async () => {
|
||||
count++
|
||||
if (await isImageDiff()) {
|
||||
clearInterval(interval)
|
||||
resolve(true)
|
||||
} else if (count > 100) {
|
||||
clearInterval(interval)
|
||||
resolve(false)
|
||||
}
|
||||
}, 50)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
10
package.json
10
package.json
@ -11,7 +11,7 @@
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@kittycad/lib": "^0.0.46",
|
||||
"@lezer/javascript": "^1.4.7",
|
||||
"@lezer/javascript": "^1.4.9",
|
||||
"@open-rpc/client-js": "^1.8.1",
|
||||
"@react-hook/resize-observer": "^1.2.6",
|
||||
"@replit/codemirror-interact": "^6.3.0",
|
||||
@ -22,7 +22,7 @@
|
||||
"@testing-library/user-event": "^14.5.1",
|
||||
"@ts-stack/markdown": "^1.5.0",
|
||||
"@types/node": "^16.7.13",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react": "^18.2.41",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@uiw/react-codemirror": "^4.21.20",
|
||||
"@xstate/inspect": "^0.8.0",
|
||||
@ -109,7 +109,9 @@
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/debounce-promise": "^3.1.8",
|
||||
"@types/isomorphic-fetch": "^0.0.36",
|
||||
"@types/react-modal": "^3.16.0",
|
||||
"@types/pixelmatch": "^5.2.6",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
"@types/react-modal": "^3.16.3",
|
||||
"@types/uuid": "^9.0.4",
|
||||
"@types/wait-on": "^5.3.4",
|
||||
"@types/wicg-file-system-access": "^2020.9.6",
|
||||
@ -127,6 +129,8 @@
|
||||
"eslint-plugin-css-modules": "^2.12.0",
|
||||
"happy-dom": "^10.8.0",
|
||||
"husky": "^8.0.3",
|
||||
"pixelmatch": "^5.3.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^2.8.0",
|
||||
"setimmediate": "^1.0.5",
|
||||
|
@ -32,14 +32,14 @@ export default defineConfig({
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
{
|
||||
name: 'Google Chrome',
|
||||
use: { ...devices['Desktop Chrome'], channel: 'chrome' }, // or 'chrome-beta'
|
||||
},
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
// {
|
||||
// name: 'firefox',
|
||||
// use: { ...devices['Desktop Firefox'] },
|
||||
|
@ -109,6 +109,21 @@ export const Toolbar = () => {
|
||||
eventName.includes('Make segment') ||
|
||||
eventName.includes('Constrain')
|
||||
)
|
||||
.sort((a, b) => {
|
||||
const aisEnabled = state.nextEvents
|
||||
.filter((event) => state.can(event as any))
|
||||
.includes(a)
|
||||
const bIsEnabled = state.nextEvents
|
||||
.filter((event) => state.can(event as any))
|
||||
.includes(b)
|
||||
if (aisEnabled && !bIsEnabled) {
|
||||
return -1
|
||||
}
|
||||
if (!aisEnabled && bIsEnabled) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
.map((eventName) => (
|
||||
<button
|
||||
key={eventName}
|
||||
|
@ -31,13 +31,17 @@ import {
|
||||
} from 'lang/std/sketch'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
|
||||
import { applyConstraintAngleBetween } from './Toolbar/SetAngleBetween'
|
||||
import {
|
||||
angleBetweenInfo,
|
||||
applyConstraintAngleBetween,
|
||||
} from './Toolbar/SetAngleBetween'
|
||||
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { pathMapToSelections } from 'lang/util'
|
||||
import { useStore } from 'useStore'
|
||||
import { handleSelectionBatch, handleSelectionWithShift } from 'lib/selections'
|
||||
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
||||
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -262,17 +266,62 @@ 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
|
||||
if (!editorView) return {}
|
||||
if (setSelections.selectionType === 'mirrorCodeMirrorSelections')
|
||||
return { selectionRanges: setSelections.selection }
|
||||
else if (setSelections.selectionType === 'otherSelection')
|
||||
else if (setSelections.selectionType === 'otherSelection') {
|
||||
// TODO KittyCAD/engine/issues/1620: send axis highlight when it's working (if that's what we settle on)
|
||||
// const axisAddCmd: EngineCommand = {
|
||||
// type: 'modeling_cmd_req',
|
||||
// cmd: {
|
||||
// type: 'highlight_set_entities',
|
||||
// entities: [
|
||||
// setSelections.selection === 'x-axis'
|
||||
// ? X_AXIS_UUID
|
||||
// : Y_AXIS_UUID,
|
||||
// ],
|
||||
// },
|
||||
// cmd_id: uuidv4(),
|
||||
// }
|
||||
|
||||
// if (!isShiftDown) {
|
||||
// engineCommandManager
|
||||
// .sendSceneCommand({
|
||||
// type: 'modeling_cmd_req',
|
||||
// cmd: {
|
||||
// type: 'select_clear',
|
||||
// },
|
||||
// cmd_id: uuidv4(),
|
||||
// })
|
||||
// .then(() => {
|
||||
// engineCommandManager.sendSceneCommand(axisAddCmd)
|
||||
// })
|
||||
// } else {
|
||||
// engineCommandManager.sendSceneCommand(axisAddCmd)
|
||||
// }
|
||||
|
||||
const {
|
||||
codeMirrorSelection,
|
||||
selectionRangeTypeMap,
|
||||
otherSelections,
|
||||
} = handleSelectionWithShift({
|
||||
otherSelection: setSelections.selection,
|
||||
currentSelections: selectionRanges,
|
||||
isShiftDown,
|
||||
})
|
||||
setTimeout(() => {
|
||||
editorView.dispatch({
|
||||
selection: codeMirrorSelection,
|
||||
})
|
||||
})
|
||||
return {
|
||||
selectionRangeTypeMap,
|
||||
selectionRanges: {
|
||||
...selectionRanges,
|
||||
otherSelections: [setSelections.selection],
|
||||
codeBasedSelections: selectionRanges.codeBasedSelections,
|
||||
otherSelections,
|
||||
},
|
||||
}
|
||||
else if (!editorView) return {}
|
||||
else if (setSelections.selectionType === 'singleCodeCursor') {
|
||||
} else if (setSelections.selectionType === 'singleCodeCursor') {
|
||||
// This DOES NOT set the `selectionRanges` in xstate context
|
||||
// instead it updates/dispatches to the editor, which in turn updates the xstate context
|
||||
// I've found this the best way to deal with the editor without causing an infinite loop
|
||||
@ -280,12 +329,16 @@ export const ModelingMachineProvider = ({
|
||||
// because we want to respect the user manually placing the cursor too.
|
||||
|
||||
// for more details on how selections see `src/lib/selections.ts`.
|
||||
const { codeMirrorSelection, selectionRangeTypeMap } =
|
||||
handleSelectionWithShift({
|
||||
codeSelection: setSelections.selection,
|
||||
currestSelections: selectionRanges,
|
||||
isShiftDown,
|
||||
})
|
||||
|
||||
const {
|
||||
codeMirrorSelection,
|
||||
selectionRangeTypeMap,
|
||||
otherSelections,
|
||||
} = handleSelectionWithShift({
|
||||
codeSelection: setSelections.selection,
|
||||
currentSelections: selectionRanges,
|
||||
isShiftDown,
|
||||
})
|
||||
if (codeMirrorSelection) {
|
||||
setTimeout(() => {
|
||||
editorView.dispatch({
|
||||
@ -293,7 +346,22 @@ export const ModelingMachineProvider = ({
|
||||
})
|
||||
})
|
||||
}
|
||||
return { selectionRangeTypeMap }
|
||||
if (!setSelections.selection) {
|
||||
return {
|
||||
selectionRangeTypeMap,
|
||||
selectionRanges: {
|
||||
codeBasedSelections: selectionRanges.codeBasedSelections,
|
||||
otherSelections,
|
||||
},
|
||||
}
|
||||
}
|
||||
return {
|
||||
selectionRangeTypeMap,
|
||||
selectionRanges: {
|
||||
codeBasedSelections: selectionRanges.codeBasedSelections,
|
||||
otherSelections,
|
||||
},
|
||||
}
|
||||
}
|
||||
// This DOES NOT set the `selectionRanges` in xstate context
|
||||
// same as comment above
|
||||
@ -363,10 +431,16 @@ export const ModelingMachineProvider = ({
|
||||
}
|
||||
},
|
||||
'Get angle info': async ({ selectionRanges }): Promise<SetSelections> => {
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
await applyConstraintAngleBetween({
|
||||
selectionRanges,
|
||||
})
|
||||
const { modifiedAst, pathToNodeMap } = await (angleBetweenInfo({
|
||||
selectionRanges,
|
||||
}).enabled
|
||||
? applyConstraintAngleBetween({
|
||||
selectionRanges,
|
||||
})
|
||||
: applyConstraintAngleLength({
|
||||
selectionRanges,
|
||||
angleOrLength: 'setAngle',
|
||||
}))
|
||||
await kclManager.updateAst(modifiedAst, true)
|
||||
return {
|
||||
selectionType: 'completeSelection',
|
||||
@ -409,6 +483,40 @@ export const ModelingMachineProvider = ({
|
||||
),
|
||||
}
|
||||
},
|
||||
'Get ABS X info': async ({ selectionRanges }): Promise<SetSelections> => {
|
||||
const { modifiedAst, pathToNodeMap } = await applyConstraintAbsDistance(
|
||||
{
|
||||
constraint: 'xAbs',
|
||||
selectionRanges,
|
||||
}
|
||||
)
|
||||
await kclManager.updateAst(modifiedAst, true)
|
||||
return {
|
||||
selectionType: 'completeSelection',
|
||||
selection: pathMapToSelections(
|
||||
kclManager.ast,
|
||||
selectionRanges,
|
||||
pathToNodeMap
|
||||
),
|
||||
}
|
||||
},
|
||||
'Get ABS Y info': async ({ selectionRanges }): Promise<SetSelections> => {
|
||||
const { modifiedAst, pathToNodeMap } = await applyConstraintAbsDistance(
|
||||
{
|
||||
constraint: 'yAbs',
|
||||
selectionRanges,
|
||||
}
|
||||
)
|
||||
await kclManager.updateAst(modifiedAst, true)
|
||||
return {
|
||||
selectionType: 'completeSelection',
|
||||
selection: pathMapToSelections(
|
||||
kclManager.ast,
|
||||
selectionRanges,
|
||||
pathToNodeMap
|
||||
),
|
||||
}
|
||||
},
|
||||
},
|
||||
devTools: true,
|
||||
})
|
||||
|
@ -42,13 +42,19 @@ export const TextEditor = ({
|
||||
}: {
|
||||
theme: Themes.Light | Themes.Dark
|
||||
}) => {
|
||||
const { editorView, isLSPServerReady, setEditorView, setIsLSPServerReady } =
|
||||
useStore((s) => ({
|
||||
editorView: s.editorView,
|
||||
isLSPServerReady: s.isLSPServerReady,
|
||||
setEditorView: s.setEditorView,
|
||||
setIsLSPServerReady: s.setIsLSPServerReady,
|
||||
}))
|
||||
const {
|
||||
editorView,
|
||||
isLSPServerReady,
|
||||
setEditorView,
|
||||
setIsLSPServerReady,
|
||||
isShiftDown,
|
||||
} = useStore((s) => ({
|
||||
editorView: s.editorView,
|
||||
isLSPServerReady: s.isLSPServerReady,
|
||||
setEditorView: s.setEditorView,
|
||||
setIsLSPServerReady: s.setIsLSPServerReady,
|
||||
isShiftDown: s.isShiftDown,
|
||||
}))
|
||||
const { code, errors } = useKclContext()
|
||||
|
||||
const {
|
||||
@ -113,6 +119,7 @@ export const TextEditor = ({
|
||||
codeMirrorRanges: viewUpdate.state.selection.ranges,
|
||||
selectionRanges,
|
||||
selectionRangeTypeMap,
|
||||
isShiftDown,
|
||||
})
|
||||
if (!eventInfo) return
|
||||
|
||||
|
@ -59,6 +59,7 @@ export function angleBetweenInfo({
|
||||
)
|
||||
|
||||
const _enableEqual =
|
||||
selectionRanges.otherSelections.length === 0 &&
|
||||
secondaryVarDecs.length === 1 &&
|
||||
isAllTooltips &&
|
||||
isOthersLinkedToPrimary &&
|
||||
|
@ -25,7 +25,7 @@ import { kclManager } from 'lang/KclSinglton'
|
||||
|
||||
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
|
||||
|
||||
export function setAngleLengthInfo({
|
||||
export function angleLengthInfo({
|
||||
selectionRanges,
|
||||
angleOrLength = 'setLength',
|
||||
}: {
|
||||
@ -50,7 +50,10 @@ export function setAngleLengthInfo({
|
||||
kclManager.ast,
|
||||
angleOrLength
|
||||
)
|
||||
const enabled = isAllTooltips && transforms.every(Boolean)
|
||||
const enabled =
|
||||
selectionRanges.codeBasedSelections.length <= 1 &&
|
||||
isAllTooltips &&
|
||||
transforms.every(Boolean)
|
||||
return { enabled, transforms }
|
||||
}
|
||||
|
||||
@ -64,7 +67,7 @@ export async function applyConstraintAngleLength({
|
||||
modifiedAst: Program
|
||||
pathToNodeMap: PathToNodeMap
|
||||
}> {
|
||||
const { transforms } = setAngleLengthInfo({ selectionRanges, angleOrLength })
|
||||
const { transforms } = angleLengthInfo({ selectionRanges, angleOrLength })
|
||||
const { valueUsedInTransform } = transformAstSketchLines({
|
||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||
selectionRanges,
|
||||
|
@ -334,10 +334,7 @@ const setAbsDistanceForAngleLineCreateNode =
|
||||
): TransformInfo['createNode'] =>
|
||||
({ tag, forceValueUsedInTransform, varValA }) => {
|
||||
return (args, referencedSegment) => {
|
||||
const valueUsedInTransform = roundOff(
|
||||
getArgLiteralVal(args?.[1]) - (referencedSegment?.to?.[index] || 0),
|
||||
2
|
||||
)
|
||||
const valueUsedInTransform = roundOff(getArgLiteralVal(args?.[1]), 2)
|
||||
const val =
|
||||
(forceValueUsedInTransform as BinaryPart) ||
|
||||
createLiteral(valueUsedInTransform)
|
||||
|
@ -34,8 +34,8 @@ export const browserSaveFile = async (blob: Blob, suggestedName: string) => {
|
||||
// Fail silently if the user has simply canceled the dialog.
|
||||
if (err.name !== 'AbortError') {
|
||||
console.error(err.name, err.message)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
// Fallback if the File System Access API is not supported…
|
||||
|
@ -34,7 +34,7 @@ export async function exportSave(data: ArrayBuffer) {
|
||||
// Create a new blob.
|
||||
const blob = new Blob([new Uint8Array(file.contents)])
|
||||
// Save the file.
|
||||
browserSaveFile(blob, file.name)
|
||||
await browserSaveFile(blob, file.name)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
@ -8,6 +8,9 @@ import { kclManager } from 'lang/KclSinglton'
|
||||
import { SelectionRange } from '@uiw/react-codemirror'
|
||||
import { isOverlap } from 'lib/utils'
|
||||
|
||||
export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b'
|
||||
export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
|
||||
|
||||
/*
|
||||
How selections work is complex due to the nature that we rely on the engine
|
||||
to tell what has been selected after we send a click command. But than the
|
||||
@ -110,6 +113,15 @@ export async function getEventForSelectWithPoint(
|
||||
data: { selectionType: 'singleCodeCursor' },
|
||||
}
|
||||
}
|
||||
if ([X_AXIS_UUID, Y_AXIS_UUID].includes(data.entity_id)) {
|
||||
return {
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
selectionType: 'otherSelection',
|
||||
selection: X_AXIS_UUID === data.entity_id ? 'x-axis' : 'y-axis',
|
||||
},
|
||||
}
|
||||
}
|
||||
const sourceRange = engineCommandManager.artifactMap[data.entity_id]?.range
|
||||
if (engineCommandManager.artifactMap[data.entity_id]) {
|
||||
return {
|
||||
@ -164,6 +176,7 @@ export function handleSelectionBatch({
|
||||
}): {
|
||||
selectionRangeTypeMap: SelectionRangeTypeMap
|
||||
codeMirrorSelection?: EditorSelection
|
||||
otherSelections: Axis[]
|
||||
} {
|
||||
const ranges: ReturnType<typeof EditorSelection.cursor>[] = []
|
||||
const selectionRangeTypeMap: SelectionRangeTypeMap = {}
|
||||
@ -180,43 +193,74 @@ export function handleSelectionBatch({
|
||||
ranges,
|
||||
selections.codeBasedSelections.length - 1
|
||||
),
|
||||
otherSelections: selections.otherSelections,
|
||||
}
|
||||
|
||||
return {
|
||||
selectionRangeTypeMap,
|
||||
otherSelections: selections.otherSelections,
|
||||
}
|
||||
}
|
||||
|
||||
export function handleSelectionWithShift({
|
||||
codeSelection,
|
||||
currestSelections,
|
||||
otherSelection,
|
||||
currentSelections,
|
||||
isShiftDown,
|
||||
}: {
|
||||
codeSelection?: Selection
|
||||
currestSelections: Selections
|
||||
otherSelection?: Axis
|
||||
currentSelections: Selections
|
||||
isShiftDown: boolean
|
||||
}): {
|
||||
selectionRangeTypeMap: SelectionRangeTypeMap
|
||||
otherSelections: Axis[]
|
||||
codeMirrorSelection?: EditorSelection
|
||||
} {
|
||||
const code = kclManager.code
|
||||
if (!codeSelection)
|
||||
if (codeSelection && otherSelection) {
|
||||
throw new Error('cannot have both code and other selection')
|
||||
}
|
||||
if (!codeSelection && !otherSelection) {
|
||||
return handleSelectionBatch({
|
||||
selections: {
|
||||
otherSelections: currestSelections.otherSelections,
|
||||
otherSelections: [],
|
||||
codeBasedSelections: [
|
||||
{
|
||||
range: [0, code.length ? code.length - 1 : 0],
|
||||
range: [0, code.length ? code.length : 0],
|
||||
type: 'default',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
if (otherSelection) {
|
||||
console.log('otherSelection in handleSelectionWithShift', otherSelection)
|
||||
return handleSelectionBatch({
|
||||
selections: {
|
||||
codeBasedSelections: isShiftDown
|
||||
? currentSelections.codeBasedSelections
|
||||
: [
|
||||
{
|
||||
range: [0, code.length ? code.length : 0],
|
||||
type: 'default',
|
||||
},
|
||||
],
|
||||
otherSelections: [otherSelection],
|
||||
},
|
||||
})
|
||||
}
|
||||
const isEndOfFileDumbySelection =
|
||||
currentSelections.codeBasedSelections.length === 1 &&
|
||||
currentSelections.codeBasedSelections[0].range[0] === kclManager.code.length
|
||||
const newCodeBasedSelections = !isShiftDown
|
||||
? [codeSelection!]
|
||||
: isEndOfFileDumbySelection
|
||||
? [codeSelection!]
|
||||
: [...currentSelections.codeBasedSelections, codeSelection!]
|
||||
const selections: Selections = {
|
||||
...currestSelections,
|
||||
codeBasedSelections: isShiftDown
|
||||
? [...currestSelections.codeBasedSelections, codeSelection]
|
||||
: [codeSelection],
|
||||
otherSelections: isShiftDown ? currentSelections.otherSelections : [],
|
||||
codeBasedSelections: newCodeBasedSelections,
|
||||
}
|
||||
return handleSelectionBatch({ selections })
|
||||
}
|
||||
@ -227,10 +271,12 @@ export function processCodeMirrorRanges({
|
||||
codeMirrorRanges,
|
||||
selectionRanges,
|
||||
selectionRangeTypeMap,
|
||||
isShiftDown,
|
||||
}: {
|
||||
codeMirrorRanges: readonly SelectionRange[]
|
||||
selectionRanges: Selections
|
||||
selectionRangeTypeMap: SelectionRangeTypeMap
|
||||
isShiftDown: boolean
|
||||
}): null | {
|
||||
modelingEvent: ModelingMachineEvent
|
||||
engineEvents: Models['WebSocketRequest_type'][]
|
||||
@ -291,7 +337,7 @@ export function processCodeMirrorRanges({
|
||||
data: {
|
||||
selectionType: 'mirrorCodeMirrorSelections',
|
||||
selection: {
|
||||
...selectionRanges,
|
||||
otherSelections: isShiftDown ? selectionRanges.otherSelections : [],
|
||||
codeBasedSelections,
|
||||
},
|
||||
},
|
||||
@ -300,7 +346,7 @@ export function processCodeMirrorRanges({
|
||||
}
|
||||
}
|
||||
|
||||
export function resetAndSetEngineEntitySelectionCmds(
|
||||
function resetAndSetEngineEntitySelectionCmds(
|
||||
selections: SelectionToEngine[]
|
||||
): Models['WebSocketRequest_type'][] {
|
||||
if (!engineCommandManager.engineConnection?.isReady()) {
|
||||
|
File diff suppressed because one or more lines are too long
@ -5,11 +5,15 @@
|
||||
'@@xstate/typegen': true;
|
||||
internalEvents: {
|
||||
"": { type: "" };
|
||||
"done.invoke.get-abs-x-info": { type: "done.invoke.get-abs-x-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
|
||||
"done.invoke.get-abs-y-info": { type: "done.invoke.get-abs-y-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
|
||||
"done.invoke.get-angle-info": { type: "done.invoke.get-angle-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
|
||||
"done.invoke.get-horizontal-info": { type: "done.invoke.get-horizontal-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
|
||||
"done.invoke.get-length-info": { type: "done.invoke.get-length-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
|
||||
"done.invoke.get-perpendicular-distance-info": { type: "done.invoke.get-perpendicular-distance-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
|
||||
"done.invoke.get-vertical-info": { type: "done.invoke.get-vertical-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
|
||||
"error.platform.get-abs-x-info": { type: "error.platform.get-abs-x-info"; data: unknown };
|
||||
"error.platform.get-abs-y-info": { type: "error.platform.get-abs-y-info"; data: unknown };
|
||||
"error.platform.get-angle-info": { type: "error.platform.get-angle-info"; data: unknown };
|
||||
"error.platform.get-horizontal-info": { type: "error.platform.get-horizontal-info"; data: unknown };
|
||||
"error.platform.get-length-info": { type: "error.platform.get-length-info"; data: unknown };
|
||||
@ -19,7 +23,9 @@
|
||||
"xstate.stop": { type: "xstate.stop" };
|
||||
};
|
||||
invokeSrcNameMap: {
|
||||
"Get angle info": "done.invoke.get-angle-info";
|
||||
"Get ABS X info": "done.invoke.get-abs-x-info";
|
||||
"Get ABS Y info": "done.invoke.get-abs-y-info";
|
||||
"Get angle info": "done.invoke.get-angle-info";
|
||||
"Get horizontal info": "done.invoke.get-horizontal-info";
|
||||
"Get length info": "done.invoke.get-length-info";
|
||||
"Get perpendicular distance info": "done.invoke.get-perpendicular-distance-info";
|
||||
@ -29,7 +35,7 @@
|
||||
actions: "AST add line segment" | "AST start new sketch" | "Modify AST" | "Set selection" | "Update code selection cursors" | "create path" | "set tool" | "show default planes" | "sketch exit execute" | "toast extrude failed";
|
||||
delays: never;
|
||||
guards: "Selection contains axis" | "Selection contains edge" | "Selection contains face" | "Selection contains line" | "Selection contains point" | "Selection is not empty" | "Selection is one face";
|
||||
services: "Get angle info" | "Get horizontal info" | "Get length info" | "Get perpendicular distance info" | "Get vertical info";
|
||||
services: "Get ABS X info" | "Get ABS Y info" | "Get angle info" | "Get horizontal info" | "Get length info" | "Get perpendicular distance info" | "Get vertical info";
|
||||
};
|
||||
eventsCausingActions: {
|
||||
"AST add line segment": "Add point";
|
||||
@ -42,19 +48,21 @@
|
||||
"Constrain horizontally align": "Constrain horizontally align";
|
||||
"Constrain parallel": "Constrain parallel";
|
||||
"Constrain remove constraints": "Constrain remove constraints";
|
||||
"Constrain snap to X": "Constrain snap to X";
|
||||
"Constrain snap to Y": "Constrain snap to Y";
|
||||
"Constrain vertically align": "Constrain vertically align";
|
||||
"Make selection horizontal": "Make segment horizontal";
|
||||
"Make selection vertical": "Make segment vertical";
|
||||
"Modify AST": "Complete line";
|
||||
"Remove from code-based selection": "Deselect edge" | "Deselect face" | "Deselect point";
|
||||
"Remove from other selection": "Deselect axis";
|
||||
"Set selection": "Set selection" | "done.invoke.get-angle-info" | "done.invoke.get-horizontal-info" | "done.invoke.get-length-info" | "done.invoke.get-perpendicular-distance-info" | "done.invoke.get-vertical-info";
|
||||
"Set selection": "Set selection" | "done.invoke.get-abs-x-info" | "done.invoke.get-abs-y-info" | "done.invoke.get-angle-info" | "done.invoke.get-horizontal-info" | "done.invoke.get-length-info" | "done.invoke.get-perpendicular-distance-info" | "done.invoke.get-vertical-info";
|
||||
"Update code selection cursors": "Complete line" | "Deselect all" | "Deselect axis" | "Deselect edge" | "Deselect face" | "Deselect point" | "Deselect segment" | "Select edge" | "Select face" | "Select point" | "Select segment";
|
||||
"create path": "Select default plane";
|
||||
"default_camera_disable_sketch_mode": "Cancel";
|
||||
"edit mode enter": "Enter sketch" | "Re-execute";
|
||||
"edit_mode_exit": "Cancel";
|
||||
"equip select": "CancelSketch" | "Constrain equal length" | "Constrain horizontally align" | "Constrain parallel" | "Constrain remove constraints" | "Constrain vertically align" | "Deselect point" | "Deselect segment" | "Enter sketch" | "Make segment horizontal" | "Make segment vertical" | "Re-execute" | "Select default plane" | "Select point" | "Select segment" | "Set selection" | "done.invoke.get-angle-info" | "done.invoke.get-horizontal-info" | "done.invoke.get-length-info" | "done.invoke.get-perpendicular-distance-info" | "done.invoke.get-vertical-info" | "error.platform.get-angle-info" | "error.platform.get-horizontal-info" | "error.platform.get-length-info" | "error.platform.get-perpendicular-distance-info" | "error.platform.get-vertical-info";
|
||||
"equip select": "CancelSketch" | "Constrain equal length" | "Constrain horizontally align" | "Constrain parallel" | "Constrain remove constraints" | "Constrain snap to X" | "Constrain snap to Y" | "Constrain vertically align" | "Deselect point" | "Deselect segment" | "Enter sketch" | "Make segment horizontal" | "Make segment vertical" | "Re-execute" | "Select default plane" | "Select point" | "Select segment" | "Set selection" | "done.invoke.get-abs-x-info" | "done.invoke.get-abs-y-info" | "done.invoke.get-angle-info" | "done.invoke.get-horizontal-info" | "done.invoke.get-length-info" | "done.invoke.get-perpendicular-distance-info" | "done.invoke.get-vertical-info" | "error.platform.get-abs-x-info" | "error.platform.get-abs-y-info" | "error.platform.get-angle-info" | "error.platform.get-horizontal-info" | "error.platform.get-length-info" | "error.platform.get-perpendicular-distance-info" | "error.platform.get-vertical-info";
|
||||
"hide default planes": "Cancel" | "Select default plane" | "xstate.stop";
|
||||
"reset sketch metadata": "Cancel" | "Select default plane";
|
||||
"set default plane id": "Select default plane";
|
||||
@ -73,6 +81,8 @@
|
||||
};
|
||||
eventsCausingGuards: {
|
||||
"Can canstrain parallel": "Constrain parallel";
|
||||
"Can constrain ABS X": "Constrain ABS X";
|
||||
"Can constrain ABS Y": "Constrain ABS Y";
|
||||
"Can constrain angle": "Constrain angle";
|
||||
"Can constrain equal length": "Constrain equal length";
|
||||
"Can constrain horizontal distance": "Constrain horizontal distance";
|
||||
@ -80,6 +90,8 @@
|
||||
"Can constrain length": "Constrain length";
|
||||
"Can constrain perpendicular distance": "Constrain perpendicular distance";
|
||||
"Can constrain remove constraints": "Constrain remove constraints";
|
||||
"Can constrain snap to X": "Constrain snap to X";
|
||||
"Can constrain snap to Y": "Constrain snap to Y";
|
||||
"Can constrain vertical distance": "Constrain vertical distance";
|
||||
"Can constrain vertically align": "Constrain vertically align";
|
||||
"Can make selection horizontal": "Make segment horizontal";
|
||||
@ -98,13 +110,15 @@
|
||||
"is editing existing sketch": "";
|
||||
};
|
||||
eventsCausingServices: {
|
||||
"Get angle info": "Constrain angle";
|
||||
"Get ABS X info": "Constrain ABS X";
|
||||
"Get ABS Y info": "Constrain ABS Y";
|
||||
"Get angle info": "Constrain angle";
|
||||
"Get horizontal info": "Constrain horizontal distance";
|
||||
"Get length info": "Constrain length";
|
||||
"Get perpendicular distance info": "Constrain perpendicular distance";
|
||||
"Get vertical info": "Constrain vertical distance";
|
||||
};
|
||||
matchesStates: "Sketch" | "Sketch no face" | "Sketch.Await angle info" | "Sketch.Await horizontal distance info" | "Sketch.Await length info" | "Sketch.Await perpendicular distance info" | "Sketch.Await vertical distance info" | "Sketch.Line Tool" | "Sketch.Line Tool.Done" | "Sketch.Line Tool.Init" | "Sketch.Line Tool.No Points" | "Sketch.Line Tool.Point Added" | "Sketch.Line Tool.Segment Added" | "Sketch.Move Tool" | "Sketch.Move Tool.Move init" | "Sketch.Move Tool.Move with execute" | "Sketch.Move Tool.Move without re-execute" | "Sketch.Move Tool.No move" | "Sketch.SketchIdle" | "awaiting selection" | "checking selection" | "idle" | { "Sketch"?: "Await angle info" | "Await horizontal distance info" | "Await length info" | "Await perpendicular distance info" | "Await vertical distance info" | "Line Tool" | "Move Tool" | "SketchIdle" | { "Line Tool"?: "Done" | "Init" | "No Points" | "Point Added" | "Segment Added";
|
||||
matchesStates: "Sketch" | "Sketch no face" | "Sketch.Await ABS X info" | "Sketch.Await ABS Y info" | "Sketch.Await angle info" | "Sketch.Await horizontal distance info" | "Sketch.Await length info" | "Sketch.Await perpendicular distance info" | "Sketch.Await vertical distance info" | "Sketch.Line Tool" | "Sketch.Line Tool.Done" | "Sketch.Line Tool.Init" | "Sketch.Line Tool.No Points" | "Sketch.Line Tool.Point Added" | "Sketch.Line Tool.Segment Added" | "Sketch.Move Tool" | "Sketch.Move Tool.Move init" | "Sketch.Move Tool.Move with execute" | "Sketch.Move Tool.Move without re-execute" | "Sketch.Move Tool.No move" | "Sketch.SketchIdle" | "awaiting selection" | "checking selection" | "idle" | { "Sketch"?: "Await ABS X info" | "Await ABS Y info" | "Await angle info" | "Await horizontal distance info" | "Await length info" | "Await perpendicular distance info" | "Await vertical distance info" | "Line Tool" | "Move Tool" | "SketchIdle" | { "Line Tool"?: "Done" | "Init" | "No Points" | "Point Added" | "Segment Added";
|
||||
"Move Tool"?: "Move init" | "Move with execute" | "Move without re-execute" | "No move"; }; };
|
||||
tags: never;
|
||||
}
|
||||
|
10
src/wasm-lib/Cargo.lock
generated
10
src/wasm-lib/Cargo.lock
generated
@ -822,6 +822,16 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "execution-plan"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"kittycad",
|
||||
"serde",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "expectorate"
|
||||
version = "1.1.0"
|
||||
|
@ -54,6 +54,7 @@ members = [
|
||||
"derive-docs",
|
||||
"kcl",
|
||||
"kcl-macros",
|
||||
"execution-plan",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
|
13
src/wasm-lib/execution-plan/Cargo.toml
Normal file
13
src/wasm-lib/execution-plan/Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "execution-plan"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
rust-version = "1.73"
|
||||
description = "A DSL for composing KittyCAD API queries"
|
||||
|
||||
[dependencies]
|
||||
bytes = "1.5"
|
||||
kittycad = { workspace = true, features = ["requests"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
thiserror = "1"
|
27
src/wasm-lib/execution-plan/src/composite.rs
Normal file
27
src/wasm-lib/execution-plan/src/composite.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use crate::{ExecutionError, NumericValue, Value};
|
||||
|
||||
/// Types that can be written to or read from KCEP program memory,
|
||||
/// but require multiple values to store.
|
||||
/// They get laid out into multiple consecutive memory addresses.
|
||||
pub trait Composite<const SIZE: usize>: Sized {
|
||||
/// Store the value in memory.
|
||||
fn into_parts(self) -> [Value; SIZE];
|
||||
/// Read the value from memory.
|
||||
fn from_parts(values: [Value; SIZE]) -> Result<Self, ExecutionError>;
|
||||
}
|
||||
|
||||
impl Composite<3> for kittycad::types::Point3D {
|
||||
fn into_parts(self) -> [Value; 3] {
|
||||
[self.x, self.y, self.z]
|
||||
.map(NumericValue::Float)
|
||||
.map(Value::NumericValue)
|
||||
}
|
||||
|
||||
fn from_parts(values: [Value; 3]) -> Result<Self, ExecutionError> {
|
||||
let [x, y, z] = values;
|
||||
let x = x.try_into()?;
|
||||
let y = y.try_into()?;
|
||||
let z = z.try_into()?;
|
||||
Ok(Self { x, y, z })
|
||||
}
|
||||
}
|
288
src/wasm-lib/execution-plan/src/lib.rs
Normal file
288
src/wasm-lib/execution-plan/src/lib.rs
Normal file
@ -0,0 +1,288 @@
|
||||
//! A KittyCAD execution plan (KCEP) is a list of
|
||||
//! - KittyCAD API requests to make
|
||||
//! - Values to send in API requests
|
||||
//! - Values to assign from API responses
|
||||
//! - Computation to perform on values
|
||||
//! You can think of it as a domain-specific language for making KittyCAD API calls and using
|
||||
//! the results to make other API calls.
|
||||
|
||||
use std::{collections::HashMap, fmt};
|
||||
|
||||
use composite::Composite;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod composite;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// KCEP's program memory. A flat, linear list of values.
|
||||
#[derive(Default, Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct Memory(HashMap<usize, Value>);
|
||||
|
||||
/// An address in KCEP's program memory.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct Address(usize);
|
||||
|
||||
impl fmt::Display for Address {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<usize> for Address {
|
||||
fn from(value: usize) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Memory {
|
||||
/// Get a value from KCEP's program memory.
|
||||
pub fn get(&self, addr: &Address) -> Option<&Value> {
|
||||
self.0.get(&addr.0)
|
||||
}
|
||||
|
||||
/// Store a value in KCEP's program memory.
|
||||
pub fn set(&mut self, addr: Address, value: Value) {
|
||||
self.0.insert(addr.0, value);
|
||||
}
|
||||
|
||||
/// Store a composite value (i.e. a value which takes up multiple addresses in memory).
|
||||
/// Store its parts in consecutive memory addresses starting at `start`.
|
||||
pub fn set_composite<T: Composite<{ N }>, const N: usize>(&mut self, composite_value: T, start: Address) {
|
||||
let parts = composite_value.into_parts().into_iter();
|
||||
for (value, addr) in parts.zip(start.0..) {
|
||||
self.0.insert(addr, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a composite value (i.e. a value which takes up multiple addresses in memory).
|
||||
/// Its parts are stored in consecutive memory addresses starting at `start`.
|
||||
pub fn get_composite<T: Composite<{ N }>, const N: usize>(&self, start: Address) -> Result<T, ExecutionError> {
|
||||
let addrs: [Address; N] = core::array::from_fn(|i| Address(i + start.0));
|
||||
let values: [Value; N] = arr_res_to_res_array(addrs.map(|addr| {
|
||||
self.get(&addr)
|
||||
.map(|x| x.to_owned())
|
||||
.ok_or(ExecutionError::MemoryEmpty { addr })
|
||||
}))?;
|
||||
|
||||
T::from_parts(values)
|
||||
}
|
||||
}
|
||||
|
||||
/// A value stored in KCEP program memory.
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub enum Value {
|
||||
String(String),
|
||||
NumericValue(NumericValue),
|
||||
}
|
||||
|
||||
impl TryFrom<Value> for f64 {
|
||||
type Error = ExecutionError;
|
||||
|
||||
fn try_from(value: Value) -> Result<Self, Self::Error> {
|
||||
if let Value::NumericValue(NumericValue::Float(x)) = value {
|
||||
Ok(x)
|
||||
} else {
|
||||
Err(ExecutionError::MemoryWrongType {
|
||||
expected: "float",
|
||||
actual: format!("{value:?}"),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl From<f64> for Value {
|
||||
fn from(value: f64) -> Self {
|
||||
Self::NumericValue(NumericValue::Float(value))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl From<usize> for Value {
|
||||
fn from(value: usize) -> Self {
|
||||
Self::NumericValue(NumericValue::Integer(value))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub enum NumericValue {
|
||||
Integer(usize),
|
||||
Float(f64),
|
||||
}
|
||||
|
||||
/// One step of the execution plan.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum Instruction {
|
||||
/// Call the KittyCAD API.
|
||||
ApiRequest {
|
||||
/// Which ModelingCmd to call.
|
||||
/// It's a composite value starting at the given address.
|
||||
endpoint: Address,
|
||||
/// Which address should the response be stored in?
|
||||
store_response: Option<usize>,
|
||||
/// Look up each API request in this register number.
|
||||
arguments: Vec<Address>,
|
||||
},
|
||||
/// Set a value in memory.
|
||||
Set {
|
||||
/// Which memory address to set.
|
||||
address: Address,
|
||||
/// What value to set the memory address to.
|
||||
value: Value,
|
||||
},
|
||||
/// Perform arithmetic on values in memory.
|
||||
Arithmetic {
|
||||
/// What to do.
|
||||
arithmetic: Arithmetic,
|
||||
/// Write the output to this memory address.
|
||||
destination: Address,
|
||||
},
|
||||
}
|
||||
|
||||
/// Instruction to perform arithmetic on values in memory.
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Arithmetic {
|
||||
/// Apply this operation
|
||||
pub operation: Operation,
|
||||
/// First operand for the operation
|
||||
pub operand0: Operand,
|
||||
/// Second operand for the operation
|
||||
pub operand1: Operand,
|
||||
}
|
||||
|
||||
macro_rules! arithmetic_body {
|
||||
($arith:ident, $mem:ident, $method:ident) => {
|
||||
match (
|
||||
$arith.operand0.eval(&$mem)?.clone(),
|
||||
$arith.operand1.eval(&$mem)?.clone(),
|
||||
) {
|
||||
// If both operands are numeric, then do the arithmetic operation.
|
||||
(Value::NumericValue(x), Value::NumericValue(y)) => {
|
||||
let num = match (x, y) {
|
||||
(NumericValue::Integer(x), NumericValue::Integer(y)) => NumericValue::Integer(x.$method(y)),
|
||||
(NumericValue::Integer(x), NumericValue::Float(y)) => NumericValue::Float((x as f64).$method(y)),
|
||||
(NumericValue::Float(x), NumericValue::Integer(y)) => NumericValue::Float(x.$method(y as f64)),
|
||||
(NumericValue::Float(x), NumericValue::Float(y)) => NumericValue::Float(x.$method(y)),
|
||||
};
|
||||
Ok(Value::NumericValue(num))
|
||||
}
|
||||
// This operation can only be done on numeric types.
|
||||
_ => Err(ExecutionError::CannotApplyOperation {
|
||||
op: $arith.operation,
|
||||
operands: vec![
|
||||
$arith.operand0.eval(&$mem)?.clone().to_owned(),
|
||||
$arith.operand1.eval(&$mem)?.clone().to_owned(),
|
||||
],
|
||||
}),
|
||||
}
|
||||
};
|
||||
}
|
||||
impl Arithmetic {
|
||||
/// Calculate the the arithmetic equation.
|
||||
/// May read values from the given memory.
|
||||
fn calculate(self, mem: &Memory) -> Result<Value, ExecutionError> {
|
||||
use std::ops::{Add, Div, Mul, Sub};
|
||||
match self.operation {
|
||||
Operation::Add => {
|
||||
arithmetic_body!(self, mem, add)
|
||||
}
|
||||
Operation::Mul => {
|
||||
arithmetic_body!(self, mem, mul)
|
||||
}
|
||||
Operation::Sub => {
|
||||
arithmetic_body!(self, mem, sub)
|
||||
}
|
||||
Operation::Div => {
|
||||
arithmetic_body!(self, mem, div)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Operations that can be applied to values in memory.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub enum Operation {
|
||||
Add,
|
||||
Mul,
|
||||
Sub,
|
||||
Div,
|
||||
}
|
||||
|
||||
impl fmt::Display for Operation {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Operation::Add => "+",
|
||||
Operation::Mul => "*",
|
||||
Operation::Sub => "-",
|
||||
Operation::Div => "/",
|
||||
}
|
||||
.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Argument to an operation.
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub enum Operand {
|
||||
Literal(Value),
|
||||
Reference(Address),
|
||||
}
|
||||
|
||||
impl Operand {
|
||||
/// Evaluate the operand, getting its value.
|
||||
fn eval(&self, mem: &Memory) -> Result<Value, ExecutionError> {
|
||||
match self {
|
||||
Operand::Literal(v) => Ok(v.to_owned()),
|
||||
Operand::Reference(addr) => match mem.get(addr) {
|
||||
None => Err(ExecutionError::MemoryEmpty { addr: *addr }),
|
||||
Some(v) => Ok(v.to_owned()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute the plan.
|
||||
pub fn execute(mem: &mut Memory, plan: Vec<Instruction>) -> Result<(), ExecutionError> {
|
||||
for step in plan {
|
||||
match step {
|
||||
Instruction::ApiRequest { .. } => todo!("Execute API calls"),
|
||||
Instruction::Set { address, value } => {
|
||||
mem.set(address, value);
|
||||
}
|
||||
Instruction::Arithmetic {
|
||||
arithmetic,
|
||||
destination,
|
||||
} => {
|
||||
let out = arithmetic.calculate(mem)?;
|
||||
mem.set(destination, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Errors that could occur when executing a KittyCAD execution plan.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ExecutionError {
|
||||
#[error("Memory address {addr} was not set")]
|
||||
MemoryEmpty { addr: Address },
|
||||
#[error("Cannot apply operation {op} to operands {operands:?}")]
|
||||
CannotApplyOperation { op: Operation, operands: Vec<Value> },
|
||||
#[error("Tried to read a '{expected}' from KCEP program memory, found an '{actual}' instead")]
|
||||
MemoryWrongType { expected: &'static str, actual: String },
|
||||
}
|
||||
|
||||
/// Take an array of result and return a result of array.
|
||||
/// If all members of the array are Ok(T), returns Ok with an array of the T values.
|
||||
/// If any member of the array was Err(E), return Err with the first E value.
|
||||
fn arr_res_to_res_array<T, E, const N: usize>(arr: [Result<T, E>; N]) -> Result<[T; N], E> {
|
||||
let mut out = core::array::from_fn(|_| None);
|
||||
for (i, res) in arr.into_iter().enumerate() {
|
||||
out[i] = match res {
|
||||
Ok(x) => Some(x),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
}
|
||||
Ok(out.map(|opt| opt.unwrap()))
|
||||
}
|
94
src/wasm-lib/execution-plan/src/tests.rs
Normal file
94
src/wasm-lib/execution-plan/src/tests.rs
Normal file
@ -0,0 +1,94 @@
|
||||
use kittycad::types::Point3D;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn write_addr_to_memory() {
|
||||
let plan = vec![Instruction::Set {
|
||||
address: Address(0),
|
||||
value: 3.4.into(),
|
||||
}];
|
||||
let mut mem = Memory::default();
|
||||
execute(&mut mem, plan).unwrap();
|
||||
assert_eq!(mem.get(&Address(0)), Some(&3.4.into()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_literals() {
|
||||
let plan = vec![Instruction::Arithmetic {
|
||||
arithmetic: Arithmetic {
|
||||
operation: Operation::Add,
|
||||
operand0: Operand::Literal(3.into()),
|
||||
operand1: Operand::Literal(2.into()),
|
||||
},
|
||||
destination: Address(1),
|
||||
}];
|
||||
let mut mem = Memory::default();
|
||||
execute(&mut mem, plan).unwrap();
|
||||
assert_eq!(mem.get(&Address(1)), Some(&5.into()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_literal_to_reference() {
|
||||
let plan = vec![
|
||||
// Memory addr 0 contains 450
|
||||
Instruction::Set {
|
||||
address: Address(0),
|
||||
value: 450.into(),
|
||||
},
|
||||
// Add 20 to addr 0
|
||||
Instruction::Arithmetic {
|
||||
arithmetic: Arithmetic {
|
||||
operation: Operation::Add,
|
||||
operand0: Operand::Reference(Address(0)),
|
||||
operand1: Operand::Literal(20.into()),
|
||||
},
|
||||
destination: Address(1),
|
||||
},
|
||||
];
|
||||
// 20 + 450 = 470
|
||||
let mut mem = Memory::default();
|
||||
execute(&mut mem, plan).unwrap();
|
||||
assert_eq!(mem.get(&Address(1)), Some(&470.into()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_to_composite_value() {
|
||||
let mut mem = Memory::default();
|
||||
|
||||
// Write a point to memory.
|
||||
let point_before = Point3D { x: 2.0, y: 3.0, z: 4.0 };
|
||||
let start_addr = Address(100);
|
||||
mem.set_composite(point_before, start_addr);
|
||||
assert_eq!(
|
||||
mem,
|
||||
Memory(HashMap::from(
|
||||
[(100, 2.0.into()), (101, 3.0.into()), (102, 4.0.into()),]
|
||||
))
|
||||
);
|
||||
|
||||
// Update the point's x-value in memory.
|
||||
execute(
|
||||
&mut mem,
|
||||
vec![Instruction::Arithmetic {
|
||||
arithmetic: Arithmetic {
|
||||
operation: Operation::Add,
|
||||
operand0: Operand::Reference(start_addr),
|
||||
operand1: Operand::Literal(40.into()),
|
||||
},
|
||||
destination: start_addr,
|
||||
}],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Read the point out of memory, validate it.
|
||||
let point_after: Point3D = mem.get_composite(start_addr).unwrap();
|
||||
assert_eq!(
|
||||
point_after,
|
||||
Point3D {
|
||||
x: 42.0,
|
||||
y: 3.0,
|
||||
z: 4.0
|
||||
}
|
||||
)
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
use crate::{
|
||||
ast::types::Program,
|
||||
errors::KclError,
|
||||
errors::KclErrorDetails,
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::SourceRange,
|
||||
token::{Token, TokenType},
|
||||
};
|
||||
|
@ -117,9 +117,8 @@ impl From<BinaryOperator> for BinaryExpressionToken {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::ast::types::Literal;
|
||||
|
||||
use super::*;
|
||||
use crate::ast::types::Literal;
|
||||
|
||||
#[test]
|
||||
fn parse_and_evaluate() {
|
||||
|
@ -4,14 +4,13 @@ use anyhow::Result;
|
||||
use derive_docs::stdlib;
|
||||
use schemars::JsonSchema;
|
||||
|
||||
use super::utils::between;
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::{MemoryItem, SketchGroup},
|
||||
std::Args,
|
||||
};
|
||||
|
||||
use super::utils::between;
|
||||
|
||||
/// Returns the segment end of x.
|
||||
pub async fn segment_end_x(args: Args) -> Result<MemoryItem, KclError> {
|
||||
let (segment_name, sketch_group) = args.get_segment_name_sketch_group()?;
|
||||
|
61
yarn.lock
61
yarn.lock
@ -8,9 +8,9 @@
|
||||
integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==
|
||||
|
||||
"@adobe/css-tools@^4.0.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.1.tgz#abfccb8ca78075a2b6187345c26243c1a0842f28"
|
||||
integrity sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg==
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.2.tgz#a6abc715fb6884851fca9dad37fc34739a04fd11"
|
||||
integrity sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw==
|
||||
|
||||
"@alloc/quick-lru@^5.2.0":
|
||||
version "5.2.0"
|
||||
@ -1710,10 +1710,10 @@
|
||||
dependencies:
|
||||
"@lezer/common" "^1.0.0"
|
||||
|
||||
"@lezer/javascript@^1.4.7":
|
||||
version "1.4.7"
|
||||
resolved "https://registry.yarnpkg.com/@lezer/javascript/-/javascript-1.4.7.tgz#4ebcce2db6043c07fbe827188c07cb001bc7fe37"
|
||||
integrity sha512-OVWlK0YEi7HM+9JRWtRkir8qvcg0/kVYg2TAMHlVtl6DU1C9yK1waEOLBMztZsV/axRJxsqfJKhzYz+bxZme5g==
|
||||
"@lezer/javascript@^1.4.9":
|
||||
version "1.4.9"
|
||||
resolved "https://registry.yarnpkg.com/@lezer/javascript/-/javascript-1.4.9.tgz#1536427af5187621b3b616f21b6a21df3ffd1dbe"
|
||||
integrity sha512-7Uv8mBBE6l44spgWEZvEMdDqGV+FIuY7kJ1o5TFm+jxIuxydO3PcKJYiINij09igd1D/9P7l2KDqpkN8c3bM6A==
|
||||
dependencies:
|
||||
"@lezer/highlight" "^1.1.3"
|
||||
"@lezer/lr" "^1.3.0"
|
||||
@ -2244,6 +2244,20 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
|
||||
|
||||
"@types/pixelmatch@^5.2.6":
|
||||
version "5.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/pixelmatch/-/pixelmatch-5.2.6.tgz#fba6de304ac958495f27d85989f5c6bb7499a686"
|
||||
integrity sha512-wC83uexE5KGuUODn6zkm9gMzTwdY5L0chiK+VrKcDfEjzxh1uadlWTvOmAbCpnM9zx/Ww3f8uKlYQVnO/TrqVg==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/pngjs@^6.0.4":
|
||||
version "6.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-6.0.4.tgz#9a457aebabd944efde1a773a0fa1d74933e8021b"
|
||||
integrity sha512-atAK9xLKOnxiuArxcHovmnOUUGBZOQ3f0vCf43FnoKs6XnqiambT1kkJWmdo71IR+BoXSh+CueeFR0GfH3dTlQ==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/prop-types@*":
|
||||
version "15.7.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
|
||||
@ -2256,17 +2270,17 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-modal@^3.16.0":
|
||||
version "3.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-modal/-/react-modal-3.16.0.tgz#b8d6be10de894139a2ea9f4a2505b1b5d02023df"
|
||||
integrity sha512-iphdqXAyUfByLbxJn5j6d+yh93dbMgshqGP0IuBeaKbZXx0aO+OXsvEkt6QctRdxjeM9/bR+Gp3h9F9djVWTQQ==
|
||||
"@types/react-modal@^3.16.3":
|
||||
version "3.16.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-modal/-/react-modal-3.16.3.tgz#250f32c07f1de28e2bcf9c3e84b56adaa6897013"
|
||||
integrity sha512-xXuGavyEGaFQDgBv4UVm8/ZsG+qxeQ7f77yNrW3n+1J6XAstUy5rYHeIHPh1KzsGc6IkCIdu6lQ2xWzu1jBTLg==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@^18.0.0":
|
||||
version "18.2.18"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.18.tgz#c8b233919eef1bdc294f6f34b37f9727ad677516"
|
||||
integrity sha512-da4NTSeBv/P34xoZPhtcLkmZuJ+oYaCxHmyHzwaDQo9RQPBeXV+06gEk2FpqEcsX9XrnNLvRpVh6bdavDSjtiQ==
|
||||
"@types/react@*", "@types/react@^18.2.41":
|
||||
version "18.2.41"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.41.tgz#9eea044246bdb10510df89ef7f8422a8b6ad8fb9"
|
||||
integrity sha512-CwOGr/PiLiNBxEBqpJ7fO3kocP/2SSuC9fpH5K7tusrg4xPSRT/193rzolYwQnTN02We/ATXKnb6GqA5w4fRxw==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
@ -6632,6 +6646,13 @@ pirates@^4.0.1:
|
||||
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9"
|
||||
integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==
|
||||
|
||||
pixelmatch@^5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-5.3.0.tgz#5e5321a7abedfb7962d60dbf345deda87cb9560a"
|
||||
integrity sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==
|
||||
dependencies:
|
||||
pngjs "^6.0.0"
|
||||
|
||||
pkg-types@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.0.3.tgz#988b42ab19254c01614d13f4f65a2cfc7880f868"
|
||||
@ -6655,6 +6676,16 @@ playwright@1.39.0:
|
||||
optionalDependencies:
|
||||
fsevents "2.3.2"
|
||||
|
||||
pngjs@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-6.0.0.tgz#ca9e5d2aa48db0228a52c419c3308e87720da821"
|
||||
integrity sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==
|
||||
|
||||
pngjs@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-7.0.0.tgz#a8b7446020ebbc6ac739db6c5415a65d17090e26"
|
||||
integrity sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==
|
||||
|
||||
portfinder@^1.0.28:
|
||||
version "1.0.32"
|
||||
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.32.tgz#2fe1b9e58389712429dc2bea5beb2146146c7f81"
|
||||
|
Reference in New Issue
Block a user