Compare commits

..

2 Commits

Author SHA1 Message Date
8e016bcae2 Fix the type errors by more explicitly assigning types
This is def more verbose but it was a lot easier to make tsc happy.
2023-11-30 15:28:31 -05:00
9ef7fcc2ea Start to add in State exports to the EngineConnection
I want to start tracking more of the connection health and putting it
into the Network Connectivity widget. This is the first step (of many)
to export the status of the connection and retain errors we have hit
during the handshake.
2023-11-30 13:20:42 -05:00
30 changed files with 161 additions and 3439 deletions

View File

@ -340,7 +340,7 @@ jobs:
cat last_download.json
- name: Authenticate to Google Cloud
uses: 'google-github-actions/auth@v2.0.0'
uses: 'google-github-actions/auth@v1.2.0'
with:
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'

2
.gitignore vendored
View File

@ -35,8 +35,6 @@ 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,8 +4,6 @@ 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
@ -60,13 +58,8 @@ test('Basic sketch', async ({ page }) => {
// click on "Start Sketch" button
await u.clearCommandLogs()
await Promise.all([
u.doAndWaitForImageDiff(
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
200
),
u.waitForDefaultPlanesVisibilityChange(),
])
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
// select a plane
await u.doAndWaitForCmd(() => page.mouse.click(700, 200), 'edit_mode_enter')
@ -86,8 +79,8 @@ test('Basic sketch', async ({ page }) => {
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
const startAt = '[10.97, -14.79]'
const tenish = '11.07'
const startAt = '[9.94, -13.41]'
const tenish = '10.03'
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
@ -105,7 +98,7 @@ test('Basic sketch', async ({ page }) => {
|> startProfileAt(${startAt}, %)
|> line([${tenish}, 0], %)
|> line([0, ${tenish}], %)
|> line([-22.04, 0], %)`)
|> line([-19.97, 0], %)`)
// deselect line tool
await u.doAndWaitForCmd(
@ -133,7 +126,7 @@ test('Basic sketch', async ({ page }) => {
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line({ to: [${tenish}, 0], tag: 'seg01' }, %)
|> line({ to: [10.03, 0], tag: 'seg01' }, %)
|> line([0, ${tenish}], %)
|> angledLine([180, segLen('seg01', %)], %)`)
})
@ -316,8 +309,8 @@ test('Can create sketches on all planes and their back sides', async ({
plane = 'XY',
sign = ''
) => `const part001 = startSketchOn('${plane}')
|> startProfileAt([${sign}6.88, -9.29], %)
|> line([${sign}6.95, 0], %)`
|> startProfileAt([${sign}3.97, -5.36], %)
|> line([${sign}4.01, 0], %)`
await TestSinglePlane({
viewCmd: camCmd,
expectedCode: codeTemplate('XY'),
@ -461,11 +454,6 @@ 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()
@ -488,9 +476,8 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
const startAt = '[10.97, -14.79]'
const tenish = '11.07'
const twentyish = '22.04'
const startAt = '[9.94, -13.41]'
const tenish = '10.03'
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
@ -508,7 +495,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|> startProfileAt(${startAt}, %)
|> line([${tenish}, 0], %)
|> line([0, ${tenish}], %)
|> line([-${twentyish}, 0], %)`)
|> line([-19.97, 0], %)`)
// deselect line tool
await u.doAndWaitForCmd(
@ -517,7 +504,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
)
await u.closeDebugPanel()
const selectionSequence = async () => {
const hoverSequency = async () => {
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
await page.mouse.move(startXPx + PUR * 15, 500 - PUR * 10)
@ -532,76 +519,20 @@ 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 selectionSequence()
await hoverSequency()
// 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(topHorzSegmentClick, 'select_clear', false)
await u.doAndWaitForCmd(
() => page.mouse.click(startXPx + PUR * 10, 500 - PUR * 20),
'select_with_point'
)
// enter sketch again
await u.doAndWaitForCmd(
@ -611,5 +542,5 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
)
// hover again and check it works
await selectionSequence()
await hoverSequency()
})

View File

@ -210,36 +210,11 @@ 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,8 +1,5 @@
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
@ -111,46 +108,5 @@ 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.9",
"@lezer/javascript": "^1.4.7",
"@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.2.41",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@uiw/react-codemirror": "^4.21.20",
"@xstate/inspect": "^0.8.0",
@ -109,9 +109,7 @@
"@types/crypto-js": "^4.1.1",
"@types/debounce-promise": "^3.1.8",
"@types/isomorphic-fetch": "^0.0.36",
"@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.4",
"@types/react-modal": "^3.16.3",
"@types/react-modal": "^3.16.0",
"@types/uuid": "^9.0.4",
"@types/wait-on": "^5.3.4",
"@types/wicg-file-system-access": "^2020.9.6",
@ -129,8 +127,6 @@
"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: 'Google Chrome',
use: { ...devices['Desktop Chrome'], channel: 'chrome' }, // or 'chrome-beta'
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'Google Chrome',
use: { ...devices['Desktop Chrome'], channel: 'chrome' }, // or 'chrome-beta'
},
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },

View File

@ -109,21 +109,6 @@ 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,17 +31,13 @@ import {
} from 'lang/std/sketch'
import { kclManager } from 'lang/KclSinglton'
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
import {
angleBetweenInfo,
applyConstraintAngleBetween,
} from './Toolbar/SetAngleBetween'
import { 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>
@ -266,62 +262,17 @@ 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') {
// 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,
})
})
else if (setSelections.selectionType === 'otherSelection')
return {
selectionRangeTypeMap,
selectionRanges: {
codeBasedSelections: selectionRanges.codeBasedSelections,
otherSelections,
...selectionRanges,
otherSelections: [setSelections.selection],
},
}
} else if (setSelections.selectionType === 'singleCodeCursor') {
else if (!editorView) return {}
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
@ -329,16 +280,12 @@ 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,
otherSelections,
} = handleSelectionWithShift({
codeSelection: setSelections.selection,
currentSelections: selectionRanges,
isShiftDown,
})
const { codeMirrorSelection, selectionRangeTypeMap } =
handleSelectionWithShift({
codeSelection: setSelections.selection,
currestSelections: selectionRanges,
isShiftDown,
})
if (codeMirrorSelection) {
setTimeout(() => {
editorView.dispatch({
@ -346,22 +293,7 @@ export const ModelingMachineProvider = ({
})
})
}
if (!setSelections.selection) {
return {
selectionRangeTypeMap,
selectionRanges: {
codeBasedSelections: selectionRanges.codeBasedSelections,
otherSelections,
},
}
}
return {
selectionRangeTypeMap,
selectionRanges: {
codeBasedSelections: selectionRanges.codeBasedSelections,
otherSelections,
},
}
return { selectionRangeTypeMap }
}
// This DOES NOT set the `selectionRanges` in xstate context
// same as comment above
@ -431,16 +363,10 @@ export const ModelingMachineProvider = ({
}
},
'Get angle info': async ({ selectionRanges }): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } = await (angleBetweenInfo({
selectionRanges,
}).enabled
? applyConstraintAngleBetween({
selectionRanges,
})
: applyConstraintAngleLength({
selectionRanges,
angleOrLength: 'setAngle',
}))
const { modifiedAst, pathToNodeMap } =
await applyConstraintAngleBetween({
selectionRanges,
})
await kclManager.updateAst(modifiedAst, true)
return {
selectionType: 'completeSelection',
@ -483,40 +409,6 @@ 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,19 +42,13 @@ export const TextEditor = ({
}: {
theme: Themes.Light | Themes.Dark
}) => {
const {
editorView,
isLSPServerReady,
setEditorView,
setIsLSPServerReady,
isShiftDown,
} = useStore((s) => ({
editorView: s.editorView,
isLSPServerReady: s.isLSPServerReady,
setEditorView: s.setEditorView,
setIsLSPServerReady: s.setIsLSPServerReady,
isShiftDown: s.isShiftDown,
}))
const { editorView, isLSPServerReady, setEditorView, setIsLSPServerReady } =
useStore((s) => ({
editorView: s.editorView,
isLSPServerReady: s.isLSPServerReady,
setEditorView: s.setEditorView,
setIsLSPServerReady: s.setIsLSPServerReady,
}))
const { code, errors } = useKclContext()
const {
@ -119,7 +113,6 @@ export const TextEditor = ({
codeMirrorRanges: viewUpdate.state.selection.ranges,
selectionRanges,
selectionRangeTypeMap,
isShiftDown,
})
if (!eventInfo) return

View File

@ -59,7 +59,6 @@ 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 angleLengthInfo({
export function setAngleLengthInfo({
selectionRanges,
angleOrLength = 'setLength',
}: {
@ -50,10 +50,7 @@ export function angleLengthInfo({
kclManager.ast,
angleOrLength
)
const enabled =
selectionRanges.codeBasedSelections.length <= 1 &&
isAllTooltips &&
transforms.every(Boolean)
const enabled = isAllTooltips && transforms.every(Boolean)
return { enabled, transforms }
}
@ -67,7 +64,7 @@ export async function applyConstraintAngleLength({
modifiedAst: Program
pathToNodeMap: PathToNodeMap
}> {
const { transforms } = angleLengthInfo({ selectionRanges, angleOrLength })
const { transforms } = setAngleLengthInfo({ selectionRanges, angleOrLength })
const { valueUsedInTransform } = transformAstSketchLines({
ast: JSON.parse(JSON.stringify(kclManager.ast)),
selectionRanges,

View File

@ -48,6 +48,41 @@ type Timeout = ReturnType<typeof setTimeout>
type ClientMetrics = Models['ClientMetrics_type']
type ConnectionStage = 'websocket' | 'ice' | 'webrtc'
type ConnectionState = 'ok' | 'pending' | 'failed'
type ConnectionError = {
type: string
message: string
raw: any
}
type ConnectionRecord = {
status: ConnectionState
errors: ConnectionError[]
}
type ConnectionStatus = {
websocket: ConnectionRecord
ice: ConnectionRecord
webrtc: ConnectionRecord
}
const CONNECTION_STATUSES_DEFAULT: ConnectionStatus = {
websocket: {
status: 'pending',
errors: [],
},
ice: {
status: 'pending',
errors: [],
},
webrtc: {
status: 'pending',
errors: [],
},
}
// EngineConnection encapsulates the connection(s) to the Engine
// for the EngineCommandManager; namely, the underlying WebSocket
// and WebRTC connections.
@ -70,6 +105,8 @@ class EngineConnection {
private onClose: (engineConnection: EngineConnection) => void
private onNewTrack: (track: NewTrackArgs) => void
private connectionStatuses: ConnectionStatus
// TODO: actual type is ClientMetrics
private webrtcStatsCollector?: () => Promise<ClientMetrics>
@ -104,6 +141,7 @@ class EngineConnection {
this.onConnectionStarted = onConnectionStarted
this.onClose = onClose
this.onNewTrack = onNewTrack
this.connectionStatuses = Object.assign({}, CONNECTION_STATUSES_DEFAULT)
// TODO(paultag): This ought to be tweakable.
const pingIntervalMs = 10000
@ -204,6 +242,7 @@ class EngineConnection {
)
}
this.connectionStatuses = Object.assign({}, CONNECTION_STATUSES_DEFAULT)
this.websocket = new WebSocket(this.url, [])
this.websocket.binaryType = 'arraybuffer'
@ -221,6 +260,12 @@ class EngineConnection {
console.error(
`ICE candidate returned an error: ${event.errorCode}: ${event.errorText} for ${event.url}`
)
this.connectionStatuses.ice.status = 'failed'
this.connectionStatuses.ice.errors.push({
type: event.type,
message: event.errorText,
raw: event,
})
})
this.pc.addEventListener('connectionstatechange', (event) => {
@ -228,15 +273,20 @@ class EngineConnection {
if (this.shouldTrace()) {
iceSpan.resolve?.()
}
this.connectionStatuses.ice.status = 'ok'
this.connectionStatuses.webrtc.status = 'ok'
console.log(this.connectionStatuses)
} else if (this.pc?.iceConnectionState === 'failed') {
// failed is a terminal state; let's explicitly kill the
// connection to the server at this point.
console.log('failed to negotiate ice connection; restarting')
this.connectionStatuses.webrtc.status = 'failed'
this.close()
}
})
this.websocket.addEventListener('open', (event) => {
this.connectionStatuses.websocket.status = 'ok'
if (this.shouldTrace()) {
websocketSpan.resolve?.()
@ -280,6 +330,12 @@ class EngineConnection {
this.websocket.addEventListener('error', (event) => {
console.log('websocket connection error', event)
this.connectionStatuses.websocket.status = 'failed'
this.connectionStatuses.websocket.errors.push({
type: event.type,
message: 'an unknown websocket error occurred',
raw: event,
})
this.close()
})

View File

@ -334,7 +334,10 @@ const setAbsDistanceForAngleLineCreateNode =
): TransformInfo['createNode'] =>
({ tag, forceValueUsedInTransform, varValA }) => {
return (args, referencedSegment) => {
const valueUsedInTransform = roundOff(getArgLiteralVal(args?.[1]), 2)
const valueUsedInTransform = roundOff(
getArgLiteralVal(args?.[1]) - (referencedSegment?.to?.[index] || 0),
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.
await browserSaveFile(blob, file.name)
browserSaveFile(blob, file.name)
}
}
} catch (e) {

View File

@ -8,9 +8,6 @@ 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
@ -113,15 +110,6 @@ 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 {
@ -176,7 +164,6 @@ export function handleSelectionBatch({
}): {
selectionRangeTypeMap: SelectionRangeTypeMap
codeMirrorSelection?: EditorSelection
otherSelections: Axis[]
} {
const ranges: ReturnType<typeof EditorSelection.cursor>[] = []
const selectionRangeTypeMap: SelectionRangeTypeMap = {}
@ -193,74 +180,43 @@ export function handleSelectionBatch({
ranges,
selections.codeBasedSelections.length - 1
),
otherSelections: selections.otherSelections,
}
return {
selectionRangeTypeMap,
otherSelections: selections.otherSelections,
}
}
export function handleSelectionWithShift({
codeSelection,
otherSelection,
currentSelections,
currestSelections,
isShiftDown,
}: {
codeSelection?: Selection
otherSelection?: Axis
currentSelections: Selections
currestSelections: Selections
isShiftDown: boolean
}): {
selectionRangeTypeMap: SelectionRangeTypeMap
otherSelections: Axis[]
codeMirrorSelection?: EditorSelection
} {
const code = kclManager.code
if (codeSelection && otherSelection) {
throw new Error('cannot have both code and other selection')
}
if (!codeSelection && !otherSelection) {
if (!codeSelection)
return handleSelectionBatch({
selections: {
otherSelections: [],
otherSelections: currestSelections.otherSelections,
codeBasedSelections: [
{
range: [0, code.length ? code.length : 0],
range: [0, code.length ? code.length - 1 : 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 = {
otherSelections: isShiftDown ? currentSelections.otherSelections : [],
codeBasedSelections: newCodeBasedSelections,
...currestSelections,
codeBasedSelections: isShiftDown
? [...currestSelections.codeBasedSelections, codeSelection]
: [codeSelection],
}
return handleSelectionBatch({ selections })
}
@ -271,12 +227,10 @@ export function processCodeMirrorRanges({
codeMirrorRanges,
selectionRanges,
selectionRangeTypeMap,
isShiftDown,
}: {
codeMirrorRanges: readonly SelectionRange[]
selectionRanges: Selections
selectionRangeTypeMap: SelectionRangeTypeMap
isShiftDown: boolean
}): null | {
modelingEvent: ModelingMachineEvent
engineEvents: Models['WebSocketRequest_type'][]
@ -337,7 +291,7 @@ export function processCodeMirrorRanges({
data: {
selectionType: 'mirrorCodeMirrorSelections',
selection: {
otherSelections: isShiftDown ? selectionRanges.otherSelections : [],
...selectionRanges,
codeBasedSelections,
},
},
@ -346,7 +300,7 @@ export function processCodeMirrorRanges({
}
}
function resetAndSetEngineEntitySelectionCmds(
export 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,15 +5,11 @@
'@@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 };
@ -23,9 +19,7 @@
"xstate.stop": { type: "xstate.stop" };
};
invokeSrcNameMap: {
"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 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";
@ -35,7 +29,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 ABS X info" | "Get ABS Y info" | "Get angle info" | "Get horizontal info" | "Get length info" | "Get perpendicular distance info" | "Get vertical info";
services: "Get angle info" | "Get horizontal info" | "Get length info" | "Get perpendicular distance info" | "Get vertical info";
};
eventsCausingActions: {
"AST add line segment": "Add point";
@ -48,21 +42,19 @@
"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-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";
"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";
"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 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";
"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";
"hide default planes": "Cancel" | "Select default plane" | "xstate.stop";
"reset sketch metadata": "Cancel" | "Select default plane";
"set default plane id": "Select default plane";
@ -81,8 +73,6 @@
};
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";
@ -90,8 +80,6 @@
"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";
@ -110,15 +98,13 @@
"is editing existing sketch": "";
};
eventsCausingServices: {
"Get ABS X info": "Constrain ABS X";
"Get ABS Y info": "Constrain ABS Y";
"Get angle info": "Constrain angle";
"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 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";
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";
"Move Tool"?: "Move init" | "Move with execute" | "Move without re-execute" | "No move"; }; };
tags: never;
}

View File

@ -822,16 +822,6 @@ 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,7 +54,6 @@ members = [
"derive-docs",
"kcl",
"kcl-macros",
"execution-plan",
]
[workspace.dependencies]

View File

@ -1,13 +0,0 @@
[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

@ -1,27 +0,0 @@
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

@ -1,288 +0,0 @@
//! 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

@ -1,94 +0,0 @@
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,6 +1,7 @@
use crate::{
ast::types::Program,
errors::{KclError, KclErrorDetails},
errors::KclError,
errors::KclErrorDetails,
executor::SourceRange,
token::{Token, TokenType},
};

View File

@ -117,9 +117,10 @@ impl From<BinaryOperator> for BinaryExpressionToken {
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::types::Literal;
use super::*;
#[test]
fn parse_and_evaluate() {
/// Make a literal

View File

@ -4,13 +4,14 @@ 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.2"
resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.2.tgz#a6abc715fb6884851fca9dad37fc34739a04fd11"
integrity sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw==
version "4.3.1"
resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.1.tgz#abfccb8ca78075a2b6187345c26243c1a0842f28"
integrity sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg==
"@alloc/quick-lru@^5.2.0":
version "5.2.0"
@ -1710,10 +1710,10 @@
dependencies:
"@lezer/common" "^1.0.0"
"@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==
"@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==
dependencies:
"@lezer/highlight" "^1.1.3"
"@lezer/lr" "^1.3.0"
@ -2244,20 +2244,6 @@
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"
@ -2270,17 +2256,17 @@
dependencies:
"@types/react" "*"
"@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==
"@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==
dependencies:
"@types/react" "*"
"@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==
"@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==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
@ -6646,13 +6632,6 @@ 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"
@ -6676,16 +6655,6 @@ 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"