Compare commits

...

14 Commits

Author SHA1 Message Date
36a5461de5 Composite is now generic over size in memory.
Previously the Composite trait accepted a Vec<Value> for reading from memory, and returned a Vec<Value> for writing to memory. This meant the caller might provide the wrong size of Vec (e.g. a Point3d requires 3 memory addresses, so the code has to check and handle if the user only passes in 2).

Now those methods accept/return a [Value; N] which is statically guaranteed to be the right size! This means there's no more need for runtime checks that the Vec is the right size -- because the array is guaranteed to be the right size.

This involves removing the SIZE constant and instead changing it into a SIZE const generic.
2023-12-04 12:33:07 -06:00
6a9a0a8bd7 Rename mod in_memory to composite (#1169) 2023-12-04 12:16:08 -06:00
90e432b10e Read/write composite values to KCEP memory (#1164)
KCEP's memory model stores 'values', which can be either numbers or strings. But we'll need to support storing complex objects like points, lines, sketchgroups, etc in memory too.

This PR adds a trait for KCEP composite types, which are laid out in KCEP memory as a consecutive series of values, one for each field.

Part of https://github.com/KittyCAD/modeling-app/issues/993
2023-12-04 11:20:42 -06:00
90499e086f Bump @adobe/css-tools from 4.3.1 to 4.3.2 (#1158)
Bumps [@adobe/css-tools](https://github.com/adobe/css-tools) from 4.3.1 to 4.3.2.
- [Changelog](https://github.com/adobe/css-tools/blob/main/History.md)
- [Commits](https://github.com/adobe/css-tools/commits)

---
updated-dependencies:
- dependency-name: "@adobe/css-tools"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-04 07:16:51 -05:00
8b398a8dd5 Bump google-github-actions/auth from 1.2.0 to 2.0.0 (#1147)
Bumps [google-github-actions/auth](https://github.com/google-github-actions/auth) from 1.2.0 to 2.0.0.
- [Release notes](https://github.com/google-github-actions/auth/releases)
- [Changelog](https://github.com/google-github-actions/auth/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google-github-actions/auth/compare/v1.2.0...v2.0.0)

---
updated-dependencies:
- dependency-name: google-github-actions/auth
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-04 07:16:28 -05:00
23d2dc8dc8 Bump @types/react-modal from 3.16.0 to 3.16.3 (#1144)
Bumps [@types/react-modal](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-modal) from 3.16.0 to 3.16.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-modal)

---
updated-dependencies:
- dependency-name: "@types/react-modal"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2023-12-04 07:15:52 -05:00
764a73ec8b Bump @types/react from 18.2.18 to 18.2.41 (#1166)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 18.2.18 to 18.2.41.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-04 06:30:21 -05:00
b69451d2fe Bump @lezer/javascript from 1.4.7 to 1.4.9 (#1060)
Bumps [@lezer/javascript](https://github.com/lezer-parser/javascript) from 1.4.7 to 1.4.9.
- [Changelog](https://github.com/lezer-parser/javascript/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lezer-parser/javascript/compare/1.4.7...1.4.9)

---
updated-dependencies:
- dependency-name: "@lezer/javascript"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2023-12-04 05:05:12 -05:00
173d50517c EP instructions must be serializable (#1163)
We want to be able to extract the execution plan (EP) from a KCL program, copy it, and paste it into an engine unit test. Therefore they must be de/serializable.
2023-12-01 19:36:39 -06:00
3b63632005 Start execution plans (#1155) 2023-12-01 16:19:54 -06:00
2bd3b06178 Sort constraint buttons (#1161)
add sorting to constraintns
2023-12-01 20:49:26 +11:00
9c58cde35f side quest for screenshot diffs (#1160)
side just for screenshot diffs
2023-12-01 20:49:12 +11:00
3eb92bb0c4 Select axis and relevant constraints (#1154)
* update select logic for axis

* add abs Y and X constraints

* make selection tests much more thorough including axis selections

* fmt

* tweak

* tweak

* add snap to XY constraints

* side just for screenshot diffs

* update angle constraint to allow axis seleciton

* fix bux in absY constraint

* add sorting to constraintns

* add issue to TODO

* Revert "side just for screenshot diffs"

This reverts commit aae7874859.

* fix number because something must have updated in the engine

* typo

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

* Revert "add sorting to constraintns"

This reverts commit 36054a4069.

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

* triggre CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-12-01 20:18:51 +11:00
f3083eb59d more e2e export fixes (#1150)
* more e2e export fixes

* fixes

* fmt

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

* trigger CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-11-30 17:20:52 +11:00
29 changed files with 3439 additions and 105 deletions

View File

@ -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
View File

@ -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/

File diff suppressed because it is too large Load Diff

View File

@ -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()
})

View File

@ -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')

View File

@ -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)
}),
}
}

View File

@ -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",

View File

@ -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'] },

View File

@ -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}

View File

@ -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,
})

View File

@ -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

View File

@ -59,6 +59,7 @@ export function angleBetweenInfo({
)
const _enableEqual =
selectionRanges.otherSelections.length === 0 &&
secondaryVarDecs.length === 1 &&
isAllTooltips &&
isOthersLinkedToPrimary &&

View File

@ -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,

View File

@ -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)

View File

@ -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…

View File

@ -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) {

View File

@ -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

View File

@ -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;
}

View File

@ -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"

View File

@ -54,6 +54,7 @@ members = [
"derive-docs",
"kcl",
"kcl-macros",
"execution-plan",
]
[workspace.dependencies]

View 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"

View 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 })
}
}

View 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()))
}

View 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
}
)
}

View File

@ -1,7 +1,6 @@
use crate::{
ast::types::Program,
errors::KclError,
errors::KclErrorDetails,
errors::{KclError, KclErrorDetails},
executor::SourceRange,
token::{Token, TokenType},
};

View File

@ -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() {

View File

@ -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()?;

View File

@ -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"