Compare commits
9 Commits
nightly-v2
...
pierremtb/
Author | SHA1 | Date | |
---|---|---|---|
34272b872d | |||
668e2afb99 | |||
548c664db0 | |||
d3a3f4410c | |||
22eb343171 | |||
f2cfa4d5cf | |||
3f1f40eeba | |||
ff2d161606 | |||
210c78029d |
@ -950,75 +950,7 @@ test(
|
||||
|
||||
test.describe('Grid visibility', { tag: '@snapshot' }, () => {
|
||||
// FIXME: Skip on macos its being weird.
|
||||
// test.skip(process.platform === 'darwin', 'Skip on macos')
|
||||
|
||||
test('Grid turned off to on via command bar', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
const stream = page.getByTestId('stream')
|
||||
const mask = [
|
||||
page.locator('#app-header'),
|
||||
page.locator('#sidebar-top-ribbon'),
|
||||
page.locator('#sidebar-bottom-ribbon'),
|
||||
]
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.openDebugPanel()
|
||||
// wait for execution done
|
||||
await expect(
|
||||
page.locator('[data-message-type="execution-done"]')
|
||||
).toHaveCount(1)
|
||||
await u.closeDebugPanel()
|
||||
await u.closeKclCodePanel()
|
||||
// TODO: Find a way to truly know that the objects have finished
|
||||
// rendering, because an execution-done message is not sufficient.
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Open the command bar.
|
||||
await page
|
||||
.getByRole('button', { name: 'Commands', exact: false })
|
||||
.or(page.getByRole('button', { name: '⌘K' }))
|
||||
.click()
|
||||
const commandName = 'show scale grid'
|
||||
const commandOption = page.getByRole('option', {
|
||||
name: commandName,
|
||||
exact: false,
|
||||
})
|
||||
const cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||
// This selector changes after we set the setting
|
||||
await cmdSearchBar.fill(commandName)
|
||||
await expect(commandOption).toBeVisible()
|
||||
await commandOption.click()
|
||||
|
||||
const toggleInput = page.getByPlaceholder('Off')
|
||||
await expect(toggleInput).toBeVisible()
|
||||
await expect(toggleInput).toBeFocused()
|
||||
|
||||
// Select On
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await expect(page.getByRole('option', { name: 'Off' })).toHaveAttribute(
|
||||
'data-headlessui-state',
|
||||
'active selected'
|
||||
)
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await expect(page.getByRole('option', { name: 'On' })).toHaveAttribute(
|
||||
'data-headlessui-state',
|
||||
'active'
|
||||
)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// Check the toast appeared
|
||||
await expect(
|
||||
page.getByText(`Set show scale grid to "true" as a user default`)
|
||||
).toBeVisible()
|
||||
|
||||
await expect(stream).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask,
|
||||
})
|
||||
})
|
||||
test.skip(process.platform === 'darwin', 'Skip on macos')
|
||||
|
||||
test('Grid turned off', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
|
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 54 KiB |
@ -200,7 +200,10 @@ function CoreDump() {
|
||||
() => new CoreDumpManager(engineCommandManager, codeManager, token),
|
||||
[]
|
||||
)
|
||||
useHotkeyWrapper(['mod + shift + .'], () => {
|
||||
// TODO: revisit once progress is made on upstream issue
|
||||
// https://github.com/JohannesKlauss/react-hotkeys-hook/issues/1064
|
||||
// const hotkey = process.platform !== 'linux' ? 'mod + shift + .' : 'mod + shift + >'
|
||||
useHotkeyWrapper(['mod + shift + .', 'mod + shift + >'], () => {
|
||||
toast
|
||||
.promise(
|
||||
coreDump(coreDumpManager, true),
|
||||
|
@ -69,7 +69,14 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [isKclLspReady, setIsKclLspReady] = useState(false)
|
||||
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
|
||||
|
||||
const { auth } = useSettingsAuthContext()
|
||||
const {
|
||||
auth,
|
||||
settings: {
|
||||
context: {
|
||||
modeling: { defaultUnit },
|
||||
},
|
||||
},
|
||||
} = useSettingsAuthContext()
|
||||
const token = auth?.context.token
|
||||
const navigate = useNavigate()
|
||||
|
||||
@ -85,6 +92,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const initEvent: KclWorkerOptions = {
|
||||
wasmUrl: wasmUrl(),
|
||||
token: token,
|
||||
baseUnit: defaultUnit.current,
|
||||
apiBaseUrl: VITE_KC_API_BASE_URL,
|
||||
}
|
||||
lspWorker.postMessage({
|
||||
|
@ -54,6 +54,7 @@ import {
|
||||
Selections,
|
||||
updateSelections,
|
||||
canLoftSelection,
|
||||
canRevolveSelection,
|
||||
canShellSelection,
|
||||
} from 'lib/selections'
|
||||
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
||||
@ -575,6 +576,26 @@ export const ModelingMachineProvider = ({
|
||||
if (err(canSweep)) return false
|
||||
return canSweep
|
||||
},
|
||||
'has valid revolve selection': ({ context: { selectionRanges } }) => {
|
||||
// A user can begin extruding if they either have 1+ faces selected or nothing selected
|
||||
// TODO: I believe this guard only allows for extruding a single face at a time
|
||||
const hasNoSelection =
|
||||
selectionRanges.graphSelections.length === 0 ||
|
||||
isRangeBetweenCharacters(selectionRanges) ||
|
||||
isSelectionLastLine(selectionRanges, codeManager.code)
|
||||
|
||||
if (hasNoSelection) {
|
||||
// they have no selection, we should enable the button
|
||||
// so they can select the face through the cmdbar
|
||||
// BUT only if there's extrudable geometry
|
||||
return doesSceneHaveSweepableSketch(kclManager.ast)
|
||||
}
|
||||
if (!isSketchPipe(selectionRanges)) return false
|
||||
|
||||
const canSweep = canRevolveSelection(selectionRanges)
|
||||
if (err(canSweep)) return false
|
||||
return canSweep
|
||||
},
|
||||
'has valid loft selection': ({ context: { selectionRanges } }) => {
|
||||
const hasNoSelection =
|
||||
selectionRanges.graphSelections.length === 0 ||
|
||||
|
@ -76,7 +76,7 @@ export const ModelingPane = ({
|
||||
return (
|
||||
<section
|
||||
{...props}
|
||||
title={title && typeof title === 'string' ? title : ''}
|
||||
aria-label={title && typeof title === 'string' ? title : ''}
|
||||
data-testid={detailsTestId}
|
||||
id={id}
|
||||
className={
|
||||
|
@ -23,6 +23,7 @@ import {
|
||||
engineCommandManager,
|
||||
sceneEntitiesManager,
|
||||
} from 'lib/singletons'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { IndexLoaderData } from 'lib/types'
|
||||
import { settings } from 'lib/settings/initialSettings'
|
||||
import {
|
||||
@ -128,11 +129,27 @@ export const SettingsAuthProviderBase = ({
|
||||
.setTheme(context.app.theme.current)
|
||||
.catch(reportRejection)
|
||||
},
|
||||
setEngineScaleGridVisibility: ({ context }) => {
|
||||
engineCommandManager.setScaleGridVisibility(
|
||||
context.modeling.showScaleGrid.current
|
||||
)
|
||||
},
|
||||
setClientTheme: ({ context }) => {
|
||||
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
||||
sceneInfra.theme = opposingTheme
|
||||
sceneEntitiesManager.updateSegmentBaseColor(opposingTheme)
|
||||
},
|
||||
setEngineEdges: ({ context }) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
engineCommandManager.sendSceneCommand({
|
||||
cmd_id: uuidv4(),
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'edge_lines_visible' as any, // TODO update kittycad.ts to get this new command type
|
||||
hidden: !context.modeling.highlightEdges.current,
|
||||
},
|
||||
})
|
||||
},
|
||||
toastSuccess: ({ event }) => {
|
||||
if (!('data' in event)) return
|
||||
const eventParts = event.type.replace(/^set./, '').split('.') as [
|
||||
@ -158,27 +175,17 @@ export const SettingsAuthProviderBase = ({
|
||||
},
|
||||
'Execute AST': ({ context, event }) => {
|
||||
try {
|
||||
const relevantSetting = (s: typeof settings) => {
|
||||
return (
|
||||
s.modeling?.defaultUnit?.current !==
|
||||
context.modeling.defaultUnit.current ||
|
||||
s.modeling.showScaleGrid.current !==
|
||||
context.modeling.showScaleGrid.current ||
|
||||
s.modeling?.highlightEdges.current !==
|
||||
context.modeling.highlightEdges.current
|
||||
)
|
||||
}
|
||||
|
||||
const allSettingsIncludesUnitChange =
|
||||
event.type === 'Set all settings' &&
|
||||
relevantSetting(event.settings)
|
||||
event.settings?.modeling?.defaultUnit?.current !==
|
||||
context.modeling.defaultUnit.current
|
||||
const resetSettingsIncludesUnitChange =
|
||||
event.type === 'Reset settings' && relevantSetting(settings)
|
||||
event.type === 'Reset settings' &&
|
||||
context.modeling.defaultUnit.current !==
|
||||
settings?.modeling?.defaultUnit?.default
|
||||
|
||||
if (
|
||||
event.type === 'set.modeling.defaultUnit' ||
|
||||
event.type === 'set.modeling.showScaleGrid' ||
|
||||
event.type === 'set.modeling.highlightEdges' ||
|
||||
allSettingsIncludesUnitChange ||
|
||||
resetSettingsIncludesUnitChange
|
||||
) {
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { LspWorkerEventType } from '@kittycad/codemirror-lsp-client'
|
||||
|
||||
import { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
|
||||
|
||||
export enum LspWorker {
|
||||
Kcl = 'kcl',
|
||||
Copilot = 'copilot',
|
||||
@ -7,6 +9,7 @@ export enum LspWorker {
|
||||
export interface KclWorkerOptions {
|
||||
wasmUrl: string
|
||||
token: string
|
||||
baseUnit: UnitLength
|
||||
apiBaseUrl: string
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@ import {
|
||||
KclWorkerOptions,
|
||||
CopilotWorkerOptions,
|
||||
} from 'editor/plugins/lsp/types'
|
||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||
import { err, reportRejection } from 'lib/trap'
|
||||
|
||||
const intoServer: IntoServer = new IntoServer()
|
||||
@ -45,12 +46,14 @@ export async function copilotLspRun(
|
||||
|
||||
export async function kclLspRun(
|
||||
config: ServerConfig,
|
||||
engineCommandManager: EngineCommandManager | null,
|
||||
token: string,
|
||||
baseUnit: string,
|
||||
baseUrl: string
|
||||
) {
|
||||
try {
|
||||
console.log('start kcl lsp')
|
||||
await kcl_lsp_run(config, null, undefined, token, baseUrl)
|
||||
await kcl_lsp_run(config, engineCommandManager, baseUnit, token, baseUrl)
|
||||
} catch (e: any) {
|
||||
console.log('kcl lsp failed', e)
|
||||
// We can't restart here because a moved value, we should do this another way.
|
||||
@ -79,7 +82,13 @@ onmessage = function (event: MessageEvent) {
|
||||
switch (worker) {
|
||||
case LspWorker.Kcl:
|
||||
const kclData = eventData as KclWorkerOptions
|
||||
await kclLspRun(config, kclData.token, kclData.apiBaseUrl)
|
||||
await kclLspRun(
|
||||
config,
|
||||
null,
|
||||
kclData.token,
|
||||
kclData.baseUnit,
|
||||
kclData.apiBaseUrl
|
||||
)
|
||||
break
|
||||
case LspWorker.Copilot:
|
||||
let copilotData = eventData as CopilotWorkerOptions
|
||||
|
@ -2,7 +2,7 @@ import { useLayoutEffect, useEffect, useRef } from 'react'
|
||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { deferExecution } from 'lib/utils'
|
||||
import { Themes } from 'lib/theme'
|
||||
import { makeDefaultPlanes } from 'lang/wasm'
|
||||
import { makeDefaultPlanes, modifyGrid } from 'lang/wasm'
|
||||
import { useModelingContext } from './useModelingContext'
|
||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||
import { useAppState, useAppStream } from 'AppState'
|
||||
@ -56,6 +56,9 @@ export function useSetupEngineManager(
|
||||
makeDefaultPlanes: () => {
|
||||
return makeDefaultPlanes(kclManager.engineCommandManager)
|
||||
},
|
||||
modifyGrid: (hidden: boolean) => {
|
||||
return modifyGrid(kclManager.engineCommandManager, hidden)
|
||||
},
|
||||
})
|
||||
hasSetNonZeroDimensions.current = true
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ beforeAll(async () => {
|
||||
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
|
||||
setMediaStream: () => {},
|
||||
setIsStreamReady: () => {},
|
||||
modifyGrid: async () => {},
|
||||
callbackOnEngineLiteConnect: () => {
|
||||
resolve(true)
|
||||
},
|
||||
|
@ -335,7 +335,7 @@ export function mutateAstWithTagForSketchSegment(
|
||||
return { modifiedAst: astClone, tag }
|
||||
}
|
||||
|
||||
function getEdgeTagCall(
|
||||
export function getEdgeTagCall(
|
||||
tag: string,
|
||||
artifact: Artifact
|
||||
): Node<Identifier | CallExpression> {
|
||||
|
154
src/lang/modifyAst/addRevolve.ts
Normal file
@ -0,0 +1,154 @@
|
||||
import { err } from 'lib/trap'
|
||||
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
|
||||
import {
|
||||
Program,
|
||||
PathToNode,
|
||||
Expr,
|
||||
CallExpression,
|
||||
PipeExpression,
|
||||
VariableDeclarator,
|
||||
} from 'lang/wasm'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import {
|
||||
createLiteral,
|
||||
createCallExpressionStdLib,
|
||||
createObjectExpression,
|
||||
createIdentifier,
|
||||
createPipeExpression,
|
||||
findUniqueName,
|
||||
createVariableDeclaration,
|
||||
} from 'lang/modifyAst'
|
||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import {
|
||||
mutateAstWithTagForSketchSegment,
|
||||
getEdgeTagCall,
|
||||
} from 'lang/modifyAst/addEdgeTreatment'
|
||||
export function revolveSketch(
|
||||
ast: Node<Program>,
|
||||
pathToSketchNode: PathToNode,
|
||||
shouldPipe = false,
|
||||
angle: Expr = createLiteral(4),
|
||||
axis: Selections
|
||||
):
|
||||
| {
|
||||
modifiedAst: Node<Program>
|
||||
pathToSketchNode: PathToNode
|
||||
pathToRevolveArg: PathToNode
|
||||
}
|
||||
| Error {
|
||||
const clonedAst = structuredClone(ast)
|
||||
const sketchNode = getNodeFromPath(clonedAst, pathToSketchNode)
|
||||
if (err(sketchNode)) return sketchNode
|
||||
|
||||
// testing code
|
||||
const pathToAxisSelection = getNodePathFromSourceRange(
|
||||
clonedAst,
|
||||
axis.graphSelections[0]?.codeRef.range
|
||||
)
|
||||
|
||||
const lineNode = getNodeFromPath<CallExpression>(
|
||||
clonedAst,
|
||||
pathToAxisSelection,
|
||||
'CallExpression'
|
||||
)
|
||||
if (err(lineNode)) return lineNode
|
||||
|
||||
// TODO Kevin: What if |> close(%)?
|
||||
// TODO Kevin: What if opposite edge
|
||||
// TODO Kevin: What if the edge isn't planar to the sketch?
|
||||
// TODO Kevin: add a tag.
|
||||
const tagResult = mutateAstWithTagForSketchSegment(
|
||||
clonedAst,
|
||||
pathToAxisSelection
|
||||
)
|
||||
|
||||
// Have the tag whether it is already created or a new one is generated
|
||||
if (err(tagResult)) return tagResult
|
||||
const { tag } = tagResult
|
||||
|
||||
/* Original Code */
|
||||
const { node: sketchExpression } = sketchNode
|
||||
|
||||
// determine if sketchExpression is in a pipeExpression or not
|
||||
const sketchPipeExpressionNode = getNodeFromPath<PipeExpression>(
|
||||
clonedAst,
|
||||
pathToSketchNode,
|
||||
'PipeExpression'
|
||||
)
|
||||
if (err(sketchPipeExpressionNode)) return sketchPipeExpressionNode
|
||||
const { node: sketchPipeExpression } = sketchPipeExpressionNode
|
||||
const isInPipeExpression = sketchPipeExpression.type === 'PipeExpression'
|
||||
|
||||
const sketchVariableDeclaratorNode = getNodeFromPath<VariableDeclarator>(
|
||||
clonedAst,
|
||||
pathToSketchNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
if (err(sketchVariableDeclaratorNode)) return sketchVariableDeclaratorNode
|
||||
const {
|
||||
node: sketchVariableDeclarator,
|
||||
shallowPath: sketchPathToDecleration,
|
||||
} = sketchVariableDeclaratorNode
|
||||
|
||||
const axisSelection = axis?.graphSelections[0]?.artifact
|
||||
|
||||
if (!axisSelection) return new Error('Axis selection is missing.')
|
||||
|
||||
const revolveCall = createCallExpressionStdLib('revolve', [
|
||||
createObjectExpression({
|
||||
angle: angle,
|
||||
axis: getEdgeTagCall(tag, axisSelection),
|
||||
}),
|
||||
createIdentifier(sketchVariableDeclarator.id.name),
|
||||
])
|
||||
|
||||
if (shouldPipe) {
|
||||
const pipeChain = createPipeExpression(
|
||||
isInPipeExpression
|
||||
? [...sketchPipeExpression.body, revolveCall]
|
||||
: [sketchExpression as any, revolveCall]
|
||||
)
|
||||
|
||||
sketchVariableDeclarator.init = pipeChain
|
||||
const pathToRevolveArg: PathToNode = [
|
||||
...sketchPathToDecleration,
|
||||
['init', 'VariableDeclarator'],
|
||||
['body', ''],
|
||||
[pipeChain.body.length - 1, 'index'],
|
||||
['arguments', 'CallExpression'],
|
||||
[0, 'index'],
|
||||
]
|
||||
|
||||
return {
|
||||
modifiedAst: clonedAst,
|
||||
pathToSketchNode,
|
||||
pathToRevolveArg,
|
||||
}
|
||||
}
|
||||
|
||||
// We're not creating a pipe expression,
|
||||
// but rather a separate constant for the extrusion
|
||||
const name = findUniqueName(clonedAst, KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE)
|
||||
const VariableDeclaration = createVariableDeclaration(name, revolveCall)
|
||||
const sketchIndexInPathToNode =
|
||||
sketchPathToDecleration.findIndex((a) => a[0] === 'body') + 1
|
||||
const sketchIndexInBody = sketchPathToDecleration[sketchIndexInPathToNode][0]
|
||||
if (typeof sketchIndexInBody !== 'number')
|
||||
return new Error('expected sketchIndexInBody to be a number')
|
||||
clonedAst.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)
|
||||
|
||||
const pathToRevolveArg: PathToNode = [
|
||||
['body', ''],
|
||||
[sketchIndexInBody + 1, 'index'],
|
||||
['declaration', 'VariableDeclaration'],
|
||||
['init', 'VariableDeclarator'],
|
||||
['arguments', 'CallExpression'],
|
||||
[0, 'index'],
|
||||
]
|
||||
return {
|
||||
modifiedAst: clonedAst,
|
||||
pathToSketchNode: [...pathToSketchNode.slice(0, -1), [-1, 'index']],
|
||||
pathToRevolveArg,
|
||||
}
|
||||
}
|
@ -139,6 +139,7 @@ beforeAll(async () => {
|
||||
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
|
||||
setMediaStream: () => {},
|
||||
setIsStreamReady: () => {},
|
||||
modifyGrid: async () => {},
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
callbackOnEngineLiteConnect: async () => {
|
||||
const cacheEntries = Object.entries(codeToWriteCacheFor) as [
|
||||
|
@ -871,3 +871,15 @@ export function codeRefFromRange(range: SourceRange, ast: Program): CodeRef {
|
||||
pathToNode: getNodePathFromSourceRange(ast, range),
|
||||
}
|
||||
}
|
||||
|
||||
export function isSolid2D(artifact: Artifact): artifact is solid2D {
|
||||
return (artifact as solid2D).pathId !== undefined
|
||||
}
|
||||
|
||||
export function isSegment(artifact: Artifact): artifact is SegmentArtifact {
|
||||
return (artifact as SegmentArtifact).pathId !== undefined
|
||||
}
|
||||
|
||||
export function isSweep(artifact: Artifact): artifact is SweepArtifact {
|
||||
return (artifact as SweepArtifact).pathId !== undefined
|
||||
}
|
||||
|
@ -1399,6 +1399,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
}
|
||||
|
||||
private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null
|
||||
private modifyGrid: (hidden: boolean) => Promise<void> | null = () => null
|
||||
|
||||
private onEngineConnectionOpened = () => {}
|
||||
private onEngineConnectionClosed = () => {}
|
||||
@ -1431,6 +1432,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
height,
|
||||
token,
|
||||
makeDefaultPlanes,
|
||||
modifyGrid,
|
||||
settings = {
|
||||
pool: null,
|
||||
theme: Themes.Dark,
|
||||
@ -1450,12 +1452,14 @@ export class EngineCommandManager extends EventTarget {
|
||||
height: number
|
||||
token?: string
|
||||
makeDefaultPlanes: () => Promise<DefaultPlanes>
|
||||
modifyGrid: (hidden: boolean) => Promise<void>
|
||||
settings?: SettingsViaQueryString
|
||||
}) {
|
||||
if (settings) {
|
||||
this.settings = settings
|
||||
}
|
||||
this.makeDefaultPlanes = makeDefaultPlanes
|
||||
this.modifyGrid = modifyGrid
|
||||
if (width === 0 || height === 0) {
|
||||
return
|
||||
}
|
||||
@ -1535,15 +1539,21 @@ export class EngineCommandManager extends EventTarget {
|
||||
type: 'default_camera_get_settings',
|
||||
},
|
||||
})
|
||||
await this.initPlanes()
|
||||
setIsStreamReady(true)
|
||||
// We want modify the grid first because we don't want it to flash.
|
||||
// Ideally these would already be default hidden in engine (TODO do
|
||||
// that) https://github.com/KittyCAD/engine/issues/2282
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.modifyGrid(!this.settings.showScaleGrid)?.then(async () => {
|
||||
await this.initPlanes()
|
||||
setIsStreamReady(true)
|
||||
|
||||
// Other parts of the application should use this to react on scene ready.
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EngineCommandManagerEvents.SceneReady, {
|
||||
detail: this.engineConnection,
|
||||
})
|
||||
)
|
||||
// Other parts of the application should use this to react on scene ready.
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EngineCommandManagerEvents.SceneReady, {
|
||||
detail: this.engineConnection,
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
this.engineConnection.addEventListener(
|
||||
@ -2202,6 +2212,15 @@ export class EngineCommandManager extends EventTarget {
|
||||
}).catch(reportRejection)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the visibility of the scale grid in the engine scene.
|
||||
* @param visible - whether to show or hide the scale grid
|
||||
*/
|
||||
setScaleGridVisibility(visible: boolean) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.modifyGrid(!visible)
|
||||
}
|
||||
|
||||
// Some "objects" have the same source range, such as sketch_mode_start and start_path.
|
||||
// So when passing a range, we need to also specify the command type
|
||||
mapRangeToObjectId(
|
||||
|
@ -1,9 +1,13 @@
|
||||
import { err } from 'lib/trap'
|
||||
import { parse, ParseResult } from './wasm'
|
||||
import { initPromise, parse, ParseResult } from './wasm'
|
||||
import { enginelessExecutor } from 'lib/testHelpers'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||
|
||||
beforeEach(async () => {
|
||||
await initPromise
|
||||
})
|
||||
|
||||
it('can execute parsed AST', async () => {
|
||||
const code = `x = 1
|
||||
// A comment.`
|
||||
|
@ -1,13 +1,14 @@
|
||||
import init, {
|
||||
parse_wasm,
|
||||
recast_wasm,
|
||||
execute,
|
||||
execute_wasm,
|
||||
kcl_lint,
|
||||
modify_ast_for_sketch_wasm,
|
||||
is_points_ccw,
|
||||
get_tangential_arc_to_info,
|
||||
program_memory_init,
|
||||
make_default_planes,
|
||||
modify_grid,
|
||||
coredump,
|
||||
toml_stringify,
|
||||
default_app_settings,
|
||||
@ -42,9 +43,7 @@ import { Environment } from '../wasm-lib/kcl/bindings/Environment'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError'
|
||||
import { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
|
||||
import { getChangedSettingsAtLevel } from 'lib/settings/settingsUtils'
|
||||
|
||||
export type { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||
export type { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||
export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
|
||||
export type { ObjectExpression } from '../wasm-lib/kcl/bindings/ObjectExpression'
|
||||
@ -494,20 +493,18 @@ export const _executor = async (
|
||||
return Promise.reject(programMemoryOverride)
|
||||
|
||||
try {
|
||||
let jsAppSettings = default_app_settings()
|
||||
let baseUnit = 'mm'
|
||||
if (!TEST) {
|
||||
const getSettingsState = import('components/SettingsAuthProvider').then(
|
||||
(module) => module.getSettingsState
|
||||
)
|
||||
const settings = (await getSettingsState)()
|
||||
if (settings) {
|
||||
jsAppSettings = getChangedSettingsAtLevel(settings, 'user')
|
||||
}
|
||||
baseUnit =
|
||||
(await getSettingsState)()?.modeling.defaultUnit.current || 'mm'
|
||||
}
|
||||
const execState: RawExecState = await execute(
|
||||
const execState: RawExecState = await execute_wasm(
|
||||
JSON.stringify(node),
|
||||
JSON.stringify(programMemoryOverride?.toRaw() || null),
|
||||
JSON.stringify({ settings: jsAppSettings }),
|
||||
baseUnit,
|
||||
engineCommandManager,
|
||||
fileSystemManager
|
||||
)
|
||||
@ -555,6 +552,20 @@ export const makeDefaultPlanes = async (
|
||||
}
|
||||
}
|
||||
|
||||
export const modifyGrid = async (
|
||||
engineCommandManager: EngineCommandManager,
|
||||
hidden: boolean
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await modify_grid(engineCommandManager, hidden)
|
||||
return
|
||||
} catch (e) {
|
||||
// TODO: do something real with the error.
|
||||
console.log('modify grid error', e)
|
||||
return Promise.reject(e)
|
||||
}
|
||||
}
|
||||
|
||||
export const modifyAstForSketch = async (
|
||||
engineCommandManager: EngineCommandManager,
|
||||
ast: Node<Program>,
|
||||
|
@ -9,6 +9,7 @@ import { Selections } from 'lib/selections'
|
||||
import { kclManager } from 'lib/singletons'
|
||||
import { err } from 'lib/trap'
|
||||
import { modelingMachine, SketchTool } from 'machines/modelingMachine'
|
||||
import { revolveAxisValidator } from './validators'
|
||||
|
||||
type OutputFormat = Models['OutputFormat_type']
|
||||
type OutputTypeKey = OutputFormat['type']
|
||||
@ -46,6 +47,7 @@ export type ModelingCommandSchema = {
|
||||
Revolve: {
|
||||
selection: Selections
|
||||
angle: KclCommandValue
|
||||
axis: Selections
|
||||
}
|
||||
Fillet: {
|
||||
// todo
|
||||
@ -330,6 +332,13 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
required: true,
|
||||
skip: true,
|
||||
},
|
||||
axis: {
|
||||
required: true,
|
||||
inputType: 'selection',
|
||||
selectionTypes: ['segment', 'sweepEdge', 'edgeCutEdge'],
|
||||
multiple: false,
|
||||
validation: revolveAxisValidator,
|
||||
},
|
||||
angle: {
|
||||
inputType: 'kcl',
|
||||
defaultValue: KCL_DEFAULT_DEGREE,
|
||||
|
107
src/lib/commandBarConfigs/validators.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { engineCommandManager } from 'lib/singletons'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { CommandBarContext } from 'machines/commandBarMachine'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { isSolid2D, isSegment, isSweep } from 'lang/std/artifactGraph'
|
||||
|
||||
export const disableDryRunWithRetry = async (numberOfRetries = 3) => {
|
||||
for (let tries = 0; tries < numberOfRetries; tries++) {
|
||||
try {
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'disable_dry_run' },
|
||||
})
|
||||
// Exit out since the command was successful
|
||||
return
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error('disable_dry_run failed. This is bad!')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Takes a callback function and wraps it around enable_dry_run and disable_dry_run
|
||||
export const dryRunWrapper = async (callback: () => Promise<any>) => {
|
||||
// Gotcha: What about race conditions?
|
||||
try {
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'enable_dry_run' },
|
||||
})
|
||||
const result = await callback()
|
||||
return result
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
await disableDryRunWithRetry(5)
|
||||
}
|
||||
}
|
||||
|
||||
function isSelections(selections: unknown): selections is Selections {
|
||||
return (
|
||||
(selections as Selections).graphSelections !== undefined &&
|
||||
(selections as Selections).otherSelections !== undefined
|
||||
)
|
||||
}
|
||||
|
||||
export const revolveAxisValidator = async ({
|
||||
data,
|
||||
context,
|
||||
}: {
|
||||
data: { [key: string]: Selections }
|
||||
context: CommandBarContext
|
||||
}): Promise<boolean | string> => {
|
||||
if (!isSelections(context.argumentsToSubmit.selection)) {
|
||||
return 'Unable to revolve, selections are missing'
|
||||
}
|
||||
const artifact =
|
||||
context.argumentsToSubmit.selection.graphSelections[0].artifact
|
||||
|
||||
if (!artifact) {
|
||||
return 'Unable to revolve, sketch not found'
|
||||
}
|
||||
|
||||
if (!(isSolid2D(artifact) || isSegment(artifact) || isSweep(artifact))) {
|
||||
return 'Unable to revolve, sketch has no path'
|
||||
}
|
||||
|
||||
const sketchSelection = artifact.pathId
|
||||
let edgeSelection = data.axis.graphSelections[0].artifact?.id
|
||||
|
||||
if (!sketchSelection) {
|
||||
return 'Unable to revolve, sketch is missing'
|
||||
}
|
||||
|
||||
if (!edgeSelection) {
|
||||
return 'Unable to revolve, edge is missing'
|
||||
}
|
||||
|
||||
const angleInDegrees: Models['Angle_type'] = {
|
||||
unit: 'degrees',
|
||||
value: 360,
|
||||
}
|
||||
|
||||
const revolveAboutEdgeCommand = async () => {
|
||||
return await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'revolve_about_edge',
|
||||
angle: angleInDegrees,
|
||||
edge_id: edgeSelection,
|
||||
target: sketchSelection,
|
||||
tolerance: 0.0001,
|
||||
},
|
||||
})
|
||||
}
|
||||
const attemptRevolve = await dryRunWrapper(revolveAboutEdgeCommand)
|
||||
if (attemptRevolve?.success) {
|
||||
return true
|
||||
} else {
|
||||
// return error message for the toast
|
||||
return 'Unable to revolve with selected axis'
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@ import { ReactNode } from 'react'
|
||||
import { MachineManager } from 'components/MachineManagerProvider'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { Artifact } from 'lang/std/artifactGraph'
|
||||
|
||||
import { CommandBarContext } from 'machines/commandBarMachine'
|
||||
type Icon = CustomIconName
|
||||
const PLATFORMS = ['both', 'web', 'desktop'] as const
|
||||
const INPUT_TYPES = [
|
||||
@ -147,6 +147,13 @@ export type CommandArgumentConfig<
|
||||
inputType: 'selection'
|
||||
selectionTypes: Artifact['type'][]
|
||||
multiple: boolean
|
||||
validation?: ({
|
||||
data,
|
||||
context,
|
||||
}: {
|
||||
data: any
|
||||
context: CommandBarContext
|
||||
}) => Promise<boolean | string>
|
||||
}
|
||||
| {
|
||||
inputType: 'kcl'
|
||||
@ -236,6 +243,13 @@ export type CommandArgument<
|
||||
inputType: 'selection'
|
||||
selectionTypes: Artifact['type'][]
|
||||
multiple: boolean
|
||||
validation?: ({
|
||||
data,
|
||||
context,
|
||||
}: {
|
||||
data: any
|
||||
context: CommandBarContext
|
||||
}) => Promise<boolean | string>
|
||||
}
|
||||
| {
|
||||
inputType: 'kcl'
|
||||
|
@ -111,3 +111,10 @@ export const KCL_SAMPLES_MANIFEST_URLS = {
|
||||
|
||||
/** Toast id for the app auto-updater toast */
|
||||
export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast'
|
||||
|
||||
/** Local sketch axis values in KCL for operations, it could either be 'X' or 'Y' */
|
||||
export const KCL_AXIS_X = 'X'
|
||||
export const KCL_AXIS_Y = 'Y'
|
||||
export const KCL_AXIS_NEG_X = '-X'
|
||||
export const KCL_AXIS_NEG_Y = '-Y'
|
||||
export const KCL_DEFAULT_AXIS = 'X'
|
||||
|
@ -155,6 +155,8 @@ export function buildCommandArgument<
|
||||
context: ContextFrom<T>,
|
||||
machineActor: Actor<T>
|
||||
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
|
||||
// GOTCHA: modelingCommandConfig is not a 1:1 mapping to this baseCommandArgument
|
||||
// You need to manually add key/value pairs here.
|
||||
const baseCommandArgument = {
|
||||
description: arg.description,
|
||||
required: arg.required,
|
||||
@ -181,6 +183,7 @@ export function buildCommandArgument<
|
||||
...baseCommandArgument,
|
||||
multiple: arg.multiple,
|
||||
selectionTypes: arg.selectionTypes,
|
||||
validation: arg.validation,
|
||||
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
|
||||
} else if (arg.inputType === 'kcl') {
|
||||
return {
|
||||
|
@ -569,6 +569,17 @@ export function canSweepSelection(selection: Selections) {
|
||||
)
|
||||
}
|
||||
|
||||
export function canRevolveSelection(selection: Selections) {
|
||||
const commonNodes = selection.graphSelections.map((_, i) =>
|
||||
buildCommonNodeFromSelection(selection, i)
|
||||
)
|
||||
return (
|
||||
!!isSketchPipe(selection) &&
|
||||
(commonNodes.every((n) => nodeHasClose(n)) ||
|
||||
commonNodes.every((n) => nodeHasCircle(n)))
|
||||
)
|
||||
}
|
||||
|
||||
export function canLoftSelection(selection: Selections) {
|
||||
const commonNodes = selection.graphSelections.map((_, i) =>
|
||||
buildCommonNodeFromSelection(selection, i)
|
||||
|
@ -112,6 +112,9 @@ export async function executor(
|
||||
makeDefaultPlanes: () => {
|
||||
return new Promise((resolve) => resolve(defaultPlanes))
|
||||
},
|
||||
modifyGrid: (hidden: boolean) => {
|
||||
return new Promise((resolve) => resolve())
|
||||
},
|
||||
})
|
||||
|
||||
return new Promise((resolve) => {
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
import { Selections__old } from 'lib/selections'
|
||||
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
|
||||
import { MachineManager } from 'components/MachineManagerProvider'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
export type CommandBarContext = {
|
||||
commands: Command[]
|
||||
@ -247,14 +248,69 @@ export const commandBarMachine = setup({
|
||||
'All arguments are skippable': () => false,
|
||||
},
|
||||
actors: {
|
||||
'Validate argument': fromPromise(({ input }) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// TODO: figure out if we should validate argument data here or in the form itself,
|
||||
// and if we should support people configuring a argument's validation function
|
||||
'Validate argument': fromPromise(
|
||||
({
|
||||
input,
|
||||
}: {
|
||||
input: {
|
||||
context: CommandBarContext | undefined
|
||||
event: CommandBarMachineEvent | undefined
|
||||
}
|
||||
}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!input || input?.event?.type !== 'Submit argument') {
|
||||
toast.error(`Unable to validate, wrong event type.`)
|
||||
return reject(`Unable to validate, wrong event type`)
|
||||
}
|
||||
|
||||
resolve(input)
|
||||
})
|
||||
}),
|
||||
const context = input?.context
|
||||
|
||||
if (!context) {
|
||||
toast.error(`Unable to validate, wrong argument.`)
|
||||
return reject(`Unable to validate, wrong argument`)
|
||||
}
|
||||
|
||||
const data = input.event.data
|
||||
const argName = context.currentArgument?.name
|
||||
const args = context?.selectedCommand?.args
|
||||
const argConfig = args && argName ? args[argName] : undefined
|
||||
// Only do a validation check if the argument, selectedCommand, and the validation function are defined
|
||||
if (
|
||||
context.currentArgument &&
|
||||
context.selectedCommand &&
|
||||
argConfig?.inputType === 'selection' &&
|
||||
argConfig?.validation
|
||||
) {
|
||||
argConfig
|
||||
.validation({ context, data })
|
||||
.then((result) => {
|
||||
if (typeof result === 'boolean' && result === true) {
|
||||
return resolve(data)
|
||||
} else {
|
||||
// validation failed
|
||||
if (typeof result === 'string') {
|
||||
// The result of the validation is the error message
|
||||
toast.error(result)
|
||||
return reject(
|
||||
`unable to validate ${argName}, Message: ${result}`
|
||||
)
|
||||
} else {
|
||||
// Default message if there is not a custom one sent
|
||||
toast.error(`Unable to validate ${argName}`)
|
||||
return reject(`unable to validate ${argName}}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
return reject(`unable to validate ${argName}}`)
|
||||
})
|
||||
} else {
|
||||
// Missing several requirements for validate argument, just bypass
|
||||
return resolve(data)
|
||||
}
|
||||
})
|
||||
}
|
||||
),
|
||||
'Validate all arguments': fromPromise(
|
||||
({ input }: { input: CommandBarContext }) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -449,9 +505,10 @@ export const commandBarMachine = setup({
|
||||
invoke: {
|
||||
src: 'Validate argument',
|
||||
id: 'validateSingleArgument',
|
||||
input: ({ event }) => {
|
||||
if (event.type !== 'Submit argument') return {}
|
||||
return event.data
|
||||
input: ({ event, context }) => {
|
||||
if (event.type !== 'Submit argument')
|
||||
return { event: undefined, context: undefined }
|
||||
return { event, context }
|
||||
},
|
||||
onDone: {
|
||||
target: '#Command Bar.Checking Arguments',
|
||||
|
@ -42,12 +42,12 @@ import {
|
||||
applyConstraintEqualLength,
|
||||
setEqualLengthInfo,
|
||||
} from 'components/Toolbar/EqualLength'
|
||||
import { revolveSketch } from 'lang/modifyAst/addRevolve'
|
||||
import {
|
||||
addOffsetPlane,
|
||||
deleteFromSelection,
|
||||
extrudeSketch,
|
||||
loftSketches,
|
||||
revolveSketch,
|
||||
} from 'lang/modifyAst'
|
||||
import {
|
||||
applyEdgeTreatmentToSelection,
|
||||
@ -394,6 +394,7 @@ export const modelingMachine = setup({
|
||||
guards: {
|
||||
'Selection is on face': () => false,
|
||||
'has valid sweep selection': () => false,
|
||||
'has valid revolve selection': () => false,
|
||||
'has valid loft selection': () => false,
|
||||
'has valid shell selection': () => false,
|
||||
'has valid edge treatment selection': () => false,
|
||||
@ -682,7 +683,7 @@ export const modelingMachine = setup({
|
||||
if (event.type !== 'Revolve') return
|
||||
;(async () => {
|
||||
if (!event.data) return
|
||||
const { selection, angle } = event.data
|
||||
const { selection, angle, axis } = event.data
|
||||
let ast = kclManager.ast
|
||||
if (
|
||||
'variableName' in angle &&
|
||||
@ -693,15 +694,21 @@ export const modelingMachine = setup({
|
||||
newBody.splice(angle.insertIndex, 0, angle.variableDeclarationAst)
|
||||
ast.body = newBody
|
||||
}
|
||||
|
||||
// This is the selection of the sketch that will be revolved
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
selection.graphSelections[0]?.codeRef.range
|
||||
)
|
||||
|
||||
const revolveSketchRes = revolveSketch(
|
||||
ast,
|
||||
pathToNode,
|
||||
false,
|
||||
'variableName' in angle ? angle.variableIdentifierAst : angle.valueAst
|
||||
'variableName' in angle
|
||||
? angle.variableIdentifierAst
|
||||
: angle.valueAst,
|
||||
axis
|
||||
)
|
||||
if (trap(revolveSketchRes)) return
|
||||
const { modifiedAst, pathToRevolveArg } = revolveSketchRes
|
||||
@ -1687,7 +1694,7 @@ export const modelingMachine = setup({
|
||||
|
||||
Revolve: {
|
||||
target: 'idle',
|
||||
guard: 'has valid sweep selection',
|
||||
guard: 'has valid revolve selection',
|
||||
actions: ['AST revolve'],
|
||||
reenter: false,
|
||||
},
|
||||
|
@ -42,6 +42,8 @@ export const settingsMachine = setup({
|
||||
setClientTheme: () => {},
|
||||
'Execute AST': () => {},
|
||||
toastSuccess: () => {},
|
||||
setEngineEdges: () => {},
|
||||
setEngineScaleGridVisibility: () => {},
|
||||
setClientSideSceneUnits: () => {},
|
||||
persistSettings: () => {},
|
||||
resetSettings: assign(({ context, event }) => {
|
||||
@ -170,7 +172,7 @@ export const settingsMachine = setup({
|
||||
'set.modeling.highlightEdges': {
|
||||
target: 'persisting settings',
|
||||
|
||||
actions: ['setSettingAtLevel', 'toastSuccess', 'Execute AST'],
|
||||
actions: ['setSettingAtLevel', 'toastSuccess', 'setEngineEdges'],
|
||||
},
|
||||
|
||||
'Reset settings': {
|
||||
@ -199,7 +201,11 @@ export const settingsMachine = setup({
|
||||
|
||||
'set.modeling.showScaleGrid': {
|
||||
target: 'persisting settings',
|
||||
actions: ['setSettingAtLevel', 'toastSuccess', 'Execute AST'],
|
||||
actions: [
|
||||
'setSettingAtLevel',
|
||||
'toastSuccess',
|
||||
'setEngineScaleGridVisibility',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
33
src/main.ts
@ -23,6 +23,15 @@ import argvFromYargs from './commandLineArgs'
|
||||
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
|
||||
// Supporting multiple instances instead of multiple applications
|
||||
let cmdQPressed = false
|
||||
const instances: BrowserWindow[] = []
|
||||
const gotTheLock = app.requestSingleInstanceLock()
|
||||
if (!gotTheLock) {
|
||||
app.quit()
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Check the command line arguments for a project path
|
||||
const args = parseCLIArgs()
|
||||
|
||||
@ -117,16 +126,34 @@ const createWindow = (filePath?: string): BrowserWindow => {
|
||||
|
||||
newWindow.show()
|
||||
|
||||
instances.push(newWindow)
|
||||
return newWindow
|
||||
}
|
||||
|
||||
// before-quit with multiple instances
|
||||
if (process.platform === 'darwin') {
|
||||
// Quit from the dock context menu should quit the application directly
|
||||
app.on('before-quit', () => {
|
||||
cmdQPressed = true
|
||||
})
|
||||
}
|
||||
|
||||
// Quit when all windows are closed, even on macOS. There, it's common
|
||||
// for applications and their menu bar to stay active until the user quits
|
||||
// explicitly with Cmd + Q, but it is a really weird behavior with our app.
|
||||
// app.on('window-all-closed', () => {
|
||||
// app.quit()
|
||||
// })
|
||||
app.on('window-all-closed', () => {
|
||||
app.quit()
|
||||
if (cmdQPressed || process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
// Various actions can trigger this event, such as launching the application for the first time,
|
||||
// attempting to re-launch the application when it's already running, or clicking on the application's dock or taskbar icon.
|
||||
app.on('activate', () => createWindow())
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
@ -135,6 +162,10 @@ app.on('ready', (event, data) => {
|
||||
mainWindow = createWindow()
|
||||
})
|
||||
|
||||
// This event will be emitted inside the primary instance of your application when a second instance
|
||||
// has been executed and calls app.requestSingleInstanceLock().
|
||||
app.on('second-instance', (event, argv, workingDirectory) => createWindow())
|
||||
|
||||
// For now there is no good reason to separate these out to another file(s)
|
||||
// There is just not enough code to warrant it and further abstracts everything
|
||||
// which is already quite abstracted
|
||||
|
@ -79,7 +79,10 @@ kittycad = { version = "0.3.28", default-features = false, features = ["js", "re
|
||||
kittycad-modeling-cmds = { version = "0.2.77", features = ["websocket"] }
|
||||
|
||||
[workspace.lints.clippy]
|
||||
assertions_on_result_states = "warn"
|
||||
dbg_macro = "warn"
|
||||
iter_over_hash_type = "warn"
|
||||
lossy_float_literal = "warn"
|
||||
|
||||
[[test]]
|
||||
name = "executor"
|
||||
|
@ -15,5 +15,5 @@ async fn kcl_to_core_test() {
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
result.unwrap();
|
||||
}
|
||||
|
@ -120,61 +120,6 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the visibility of edges.
|
||||
async fn set_edge_visibility(
|
||||
&self,
|
||||
visible: bool,
|
||||
source_range: SourceRange,
|
||||
) -> Result<(), crate::errors::KclError> {
|
||||
self.batch_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
source_range,
|
||||
&ModelingCmd::from(mcmd::EdgeLinesVisible { hidden: !visible }),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_units(
|
||||
&self,
|
||||
units: crate::UnitLength,
|
||||
source_range: SourceRange,
|
||||
) -> Result<(), crate::errors::KclError> {
|
||||
// Before we even start executing the program, set the units.
|
||||
self.batch_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
source_range,
|
||||
&ModelingCmd::from(mcmd::SetSceneUnits { unit: units.into() }),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Re-run the command to apply the settings.
|
||||
async fn reapply_settings(
|
||||
&self,
|
||||
settings: &crate::ExecutorSettings,
|
||||
source_range: SourceRange,
|
||||
) -> Result<(), crate::errors::KclError> {
|
||||
// Set the edge visibility.
|
||||
self.set_edge_visibility(settings.highlight_edges, source_range).await?;
|
||||
|
||||
// Change the units.
|
||||
self.set_units(settings.units, source_range).await?;
|
||||
|
||||
// Send the command to show the grid.
|
||||
self.modify_grid(!settings.show_grid, source_range).await?;
|
||||
|
||||
// We do not have commands for changing ssao on the fly.
|
||||
|
||||
// Flush the batch queue, so the settings are applied right away.
|
||||
self.flush_batch(false, source_range).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Add a modeling command to the batch but don't fire it right away.
|
||||
async fn batch_modeling_cmd(
|
||||
&self,
|
||||
@ -559,11 +504,11 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
||||
}))
|
||||
}
|
||||
|
||||
async fn modify_grid(&self, hidden: bool, source_range: SourceRange) -> Result<(), KclError> {
|
||||
async fn modify_grid(&self, hidden: bool) -> Result<(), KclError> {
|
||||
// Hide/show the grid.
|
||||
self.batch_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
source_range,
|
||||
Default::default(),
|
||||
&ModelingCmd::from(mcmd::ObjectVisible {
|
||||
hidden,
|
||||
object_id: *GRID_OBJECT_ID,
|
||||
@ -574,7 +519,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
||||
// Hide/show the grid scale text.
|
||||
self.batch_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
source_range,
|
||||
Default::default(),
|
||||
&ModelingCmd::from(mcmd::ObjectVisible {
|
||||
hidden,
|
||||
object_id: *GRID_SCALE_TEXT_OBJECT_ID,
|
||||
@ -582,6 +527,8 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.flush_batch(false, Default::default()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -1,50 +0,0 @@
|
||||
//! Functions for helping with caching an ast and finding the parts the changed.
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
execution::ExecState,
|
||||
parsing::ast::types::{Node, Program},
|
||||
};
|
||||
|
||||
/// Information for the caching an AST and smartly re-executing it if we can.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
pub struct CacheInformation {
|
||||
/// The old information.
|
||||
pub old: Option<OldAstState>,
|
||||
/// The new ast to executed.
|
||||
pub new_ast: Node<Program>,
|
||||
}
|
||||
|
||||
/// The old ast and program memory.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
pub struct OldAstState {
|
||||
/// The ast.
|
||||
pub ast: Node<Program>,
|
||||
/// The exec state.
|
||||
pub exec_state: ExecState,
|
||||
/// The last settings used for execution.
|
||||
pub settings: crate::execution::ExecutorSettings,
|
||||
}
|
||||
|
||||
impl From<crate::Program> for CacheInformation {
|
||||
fn from(program: crate::Program) -> Self {
|
||||
CacheInformation {
|
||||
old: None,
|
||||
new_ast: program.ast,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of a cache check.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
pub struct CacheResult {
|
||||
/// Should we clear the scene and start over?
|
||||
pub clear_scene: bool,
|
||||
/// The program that needs to be executed.
|
||||
pub program: Node<Program>,
|
||||
}
|
@ -366,6 +366,7 @@ impl Node<CallExpressionKw> {
|
||||
#[async_recursion]
|
||||
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
||||
let fn_name = &self.callee.name;
|
||||
let callsite: SourceRange = self.into();
|
||||
|
||||
// Build a hashmap from argument labels to the final evaluated values.
|
||||
let mut fn_args = HashMap::with_capacity(self.arguments.len());
|
||||
@ -412,7 +413,39 @@ impl Node<CallExpressionKw> {
|
||||
Ok(result)
|
||||
}
|
||||
FunctionKind::UserDefined => {
|
||||
todo!("Part of modeling-app#4600: Support keyword arguments for user-defined functions")
|
||||
let source_range = SourceRange::from(self);
|
||||
// Clone the function so that we can use a mutable reference to
|
||||
// exec_state.
|
||||
let func = exec_state.memory.get(fn_name, source_range)?.clone();
|
||||
let fn_dynamic_state = exec_state.dynamic_state.merge(&exec_state.memory);
|
||||
|
||||
let return_value = {
|
||||
let previous_dynamic_state = std::mem::replace(&mut exec_state.dynamic_state, fn_dynamic_state);
|
||||
let result = func
|
||||
.call_fn_kw(args, exec_state, ctx.clone(), callsite)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
// Add the call expression to the source ranges.
|
||||
// TODO currently ignored by the frontend
|
||||
e.add_source_ranges(vec![source_range])
|
||||
});
|
||||
exec_state.dynamic_state = previous_dynamic_state;
|
||||
result?
|
||||
};
|
||||
|
||||
let result = return_value.ok_or_else(move || {
|
||||
let mut source_ranges: Vec<SourceRange> = vec![source_range];
|
||||
// We want to send the source range of the original function.
|
||||
if let KclValue::Function { meta, .. } = func {
|
||||
source_ranges = meta.iter().map(|m| m.source_range).collect();
|
||||
};
|
||||
KclError::UndefinedValue(KclErrorDetails {
|
||||
message: format!("Result of user-defined function {} is undefined", fn_name),
|
||||
source_ranges,
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -72,6 +72,10 @@ pub enum KclValue {
|
||||
ImportedGeometry(ImportedGeometry),
|
||||
#[ts(skip)]
|
||||
Function {
|
||||
/// Adam Chalmers speculation:
|
||||
/// Reference to a KCL stdlib function (written in Rust).
|
||||
/// Some if the KCL value is an alias of a stdlib function,
|
||||
/// None if it's a KCL function written/declared in KCL.
|
||||
#[serde(skip)]
|
||||
func: Option<MemoryFunction>,
|
||||
#[schemars(skip)]
|
||||
@ -503,4 +507,39 @@ impl KclValue {
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// If this is a function, call it by applying keyword arguments.
|
||||
/// If it's not a function, returns an error.
|
||||
pub async fn call_fn_kw(
|
||||
&self,
|
||||
args: crate::std::Args,
|
||||
exec_state: &mut ExecState,
|
||||
ctx: ExecutorContext,
|
||||
callsite: SourceRange,
|
||||
) -> Result<Option<KclValue>, KclError> {
|
||||
let KclValue::Function {
|
||||
func,
|
||||
expression,
|
||||
memory: closure_memory,
|
||||
meta: _,
|
||||
} = &self
|
||||
else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "cannot call this because it isn't a function".to_string(),
|
||||
source_ranges: vec![callsite],
|
||||
}));
|
||||
};
|
||||
if let Some(_func) = func {
|
||||
todo!("Implement calling KCL stdlib fns that are aliased. Part of https://github.com/KittyCAD/modeling-app/issues/4600");
|
||||
} else {
|
||||
crate::execution::call_user_defined_function_kw(
|
||||
args.kw_args,
|
||||
closure_memory.as_ref(),
|
||||
expression.as_ref(),
|
||||
exec_state,
|
||||
&ctx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,18 +23,15 @@ type Point3D = kcmc::shared::Point3d<f64>;
|
||||
pub use function_param::FunctionParam;
|
||||
pub use kcl_value::{KclObjectFields, KclValue};
|
||||
|
||||
pub(crate) mod cache;
|
||||
mod exec_ast;
|
||||
mod function_param;
|
||||
mod kcl_value;
|
||||
|
||||
use crate::{
|
||||
engine::{EngineManager, ExecutionKind},
|
||||
errors::{KclError, KclErrorDetails},
|
||||
execution::cache::{CacheInformation, CacheResult},
|
||||
fs::{FileManager, FileSystem},
|
||||
parsing::ast::types::{
|
||||
BodyItem, Expr, FunctionExpression, ImportSelector, ItemVisibility, Node, NodeRef, TagDeclarator, TagNode,
|
||||
parsing::ast::{
|
||||
cache::{get_changed_program, CacheInformation},
|
||||
types::{
|
||||
BodyItem, Expr, FunctionExpression, ImportSelector, ItemVisibility, Node, NodeRef, TagDeclarator, TagNode,
|
||||
},
|
||||
},
|
||||
settings::types::UnitLength,
|
||||
source_range::{ModuleId, SourceRange},
|
||||
@ -42,6 +39,10 @@ use crate::{
|
||||
ExecError, Program,
|
||||
};
|
||||
|
||||
mod exec_ast;
|
||||
mod function_param;
|
||||
mod kcl_value;
|
||||
|
||||
/// State for executing a program.
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
@ -1659,6 +1660,17 @@ impl ExecutorContext {
|
||||
let engine: Arc<Box<dyn EngineManager>> =
|
||||
Arc::new(Box::new(crate::engine::conn::EngineConnection::new(ws).await?));
|
||||
|
||||
// Set the edge visibility.
|
||||
engine
|
||||
.batch_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
SourceRange::default(),
|
||||
&ModelingCmd::from(mcmd::EdgeLinesVisible {
|
||||
hidden: !settings.highlight_edges,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Self {
|
||||
engine,
|
||||
fs: Arc::new(FileManager::new()),
|
||||
@ -1685,7 +1697,7 @@ impl ExecutorContext {
|
||||
pub async fn new(
|
||||
engine_manager: crate::engine::conn_wasm::EngineCommandManager,
|
||||
fs_manager: crate::fs::wasm::FileSystemManager,
|
||||
settings: ExecutorSettings,
|
||||
units: UnitLength,
|
||||
) -> Result<Self, String> {
|
||||
Ok(ExecutorContext {
|
||||
engine: Arc::new(Box::new(
|
||||
@ -1695,16 +1707,16 @@ impl ExecutorContext {
|
||||
)),
|
||||
fs: Arc::new(FileManager::new(fs_manager)),
|
||||
stdlib: Arc::new(StdLib::new()),
|
||||
settings,
|
||||
settings: ExecutorSettings {
|
||||
units,
|
||||
..Default::default()
|
||||
},
|
||||
context_type: ContextType::Live,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn new_mock(
|
||||
fs_manager: crate::fs::wasm::FileSystemManager,
|
||||
settings: ExecutorSettings,
|
||||
) -> Result<Self, String> {
|
||||
pub async fn new_mock(fs_manager: crate::fs::wasm::FileSystemManager, units: UnitLength) -> Result<Self, String> {
|
||||
Ok(ExecutorContext {
|
||||
engine: Arc::new(Box::new(
|
||||
crate::engine::conn_mock::EngineConnection::new()
|
||||
@ -1713,7 +1725,10 @@ impl ExecutorContext {
|
||||
)),
|
||||
fs: Arc::new(FileManager::new(fs_manager)),
|
||||
stdlib: Arc::new(StdLib::new()),
|
||||
settings,
|
||||
settings: ExecutorSettings {
|
||||
units,
|
||||
..Default::default()
|
||||
},
|
||||
context_type: ContextType::Mock,
|
||||
})
|
||||
}
|
||||
@ -1802,71 +1817,6 @@ impl ExecutorContext {
|
||||
// AND if we aren't in wasm it doesn't really matter.
|
||||
Ok(())
|
||||
}
|
||||
// Given an old ast, old program memory and new ast, find the parts of the code that need to be
|
||||
// re-executed.
|
||||
// This function should never error, because in the case of any internal error, we should just pop
|
||||
// the cache.
|
||||
pub async fn get_changed_program(&self, info: CacheInformation) -> Option<CacheResult> {
|
||||
let Some(old) = info.old else {
|
||||
// We have no old info, we need to re-execute the whole thing.
|
||||
return Some(CacheResult {
|
||||
clear_scene: true,
|
||||
program: info.new_ast,
|
||||
});
|
||||
};
|
||||
|
||||
// If the settings are different we might need to bust the cache.
|
||||
// We specifically do this before checking if they are the exact same.
|
||||
if old.settings != self.settings {
|
||||
// If the units are different we need to re-execute the whole thing.
|
||||
if old.settings.units != self.settings.units {
|
||||
return Some(CacheResult {
|
||||
clear_scene: true,
|
||||
program: info.new_ast,
|
||||
});
|
||||
}
|
||||
|
||||
// If anything else is different we do not need to re-execute, but rather just
|
||||
// run the settings again.
|
||||
|
||||
if self
|
||||
.engine
|
||||
.reapply_settings(&self.settings, Default::default())
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
// Bust the cache, we errored.
|
||||
return Some(CacheResult {
|
||||
clear_scene: true,
|
||||
program: info.new_ast,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If the ASTs are the EXACT same we return None.
|
||||
// We don't even need to waste time computing the digests.
|
||||
if old.ast == info.new_ast {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut old_ast = old.ast.inner;
|
||||
old_ast.compute_digest();
|
||||
let mut new_ast = info.new_ast.inner.clone();
|
||||
new_ast.compute_digest();
|
||||
|
||||
// Check if the digest is the same.
|
||||
if old_ast.digest == new_ast.digest {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Check if the changes were only to Non-code areas, like comments or whitespace.
|
||||
|
||||
// For any unhandled cases just re-execute the whole thing.
|
||||
Some(CacheResult {
|
||||
clear_scene: true,
|
||||
program: info.new_ast,
|
||||
})
|
||||
}
|
||||
|
||||
/// Perform the execution of a program.
|
||||
/// You can optionally pass in some initialization memory.
|
||||
@ -1887,7 +1837,7 @@ impl ExecutorContext {
|
||||
let _stats = crate::log::LogPerfStats::new("Interpretation");
|
||||
|
||||
// Get the program that actually changed from the old and new information.
|
||||
let cache_result = self.get_changed_program(cache_info.clone()).await;
|
||||
let cache_result = get_changed_program(cache_info.clone(), &self.settings);
|
||||
|
||||
// Check if we don't need to re-execute.
|
||||
let Some(cache_result) = cache_result else {
|
||||
@ -1904,9 +1854,23 @@ impl ExecutorContext {
|
||||
|
||||
// TODO: Use the top-level file's path.
|
||||
exec_state.add_module(std::path::PathBuf::from(""));
|
||||
|
||||
// Re-apply the settings, in case the cache was busted.
|
||||
self.engine.reapply_settings(&self.settings, Default::default()).await?;
|
||||
// Before we even start executing the program, set the units.
|
||||
self.engine
|
||||
.batch_modeling_cmd(
|
||||
exec_state.id_generator.next_uuid(),
|
||||
SourceRange::default(),
|
||||
&ModelingCmd::from(mcmd::SetSceneUnits {
|
||||
unit: match self.settings.units {
|
||||
UnitLength::Cm => kcmc::units::UnitLength::Centimeters,
|
||||
UnitLength::Ft => kcmc::units::UnitLength::Feet,
|
||||
UnitLength::In => kcmc::units::UnitLength::Inches,
|
||||
UnitLength::M => kcmc::units::UnitLength::Meters,
|
||||
UnitLength::Mm => kcmc::units::UnitLength::Millimeters,
|
||||
UnitLength::Yd => kcmc::units::UnitLength::Yards,
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.inner_execute(&cache_result.program, exec_state, crate::execution::BodyType::Root)
|
||||
.await?;
|
||||
@ -2183,8 +2147,23 @@ impl ExecutorContext {
|
||||
self.settings.units = units;
|
||||
}
|
||||
|
||||
/// Get a snapshot of the current scene.
|
||||
pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
|
||||
/// Execute the program, then get a PNG screenshot.
|
||||
pub async fn execute_and_prepare_snapshot(
|
||||
&self,
|
||||
program: &Program,
|
||||
exec_state: &mut ExecState,
|
||||
) -> std::result::Result<TakeSnapshot, ExecError> {
|
||||
self.execute_and_prepare(program, exec_state).await
|
||||
}
|
||||
|
||||
/// Execute the program, return the interpreter and outputs.
|
||||
pub async fn execute_and_prepare(
|
||||
&self,
|
||||
program: &Program,
|
||||
exec_state: &mut ExecState,
|
||||
) -> std::result::Result<TakeSnapshot, ExecError> {
|
||||
self.run(program.clone().into(), exec_state).await?;
|
||||
|
||||
// Zoom to fit.
|
||||
self.engine
|
||||
.send_modeling_cmd(
|
||||
@ -2220,17 +2199,6 @@ impl ExecutorContext {
|
||||
};
|
||||
Ok(contents)
|
||||
}
|
||||
|
||||
/// Execute the program, then get a PNG screenshot.
|
||||
pub async fn execute_and_prepare_snapshot(
|
||||
&self,
|
||||
program: &Program,
|
||||
exec_state: &mut ExecState,
|
||||
) -> std::result::Result<TakeSnapshot, ExecError> {
|
||||
self.run(program.clone().into(), exec_state).await?;
|
||||
|
||||
self.prepare_snapshot().await
|
||||
}
|
||||
}
|
||||
|
||||
/// For each argument given,
|
||||
@ -2285,6 +2253,59 @@ fn assign_args_to_params(
|
||||
Ok(fn_memory)
|
||||
}
|
||||
|
||||
fn assign_args_to_params_kw(
|
||||
function_expression: NodeRef<'_, FunctionExpression>,
|
||||
mut args: crate::std::args::KwArgs,
|
||||
mut fn_memory: ProgramMemory,
|
||||
) -> Result<ProgramMemory, KclError> {
|
||||
// Add the arguments to the memory. A new call frame should have already
|
||||
// been created.
|
||||
let source_ranges = vec![function_expression.into()];
|
||||
for param in function_expression.params.iter() {
|
||||
if param.labeled {
|
||||
let arg = args.labeled.get(¶m.identifier.name);
|
||||
let arg_val = match arg {
|
||||
Some(arg) => arg.value.clone(),
|
||||
None => match param.default_value {
|
||||
Some(ref default_val) => KclValue::from(default_val.clone()),
|
||||
None => {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
source_ranges,
|
||||
message: format!(
|
||||
"This function requires a parameter {}, but you haven't passed it one.",
|
||||
param.identifier.name
|
||||
),
|
||||
}));
|
||||
}
|
||||
},
|
||||
};
|
||||
fn_memory.add(¶m.identifier.name, arg_val, (¶m.identifier).into())?;
|
||||
} else {
|
||||
let Some(unlabeled) = args.unlabeled.take() else {
|
||||
let param_name = ¶m.identifier.name;
|
||||
return Err(if args.labeled.contains_key(param_name) {
|
||||
KclError::Semantic(KclErrorDetails {
|
||||
source_ranges,
|
||||
message: format!("The function does declare a parameter named '{param_name}', but this parameter doesn't use a label. Try removing the `{param_name}:`"),
|
||||
})
|
||||
} else {
|
||||
KclError::Semantic(KclErrorDetails {
|
||||
source_ranges,
|
||||
message: "This function expects an unlabeled first parameter, but you haven't passed it one."
|
||||
.to_owned(),
|
||||
})
|
||||
});
|
||||
};
|
||||
fn_memory.add(
|
||||
¶m.identifier.name,
|
||||
unlabeled.value.clone(),
|
||||
(¶m.identifier).into(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(fn_memory)
|
||||
}
|
||||
|
||||
pub(crate) async fn call_user_defined_function(
|
||||
args: Vec<Arg>,
|
||||
memory: &ProgramMemory,
|
||||
@ -2315,6 +2336,36 @@ pub(crate) async fn call_user_defined_function(
|
||||
result.map(|_| fn_memory.return_)
|
||||
}
|
||||
|
||||
pub(crate) async fn call_user_defined_function_kw(
|
||||
args: crate::std::args::KwArgs,
|
||||
memory: &ProgramMemory,
|
||||
function_expression: NodeRef<'_, FunctionExpression>,
|
||||
exec_state: &mut ExecState,
|
||||
ctx: &ExecutorContext,
|
||||
) -> Result<Option<KclValue>, KclError> {
|
||||
// Create a new environment to execute the function body in so that local
|
||||
// variables shadow variables in the parent scope. The new environment's
|
||||
// parent should be the environment of the closure.
|
||||
let mut body_memory = memory.clone();
|
||||
let body_env = body_memory.new_env_for_call(memory.current_env);
|
||||
body_memory.current_env = body_env;
|
||||
let fn_memory = assign_args_to_params_kw(function_expression, args, body_memory)?;
|
||||
|
||||
// Execute the function body using the memory we just created.
|
||||
let (result, fn_memory) = {
|
||||
let previous_memory = std::mem::replace(&mut exec_state.memory, fn_memory);
|
||||
let result = ctx
|
||||
.inner_execute(&function_expression.body, exec_state, BodyType::Block)
|
||||
.await;
|
||||
// Restore the previous memory.
|
||||
let fn_memory = std::mem::replace(&mut exec_state.memory, previous_memory);
|
||||
|
||||
(result, fn_memory)
|
||||
};
|
||||
|
||||
result.map(|_| fn_memory.return_)
|
||||
}
|
||||
|
||||
pub enum StatementKind<'a> {
|
||||
Declaration { name: &'a str },
|
||||
Expression,
|
||||
@ -2327,12 +2378,9 @@ mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
parsing::ast::types::{DefaultParamVal, Identifier, Node, Parameter},
|
||||
OldAstState,
|
||||
};
|
||||
use crate::parsing::ast::types::{DefaultParamVal, Identifier, Node, Parameter};
|
||||
|
||||
pub async fn parse_execute(code: &str) -> Result<(Program, ExecutorContext, ExecState)> {
|
||||
pub async fn parse_execute(code: &str) -> Result<ProgramMemory> {
|
||||
let program = Program::parse_no_errs(code)?;
|
||||
|
||||
let ctx = ExecutorContext {
|
||||
@ -2343,9 +2391,9 @@ mod tests {
|
||||
context_type: ContextType::Mock,
|
||||
};
|
||||
let mut exec_state = ExecState::default();
|
||||
ctx.run(program.clone().into(), &mut exec_state).await?;
|
||||
ctx.run(program.into(), &mut exec_state).await?;
|
||||
|
||||
Ok((program, ctx, exec_state))
|
||||
Ok(exec_state.memory)
|
||||
}
|
||||
|
||||
/// Convenience function to get a JSON value from memory and unwrap.
|
||||
@ -2756,39 +2804,36 @@ let shape = layer() |> patternTransform(10, transform, %)
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_math_execute_with_functions() {
|
||||
let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#;
|
||||
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(5.0, mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap());
|
||||
let memory = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(5.0, mem_get_json(&memory, "myVar").as_f64().unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_math_execute() {
|
||||
let ast = r#"const myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
|
||||
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(7.4, mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap());
|
||||
let memory = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(7.4, mem_get_json(&memory, "myVar").as_f64().unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_math_execute_start_negative() {
|
||||
let ast = r#"const myVar = -5 + 6"#;
|
||||
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(1.0, mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap());
|
||||
let memory = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(1.0, mem_get_json(&memory, "myVar").as_f64().unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_math_execute_with_pi() {
|
||||
let ast = r#"const myVar = pi() * 2"#;
|
||||
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(
|
||||
std::f64::consts::TAU,
|
||||
mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap()
|
||||
);
|
||||
let memory = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(std::f64::consts::TAU, mem_get_json(&memory, "myVar").as_f64().unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_math_define_decimal_without_leading_zero() {
|
||||
let ast = r#"let thing = .4 + 7"#;
|
||||
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(7.4, mem_get_json(&exec_state.memory, "thing").as_f64().unwrap());
|
||||
let memory = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(7.4, mem_get_json(&memory, "thing").as_f64().unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -2827,11 +2872,11 @@ fn check = (x) => {
|
||||
}
|
||||
check(false)
|
||||
"#;
|
||||
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(false, mem_get_json(&exec_state.memory, "notTrue").as_bool().unwrap());
|
||||
assert_eq!(true, mem_get_json(&exec_state.memory, "notFalse").as_bool().unwrap());
|
||||
assert_eq!(true, mem_get_json(&exec_state.memory, "c").as_bool().unwrap());
|
||||
assert_eq!(false, mem_get_json(&exec_state.memory, "d").as_bool().unwrap());
|
||||
let mem = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(false, mem_get_json(&mem, "notTrue").as_bool().unwrap());
|
||||
assert_eq!(true, mem_get_json(&mem, "notFalse").as_bool().unwrap());
|
||||
assert_eq!(true, mem_get_json(&mem, "c").as_bool().unwrap());
|
||||
assert_eq!(false, mem_get_json(&mem, "d").as_bool().unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -3213,310 +3258,4 @@ let w = f() + f()
|
||||
let json = serde_json::to_string(&mem).unwrap();
|
||||
assert_eq!(json, r#"{"type":"Solids","value":[]}"#);
|
||||
}
|
||||
|
||||
// Easy case where we have no old ast and memory.
|
||||
// We need to re-execute everything.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_changed_program_no_old_information() {
|
||||
let new = r#"// Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line([24, 0], %)
|
||||
|> line([0, -24], %)
|
||||
|> line([-24, 0], %)
|
||||
|> close(%)
|
||||
|> extrude(6, %)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
let (program, ctx, _) = parse_execute(new).await.unwrap();
|
||||
|
||||
let result = ctx
|
||||
.get_changed_program(CacheInformation {
|
||||
old: None,
|
||||
new_ast: program.ast.clone(),
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(result.is_some());
|
||||
|
||||
let result = result.unwrap();
|
||||
|
||||
assert_eq!(result.program, program.ast);
|
||||
assert!(result.clear_scene);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_changed_program_same_code() {
|
||||
let new = r#"// Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line([24, 0], %)
|
||||
|> line([0, -24], %)
|
||||
|> line([-24, 0], %)
|
||||
|> close(%)
|
||||
|> extrude(6, %)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
|
||||
let (program, ctx, exec_state) = parse_execute(new).await.unwrap();
|
||||
|
||||
let result = ctx
|
||||
.get_changed_program(CacheInformation {
|
||||
old: Some(OldAstState {
|
||||
ast: program.ast.clone(),
|
||||
exec_state,
|
||||
settings: Default::default(),
|
||||
}),
|
||||
new_ast: program.ast.clone(),
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_changed_program_same_code_changed_whitespace() {
|
||||
let old = r#" // Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line([24, 0], %)
|
||||
|> line([0, -24], %)
|
||||
|> line([-24, 0], %)
|
||||
|> close(%)
|
||||
|> extrude(6, %)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
|
||||
|
||||
let new = r#"// Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line([24, 0], %)
|
||||
|> line([0, -24], %)
|
||||
|> line([-24, 0], %)
|
||||
|> close(%)
|
||||
|> extrude(6, %)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
|
||||
let (program_old, ctx, exec_state) = parse_execute(old).await.unwrap();
|
||||
|
||||
let program_new = crate::Program::parse_no_errs(new).unwrap();
|
||||
|
||||
let result = ctx
|
||||
.get_changed_program(CacheInformation {
|
||||
old: Some(OldAstState {
|
||||
ast: program_old.ast.clone(),
|
||||
exec_state,
|
||||
settings: Default::default(),
|
||||
}),
|
||||
new_ast: program_new.ast.clone(),
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
|
||||
let old = r#" // Removed the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line([24, 0], %)
|
||||
|> line([0, -24], %)
|
||||
|> line([-24, 0], %)
|
||||
|> close(%)
|
||||
|> extrude(6, %)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
|
||||
|
||||
let new = r#"// Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line([24, 0], %)
|
||||
|> line([0, -24], %)
|
||||
|> line([-24, 0], %)
|
||||
|> close(%)
|
||||
|> extrude(6, %)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
|
||||
let (program, ctx, exec_state) = parse_execute(old).await.unwrap();
|
||||
|
||||
let program_new = crate::Program::parse_no_errs(new).unwrap();
|
||||
|
||||
let result = ctx
|
||||
.get_changed_program(CacheInformation {
|
||||
old: Some(OldAstState {
|
||||
ast: program.ast.clone(),
|
||||
exec_state,
|
||||
settings: Default::default(),
|
||||
}),
|
||||
new_ast: program_new.ast.clone(),
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_changed_program_same_code_changed_code_comments() {
|
||||
let old = r#" // Removed the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line([24, 0], %)
|
||||
|> line([0, -24], %)
|
||||
|> line([-24, 0], %) // my thing
|
||||
|> close(%)
|
||||
|> extrude(6, %)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
|
||||
|
||||
let new = r#"// Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line([24, 0], %)
|
||||
|> line([0, -24], %)
|
||||
|> line([-24, 0], %)
|
||||
|> close(%)
|
||||
|> extrude(6, %)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
|
||||
let (program, ctx, exec_state) = parse_execute(old).await.unwrap();
|
||||
|
||||
let program_new = crate::Program::parse_no_errs(new).unwrap();
|
||||
|
||||
let result = ctx
|
||||
.get_changed_program(CacheInformation {
|
||||
old: Some(OldAstState {
|
||||
ast: program.ast.clone(),
|
||||
exec_state,
|
||||
settings: Default::default(),
|
||||
}),
|
||||
new_ast: program_new.ast.clone(),
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(result.is_some());
|
||||
|
||||
let result = result.unwrap();
|
||||
|
||||
assert_eq!(result.program, program_new.ast);
|
||||
assert!(result.clear_scene);
|
||||
}
|
||||
|
||||
// Changing the units with the exact same file should bust the cache.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_changed_program_same_code_but_different_units() {
|
||||
let new = r#"// Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line([24, 0], %)
|
||||
|> line([0, -24], %)
|
||||
|> line([-24, 0], %)
|
||||
|> close(%)
|
||||
|> extrude(6, %)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
|
||||
let (program, mut ctx, exec_state) = parse_execute(new).await.unwrap();
|
||||
|
||||
// Change the settings to cm.
|
||||
ctx.settings.units = crate::UnitLength::Cm;
|
||||
|
||||
let result = ctx
|
||||
.get_changed_program(CacheInformation {
|
||||
old: Some(OldAstState {
|
||||
ast: program.ast.clone(),
|
||||
exec_state,
|
||||
settings: Default::default(),
|
||||
}),
|
||||
new_ast: program.ast.clone(),
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(result.is_some());
|
||||
|
||||
let result = result.unwrap();
|
||||
|
||||
assert_eq!(result.program, program.ast);
|
||||
assert!(result.clear_scene);
|
||||
}
|
||||
|
||||
// Changing the grid settings with the exact same file should NOT bust the cache.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_changed_program_same_code_but_different_grid_setting() {
|
||||
let new = r#"// Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line([24, 0], %)
|
||||
|> line([0, -24], %)
|
||||
|> line([-24, 0], %)
|
||||
|> close(%)
|
||||
|> extrude(6, %)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
|
||||
let (program, mut ctx, exec_state) = parse_execute(new).await.unwrap();
|
||||
|
||||
// Change the settings.
|
||||
ctx.settings.show_grid = !ctx.settings.show_grid;
|
||||
|
||||
let result = ctx
|
||||
.get_changed_program(CacheInformation {
|
||||
old: Some(OldAstState {
|
||||
ast: program.ast.clone(),
|
||||
exec_state,
|
||||
settings: Default::default(),
|
||||
}),
|
||||
new_ast: program.ast.clone(),
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
// Changing the edge visibility settings with the exact same file should NOT bust the cache.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_changed_program_same_code_but_different_edge_visiblity_setting() {
|
||||
let new = r#"// Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line([24, 0], %)
|
||||
|> line([0, -24], %)
|
||||
|> line([-24, 0], %)
|
||||
|> close(%)
|
||||
|> extrude(6, %)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
|
||||
let (program, mut ctx, exec_state) = parse_execute(new).await.unwrap();
|
||||
|
||||
// Change the settings.
|
||||
ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
|
||||
|
||||
let result = ctx
|
||||
.get_changed_program(CacheInformation {
|
||||
old: Some(OldAstState {
|
||||
ast: program.ast.clone(),
|
||||
exec_state,
|
||||
settings: Default::default(),
|
||||
}),
|
||||
new_ast: program.ast.clone(),
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
}
|
||||
|
@ -82,15 +82,16 @@ mod wasm;
|
||||
pub use coredump::CoreDump;
|
||||
pub use engine::{EngineManager, ExecutionKind};
|
||||
pub use errors::{CompilationError, ConnectionError, ExecError, KclError};
|
||||
pub use execution::{
|
||||
cache::{CacheInformation, OldAstState},
|
||||
ExecState, ExecutorContext, ExecutorSettings,
|
||||
};
|
||||
pub use execution::{ExecState, ExecutorContext, ExecutorSettings};
|
||||
pub use lsp::{
|
||||
copilot::Backend as CopilotLspBackend,
|
||||
kcl::{Backend as KclLspBackend, Server as KclLspServerSubCommand},
|
||||
};
|
||||
pub use parsing::ast::{modify::modify_ast_for_sketch, types::FormatOptions};
|
||||
pub use parsing::ast::{
|
||||
cache::{CacheInformation, OldAstState},
|
||||
modify::modify_ast_for_sketch,
|
||||
types::FormatOptions,
|
||||
};
|
||||
pub use settings::types::{project::ProjectConfiguration, Configuration, UnitLength};
|
||||
pub use source_range::{ModuleId, SourceRange};
|
||||
|
||||
|
@ -45,11 +45,14 @@ use crate::{
|
||||
errors::Suggestion,
|
||||
lsp::{backend::Backend as _, util::IntoDiagnostic},
|
||||
parsing::{
|
||||
ast::types::{Expr, Node, VariableKind},
|
||||
ast::{
|
||||
cache::{CacheInformation, OldAstState},
|
||||
types::{Expr, Node, VariableKind},
|
||||
},
|
||||
token::TokenStream,
|
||||
PIPE_OPERATOR,
|
||||
},
|
||||
CacheInformation, ModuleId, OldAstState, Program, SourceRange,
|
||||
ModuleId, Program, SourceRange,
|
||||
};
|
||||
const SEMANTIC_TOKEN_TYPES: [SemanticTokenType; 10] = [
|
||||
SemanticTokenType::NUMBER,
|
||||
|
373
src/wasm-lib/kcl/src/parsing/ast/cache.rs
Normal file
@ -0,0 +1,373 @@
|
||||
//! Functions for helping with caching an ast and finding the parts the changed.
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
execution::ExecState,
|
||||
parsing::ast::types::{Node, Program},
|
||||
};
|
||||
|
||||
/// Information for the caching an AST and smartly re-executing it if we can.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
pub struct CacheInformation {
|
||||
/// The old information.
|
||||
pub old: Option<OldAstState>,
|
||||
/// The new ast to executed.
|
||||
pub new_ast: Node<Program>,
|
||||
}
|
||||
|
||||
/// The old ast and program memory.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
pub struct OldAstState {
|
||||
/// The ast.
|
||||
pub ast: Node<Program>,
|
||||
/// The exec state.
|
||||
pub exec_state: ExecState,
|
||||
/// The last settings used for execution.
|
||||
pub settings: crate::execution::ExecutorSettings,
|
||||
}
|
||||
|
||||
impl From<crate::Program> for CacheInformation {
|
||||
fn from(program: crate::Program) -> Self {
|
||||
CacheInformation {
|
||||
old: None,
|
||||
new_ast: program.ast,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of a cache check.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
pub struct CacheResult {
|
||||
/// Should we clear the scene and start over?
|
||||
pub clear_scene: bool,
|
||||
/// The program that needs to be executed.
|
||||
pub program: Node<Program>,
|
||||
}
|
||||
|
||||
// Given an old ast, old program memory and new ast, find the parts of the code that need to be
|
||||
// re-executed.
|
||||
// This function should never error, because in the case of any internal error, we should just pop
|
||||
// the cache.
|
||||
pub fn get_changed_program(
|
||||
info: CacheInformation,
|
||||
new_settings: &crate::execution::ExecutorSettings,
|
||||
) -> Option<CacheResult> {
|
||||
let Some(old) = info.old else {
|
||||
// We have no old info, we need to re-execute the whole thing.
|
||||
return Some(CacheResult {
|
||||
clear_scene: true,
|
||||
program: info.new_ast,
|
||||
});
|
||||
};
|
||||
|
||||
// If the settings are different we need to bust the cache.
|
||||
// We specifically do this before checking if they are the exact same.
|
||||
if old.settings != *new_settings {
|
||||
return Some(CacheResult {
|
||||
clear_scene: true,
|
||||
program: info.new_ast,
|
||||
});
|
||||
}
|
||||
|
||||
// If the ASTs are the EXACT same we return None.
|
||||
// We don't even need to waste time computing the digests.
|
||||
if old.ast == info.new_ast {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut old_ast = old.ast.inner;
|
||||
old_ast.compute_digest();
|
||||
let mut new_ast = info.new_ast.inner.clone();
|
||||
new_ast.compute_digest();
|
||||
|
||||
// Check if the digest is the same.
|
||||
if old_ast.digest == new_ast.digest {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Check if the changes were only to Non-code areas, like comments or whitespace.
|
||||
|
||||
// For any unhandled cases just re-execute the whole thing.
|
||||
Some(CacheResult {
|
||||
clear_scene: true,
|
||||
program: info.new_ast,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
async fn execute(program: &crate::Program) -> Result<ExecState> {
|
||||
let ctx = crate::execution::ExecutorContext {
|
||||
engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await?)),
|
||||
fs: Arc::new(crate::fs::FileManager::new()),
|
||||
stdlib: Arc::new(crate::std::StdLib::new()),
|
||||
settings: Default::default(),
|
||||
context_type: crate::execution::ContextType::Mock,
|
||||
};
|
||||
let mut exec_state = crate::execution::ExecState::default();
|
||||
ctx.run(program.clone().into(), &mut exec_state).await?;
|
||||
|
||||
Ok(exec_state)
|
||||
}
|
||||
|
||||
// Easy case where we have no old ast and memory.
|
||||
// We need to re-execute everything.
|
||||
#[test]
|
||||
fn test_get_changed_program_no_old_information() {
|
||||
let new = r#"// Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line([24, 0], %)
|
||||
|> line([0, -24], %)
|
||||
|> line([-24, 0], %)
|
||||
|> close(%)
|
||||
|> extrude(6, %)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
let program = crate::Program::parse_no_errs(new).unwrap().ast;
|
||||
|
||||
let result = get_changed_program(
|
||||
CacheInformation {
|
||||
old: None,
|
||||
new_ast: program.clone(),
|
||||
},
|
||||
&Default::default(),
|
||||
);
|
||||
|
||||
assert!(result.is_some());
|
||||
|
||||
let result = result.unwrap();
|
||||
|
||||
assert_eq!(result.program, program);
|
||||
assert!(result.clear_scene);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_changed_program_same_code() {
|
||||
let new = r#"// Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line([24, 0], %)
|
||||
|> line([0, -24], %)
|
||||
|> line([-24, 0], %)
|
||||
|> close(%)
|
||||
|> extrude(6, %)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
let program = crate::Program::parse_no_errs(new).unwrap();
|
||||
|
||||
let executed = execute(&program).await.unwrap();
|
||||
|
||||
let result = get_changed_program(
|
||||
CacheInformation {
|
||||
old: Some(OldAstState {
|
||||
ast: program.ast.clone(),
|
||||
exec_state: executed,
|
||||
settings: Default::default(),
|
||||
}),
|
||||
new_ast: program.ast.clone(),
|
||||
},
|
||||
&Default::default(),
|
||||
);
|
||||
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_changed_program_same_code_changed_whitespace() {
|
||||
let old = r#" // Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line([24, 0], %)
|
||||
|> line([0, -24], %)
|
||||
|> line([-24, 0], %)
|
||||
|> close(%)
|
||||
|> extrude(6, %)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
|
||||
|
||||
let new = r#"// Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line([24, 0], %)
|
||||
|> line([0, -24], %)
|
||||
|> line([-24, 0], %)
|
||||
|> close(%)
|
||||
|> extrude(6, %)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
let program_old = crate::Program::parse_no_errs(old).unwrap();
|
||||
|
||||
let executed = execute(&program_old).await.unwrap();
|
||||
|
||||
let program_new = crate::Program::parse_no_errs(new).unwrap();
|
||||
|
||||
let result = get_changed_program(
|
||||
CacheInformation {
|
||||
old: Some(OldAstState {
|
||||
ast: program_old.ast.clone(),
|
||||
exec_state: executed,
|
||||
settings: Default::default(),
|
||||
}),
|
||||
new_ast: program_new.ast.clone(),
|
||||
},
|
||||
&Default::default(),
|
||||
);
|
||||
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
|
||||
let old = r#" // Removed the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line([24, 0], %)
|
||||
|> line([0, -24], %)
|
||||
|> line([-24, 0], %)
|
||||
|> close(%)
|
||||
|> extrude(6, %)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
|
||||
|
||||
let new = r#"// Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line([24, 0], %)
|
||||
|> line([0, -24], %)
|
||||
|> line([-24, 0], %)
|
||||
|> close(%)
|
||||
|> extrude(6, %)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
let program_old = crate::Program::parse_no_errs(old).unwrap();
|
||||
|
||||
let executed = execute(&program_old).await.unwrap();
|
||||
|
||||
let program_new = crate::Program::parse_no_errs(new).unwrap();
|
||||
|
||||
let result = get_changed_program(
|
||||
CacheInformation {
|
||||
old: Some(OldAstState {
|
||||
ast: program_old.ast.clone(),
|
||||
exec_state: executed,
|
||||
settings: Default::default(),
|
||||
}),
|
||||
new_ast: program_new.ast.clone(),
|
||||
},
|
||||
&Default::default(),
|
||||
);
|
||||
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_changed_program_same_code_changed_code_comments() {
|
||||
let old = r#" // Removed the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line([24, 0], %)
|
||||
|> line([0, -24], %)
|
||||
|> line([-24, 0], %) // my thing
|
||||
|> close(%)
|
||||
|> extrude(6, %)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
|
||||
|
||||
let new = r#"// Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line([24, 0], %)
|
||||
|> line([0, -24], %)
|
||||
|> line([-24, 0], %)
|
||||
|> close(%)
|
||||
|> extrude(6, %)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
let program_old = crate::Program::parse_no_errs(old).unwrap();
|
||||
|
||||
let executed = execute(&program_old).await.unwrap();
|
||||
|
||||
let program_new = crate::Program::parse_no_errs(new).unwrap();
|
||||
|
||||
let result = get_changed_program(
|
||||
CacheInformation {
|
||||
old: Some(OldAstState {
|
||||
ast: program_old.ast.clone(),
|
||||
exec_state: executed,
|
||||
settings: Default::default(),
|
||||
}),
|
||||
new_ast: program_new.ast.clone(),
|
||||
},
|
||||
&Default::default(),
|
||||
);
|
||||
|
||||
assert!(result.is_some());
|
||||
|
||||
let result = result.unwrap();
|
||||
|
||||
assert_eq!(result.program, program_new.ast);
|
||||
assert!(result.clear_scene);
|
||||
}
|
||||
|
||||
// Changing the units with the exact same file should bust the cache.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_changed_program_same_code_but_different_units() {
|
||||
let new = r#"// Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
|> startProfileAt([-12, 12], %)
|
||||
|> line([24, 0], %)
|
||||
|> line([0, -24], %)
|
||||
|> line([-24, 0], %)
|
||||
|> close(%)
|
||||
|> extrude(6, %)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
let program = crate::Program::parse_no_errs(new).unwrap();
|
||||
|
||||
let executed = execute(&program).await.unwrap();
|
||||
|
||||
let result = get_changed_program(
|
||||
CacheInformation {
|
||||
old: Some(OldAstState {
|
||||
ast: program.ast.clone(),
|
||||
exec_state: executed,
|
||||
settings: Default::default(),
|
||||
}),
|
||||
new_ast: program.ast.clone(),
|
||||
},
|
||||
&crate::ExecutorSettings {
|
||||
units: crate::UnitLength::Cm,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert!(result.is_some());
|
||||
|
||||
let result = result.unwrap();
|
||||
|
||||
assert_eq!(result.program, program.ast);
|
||||
assert!(result.clear_scene);
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
pub(crate) mod cache;
|
||||
pub(crate) mod digest;
|
||||
pub mod modify;
|
||||
pub mod types;
|
||||
|
@ -2810,7 +2810,8 @@ pub struct Parameter {
|
||||
pub identifier: Node<Identifier>,
|
||||
/// The type of the parameter.
|
||||
/// This is optional if the user defines a type.
|
||||
#[serde(skip)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(skip)]
|
||||
pub type_: Option<FnArgType>,
|
||||
/// Is the parameter optional?
|
||||
/// If so, what is its default value?
|
||||
|
@ -2280,23 +2280,34 @@ struct ParamDescription {
|
||||
labeled: bool,
|
||||
arg_name: Token,
|
||||
type_: std::option::Option<FnArgType>,
|
||||
is_optional: bool,
|
||||
default_value: Option<DefaultParamVal>,
|
||||
}
|
||||
|
||||
fn parameter(i: &mut TokenSlice) -> PResult<ParamDescription> {
|
||||
let (found_at_sign, arg_name, optional, _, type_) = (
|
||||
let (found_at_sign, arg_name, question_mark, _, type_, _ws, default_literal) = (
|
||||
opt(at_sign),
|
||||
any.verify(|token: &Token| !matches!(token.token_type, TokenType::Brace) || token.value != ")"),
|
||||
opt(question_mark),
|
||||
opt(whitespace),
|
||||
opt((colon, opt(whitespace), argument_type).map(|tup| tup.2)),
|
||||
opt(whitespace),
|
||||
opt((equals, opt(whitespace), literal).map(|(_, _, literal)| literal)),
|
||||
)
|
||||
.parse_next(i)?;
|
||||
Ok(ParamDescription {
|
||||
labeled: found_at_sign.is_none(),
|
||||
arg_name,
|
||||
type_,
|
||||
is_optional: optional.is_some(),
|
||||
default_value: match (question_mark.is_some(), default_literal) {
|
||||
(true, Some(lit)) => Some(DefaultParamVal::Literal(lit.inner)),
|
||||
(true, None) => Some(DefaultParamVal::none()),
|
||||
(false, None) => None,
|
||||
(false, Some(lit)) => {
|
||||
let msg = "You're trying to set a default value for an argument, but only optional arguments can have default values, and this argument is mandatory. Try putting a ? after the argument name, to make the argument optional.";
|
||||
let e = CompilationError::fatal((&lit).into(), msg);
|
||||
return Err(ErrMode::Backtrack(ContextError::from(e)));
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -2315,7 +2326,7 @@ fn parameters(i: &mut TokenSlice) -> PResult<Vec<Parameter>> {
|
||||
labeled,
|
||||
arg_name,
|
||||
type_,
|
||||
is_optional,
|
||||
default_value,
|
||||
}| {
|
||||
let identifier =
|
||||
Node::<Identifier>::try_from(arg_name).and_then(Node::<Identifier>::into_valid_binding_name)?;
|
||||
@ -2323,11 +2334,7 @@ fn parameters(i: &mut TokenSlice) -> PResult<Vec<Parameter>> {
|
||||
Ok(Parameter {
|
||||
identifier,
|
||||
type_,
|
||||
default_value: if is_optional {
|
||||
Some(DefaultParamVal::none())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
default_value,
|
||||
labeled,
|
||||
digest: None,
|
||||
})
|
||||
@ -4459,6 +4466,11 @@ my14 = 4 ^ 2 - 3 ^ 2 * 2
|
||||
snapshot_test!(kw_function_all_named, r#"val = foo(x: a, y: b)"#);
|
||||
snapshot_test!(kw_function_decl_all_labeled, r#"fn foo(x, y) { return 1 }"#);
|
||||
snapshot_test!(kw_function_decl_first_unlabeled, r#"fn foo(@x, y) { return 1 }"#);
|
||||
snapshot_test!(kw_function_decl_with_default_no_type, r#"fn foo(x? = 2) { return 1 }"#);
|
||||
snapshot_test!(
|
||||
kw_function_decl_with_default_and_type,
|
||||
r#"fn foo(x?: number = 2) { return 1 }"#
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
|
@ -0,0 +1,76 @@
|
||||
---
|
||||
source: kcl/src/parsing/parser.rs
|
||||
expression: actual
|
||||
snapshot_kind: text
|
||||
---
|
||||
{
|
||||
"body": [
|
||||
{
|
||||
"declaration": {
|
||||
"end": 35,
|
||||
"id": {
|
||||
"end": 6,
|
||||
"name": "foo",
|
||||
"start": 3,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"init": {
|
||||
"body": {
|
||||
"body": [
|
||||
{
|
||||
"argument": {
|
||||
"end": 33,
|
||||
"raw": "1",
|
||||
"start": 32,
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"value": 1.0
|
||||
},
|
||||
"end": 33,
|
||||
"start": 25,
|
||||
"type": "ReturnStatement",
|
||||
"type": "ReturnStatement"
|
||||
}
|
||||
],
|
||||
"end": 35,
|
||||
"start": 23
|
||||
},
|
||||
"end": 35,
|
||||
"params": [
|
||||
{
|
||||
"type": "Parameter",
|
||||
"identifier": {
|
||||
"end": 8,
|
||||
"name": "x",
|
||||
"start": 7,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"type_": {
|
||||
"type": "Primitive",
|
||||
"type": "Number"
|
||||
},
|
||||
"default_value": {
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"value": 2.0,
|
||||
"raw": "2"
|
||||
}
|
||||
}
|
||||
],
|
||||
"start": 6,
|
||||
"type": "FunctionExpression",
|
||||
"type": "FunctionExpression"
|
||||
},
|
||||
"start": 3,
|
||||
"type": "VariableDeclarator"
|
||||
},
|
||||
"end": 35,
|
||||
"kind": "fn",
|
||||
"start": 0,
|
||||
"type": "VariableDeclaration",
|
||||
"type": "VariableDeclaration"
|
||||
}
|
||||
],
|
||||
"end": 35,
|
||||
"start": 0
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
---
|
||||
source: kcl/src/parsing/parser.rs
|
||||
expression: actual
|
||||
snapshot_kind: text
|
||||
---
|
||||
{
|
||||
"body": [
|
||||
{
|
||||
"declaration": {
|
||||
"end": 27,
|
||||
"id": {
|
||||
"end": 6,
|
||||
"name": "foo",
|
||||
"start": 3,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"init": {
|
||||
"body": {
|
||||
"body": [
|
||||
{
|
||||
"argument": {
|
||||
"end": 25,
|
||||
"raw": "1",
|
||||
"start": 24,
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"value": 1.0
|
||||
},
|
||||
"end": 25,
|
||||
"start": 17,
|
||||
"type": "ReturnStatement",
|
||||
"type": "ReturnStatement"
|
||||
}
|
||||
],
|
||||
"end": 27,
|
||||
"start": 15
|
||||
},
|
||||
"end": 27,
|
||||
"params": [
|
||||
{
|
||||
"type": "Parameter",
|
||||
"identifier": {
|
||||
"end": 8,
|
||||
"name": "x",
|
||||
"start": 7,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"default_value": {
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"value": 2.0,
|
||||
"raw": "2"
|
||||
}
|
||||
}
|
||||
],
|
||||
"start": 6,
|
||||
"type": "FunctionExpression",
|
||||
"type": "FunctionExpression"
|
||||
},
|
||||
"start": 3,
|
||||
"type": "VariableDeclarator"
|
||||
},
|
||||
"end": 27,
|
||||
"kind": "fn",
|
||||
"start": 0,
|
||||
"type": "VariableDeclaration",
|
||||
"type": "VariableDeclaration"
|
||||
}
|
||||
],
|
||||
"end": 27,
|
||||
"start": 0
|
||||
}
|
@ -1544,3 +1544,66 @@ mod tag_proxied_through_function_does_not_define_var {
|
||||
super::execute(TEST_NAME, false).await
|
||||
}
|
||||
}
|
||||
mod kw_fn_too_few_args {
|
||||
const TEST_NAME: &str = "kw_fn_too_few_args";
|
||||
|
||||
/// Test parsing KCL.
|
||||
#[test]
|
||||
fn parse() {
|
||||
super::parse(TEST_NAME)
|
||||
}
|
||||
|
||||
/// Test that parsing and unparsing KCL produces the original KCL input.
|
||||
#[test]
|
||||
fn unparse() {
|
||||
super::unparse(TEST_NAME)
|
||||
}
|
||||
|
||||
/// Test that KCL is executed correctly.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn kcl_test_execute() {
|
||||
super::execute(TEST_NAME, false).await
|
||||
}
|
||||
}
|
||||
mod kw_fn_unlabeled_but_has_label {
|
||||
const TEST_NAME: &str = "kw_fn_unlabeled_but_has_label";
|
||||
|
||||
/// Test parsing KCL.
|
||||
#[test]
|
||||
fn parse() {
|
||||
super::parse(TEST_NAME)
|
||||
}
|
||||
|
||||
/// Test that parsing and unparsing KCL produces the original KCL input.
|
||||
#[test]
|
||||
fn unparse() {
|
||||
super::unparse(TEST_NAME)
|
||||
}
|
||||
|
||||
/// Test that KCL is executed correctly.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn kcl_test_execute() {
|
||||
super::execute(TEST_NAME, false).await
|
||||
}
|
||||
}
|
||||
mod kw_fn_with_defaults {
|
||||
const TEST_NAME: &str = "kw_fn_with_defaults";
|
||||
|
||||
/// Test parsing KCL.
|
||||
#[test]
|
||||
fn parse() {
|
||||
super::parse(TEST_NAME)
|
||||
}
|
||||
|
||||
/// Test that parsing and unparsing KCL produces the original KCL input.
|
||||
#[test]
|
||||
fn unparse() {
|
||||
super::unparse(TEST_NAME)
|
||||
}
|
||||
|
||||
/// Test that KCL is executed correctly.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn kcl_test_execute() {
|
||||
super::execute(TEST_NAME, false).await
|
||||
}
|
||||
}
|
||||
|
@ -50,6 +50,13 @@ pub struct KwArgs {
|
||||
pub labeled: HashMap<String, Arg>,
|
||||
}
|
||||
|
||||
impl KwArgs {
|
||||
/// How many arguments are there?
|
||||
pub fn len(&self) -> usize {
|
||||
self.labeled.len() + if self.unlabeled.is_some() { 1 } else { 0 }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Args {
|
||||
/// Positional args.
|
||||
|
@ -55,11 +55,7 @@ async fn do_execute_and_snapshot(
|
||||
program: Program,
|
||||
) -> Result<(crate::execution::ExecState, image::DynamicImage), ExecError> {
|
||||
let mut exec_state = Default::default();
|
||||
let snapshot_png_bytes = ctx
|
||||
.execute_and_prepare_snapshot(&program, &mut exec_state)
|
||||
.await?
|
||||
.contents
|
||||
.0;
|
||||
let snapshot_png_bytes = ctx.execute_and_prepare(&program, &mut exec_state).await?.contents.0;
|
||||
|
||||
// Decode the snapshot, return it.
|
||||
let img = image::ImageReader::new(std::io::Cursor::new(snapshot_png_bytes))
|
||||
|
@ -3,10 +3,10 @@ use std::fmt::Write;
|
||||
use crate::parsing::{
|
||||
ast::types::{
|
||||
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression,
|
||||
CallExpressionKw, Expr, FnArgType, FormatOptions, FunctionExpression, IfExpression, ImportSelector,
|
||||
ImportStatement, ItemVisibility, LabeledArg, Literal, LiteralIdentifier, LiteralValue, MemberExpression,
|
||||
MemberObject, Node, NonCodeValue, ObjectExpression, Parameter, PipeExpression, Program, TagDeclarator,
|
||||
UnaryExpression, VariableDeclaration, VariableKind,
|
||||
CallExpressionKw, DefaultParamVal, Expr, FnArgType, FormatOptions, FunctionExpression, IfExpression,
|
||||
ImportSelector, ImportStatement, ItemVisibility, LabeledArg, Literal, LiteralIdentifier, LiteralValue,
|
||||
MemberExpression, MemberObject, Node, NonCodeValue, ObjectExpression, Parameter, PipeExpression, Program,
|
||||
TagDeclarator, UnaryExpression, VariableDeclaration, VariableKind,
|
||||
},
|
||||
PIPE_OPERATOR,
|
||||
};
|
||||
@ -662,15 +662,18 @@ impl FunctionExpression {
|
||||
|
||||
impl Parameter {
|
||||
pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
|
||||
let mut result = format!(
|
||||
"{}{}",
|
||||
if self.labeled { "" } else { "@" },
|
||||
self.identifier.name.clone()
|
||||
);
|
||||
let at_sign = if self.labeled { "" } else { "@" };
|
||||
let identifier = &self.identifier.name;
|
||||
let question_mark = if self.default_value.is_some() { "?" } else { "" };
|
||||
let mut result = format!("{at_sign}{identifier}{question_mark}");
|
||||
if let Some(ty) = &self.type_ {
|
||||
result += ": ";
|
||||
result += &ty.recast(options, indentation_level);
|
||||
}
|
||||
if let Some(DefaultParamVal::Literal(ref literal)) = self.default_value {
|
||||
let lit = literal.recast();
|
||||
result.push_str(&format!(" = {lit}"));
|
||||
};
|
||||
|
||||
result
|
||||
}
|
||||
|
@ -78,46 +78,179 @@ snapshot_kind: text
|
||||
},
|
||||
{
|
||||
"declaration": {
|
||||
"end": 55,
|
||||
"end": 77,
|
||||
"id": {
|
||||
"end": 40,
|
||||
"end": 43,
|
||||
"name": "add",
|
||||
"start": 40,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"init": {
|
||||
"body": {
|
||||
"body": [
|
||||
{
|
||||
"argument": {
|
||||
"end": 75,
|
||||
"left": {
|
||||
"end": 67,
|
||||
"name": "x",
|
||||
"start": 66,
|
||||
"type": "Identifier",
|
||||
"type": "Identifier"
|
||||
},
|
||||
"operator": "+",
|
||||
"right": {
|
||||
"end": 75,
|
||||
"name": "delta",
|
||||
"start": 70,
|
||||
"type": "Identifier",
|
||||
"type": "Identifier"
|
||||
},
|
||||
"start": 66,
|
||||
"type": "BinaryExpression",
|
||||
"type": "BinaryExpression"
|
||||
},
|
||||
"end": 75,
|
||||
"start": 59,
|
||||
"type": "ReturnStatement",
|
||||
"type": "ReturnStatement"
|
||||
}
|
||||
],
|
||||
"end": 77,
|
||||
"start": 55
|
||||
},
|
||||
"end": 77,
|
||||
"params": [
|
||||
{
|
||||
"type": "Parameter",
|
||||
"identifier": {
|
||||
"end": 46,
|
||||
"name": "x",
|
||||
"start": 45,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"labeled": false
|
||||
},
|
||||
{
|
||||
"type": "Parameter",
|
||||
"identifier": {
|
||||
"end": 53,
|
||||
"name": "delta",
|
||||
"start": 48,
|
||||
"type": "Identifier"
|
||||
}
|
||||
}
|
||||
],
|
||||
"start": 43,
|
||||
"type": "FunctionExpression",
|
||||
"type": "FunctionExpression"
|
||||
},
|
||||
"start": 40,
|
||||
"type": "VariableDeclarator"
|
||||
},
|
||||
"end": 77,
|
||||
"kind": "fn",
|
||||
"start": 37,
|
||||
"type": "VariableDeclaration",
|
||||
"type": "VariableDeclaration"
|
||||
},
|
||||
{
|
||||
"declaration": {
|
||||
"end": 97,
|
||||
"id": {
|
||||
"end": 82,
|
||||
"name": "two",
|
||||
"start": 37,
|
||||
"start": 79,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"init": {
|
||||
"arguments": [
|
||||
{
|
||||
"end": 54,
|
||||
"end": 96,
|
||||
"raw": "1",
|
||||
"start": 53,
|
||||
"start": 95,
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"value": 1.0
|
||||
}
|
||||
],
|
||||
"callee": {
|
||||
"end": 52,
|
||||
"end": 94,
|
||||
"name": "increment",
|
||||
"start": 43,
|
||||
"start": 85,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 55,
|
||||
"start": 43,
|
||||
"end": 97,
|
||||
"start": 85,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
},
|
||||
"start": 37,
|
||||
"start": 79,
|
||||
"type": "VariableDeclarator"
|
||||
},
|
||||
"end": 55,
|
||||
"end": 97,
|
||||
"kind": "const",
|
||||
"start": 37,
|
||||
"start": 79,
|
||||
"type": "VariableDeclaration",
|
||||
"type": "VariableDeclaration"
|
||||
},
|
||||
{
|
||||
"declaration": {
|
||||
"end": 122,
|
||||
"id": {
|
||||
"end": 103,
|
||||
"name": "three",
|
||||
"start": 98,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"init": {
|
||||
"arguments": [
|
||||
{
|
||||
"type": "LabeledArg",
|
||||
"label": {
|
||||
"type": "Identifier",
|
||||
"name": "delta"
|
||||
},
|
||||
"arg": {
|
||||
"end": 121,
|
||||
"raw": "2",
|
||||
"start": 120,
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"value": 2.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"callee": {
|
||||
"end": 109,
|
||||
"name": "add",
|
||||
"start": 106,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 122,
|
||||
"start": 106,
|
||||
"type": "CallExpressionKw",
|
||||
"type": "CallExpressionKw",
|
||||
"unlabeled": {
|
||||
"end": 111,
|
||||
"raw": "1",
|
||||
"start": 110,
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"value": 1.0
|
||||
}
|
||||
},
|
||||
"start": 98,
|
||||
"type": "VariableDeclarator"
|
||||
},
|
||||
"end": 122,
|
||||
"kind": "const",
|
||||
"start": 98,
|
||||
"type": "VariableDeclaration",
|
||||
"type": "VariableDeclaration"
|
||||
}
|
||||
],
|
||||
"end": 56,
|
||||
"end": 123,
|
||||
"nonCodeMeta": {
|
||||
"nonCodeNodes": {
|
||||
"0": [
|
||||
@ -129,6 +262,16 @@ snapshot_kind: text
|
||||
"type": "newLine"
|
||||
}
|
||||
}
|
||||
],
|
||||
"1": [
|
||||
{
|
||||
"end": 79,
|
||||
"start": 77,
|
||||
"type": "NonCodeNode",
|
||||
"value": {
|
||||
"type": "newLine"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"startNodes": []
|
||||
|
@ -2,4 +2,9 @@ fn increment(@x) {
|
||||
return x + 1
|
||||
}
|
||||
|
||||
fn add(@x, delta) {
|
||||
return x + delta
|
||||
}
|
||||
|
||||
two = increment(1)
|
||||
three = add(1, delta: 2)
|
||||
|
@ -27,6 +27,202 @@ snapshot_kind: text
|
||||
"value": 0.0,
|
||||
"__meta": []
|
||||
},
|
||||
"add": {
|
||||
"type": "Function",
|
||||
"expression": {
|
||||
"body": {
|
||||
"body": [
|
||||
{
|
||||
"argument": {
|
||||
"end": 75,
|
||||
"left": {
|
||||
"end": 67,
|
||||
"name": "x",
|
||||
"start": 66,
|
||||
"type": "Identifier",
|
||||
"type": "Identifier"
|
||||
},
|
||||
"operator": "+",
|
||||
"right": {
|
||||
"end": 75,
|
||||
"name": "delta",
|
||||
"start": 70,
|
||||
"type": "Identifier",
|
||||
"type": "Identifier"
|
||||
},
|
||||
"start": 66,
|
||||
"type": "BinaryExpression",
|
||||
"type": "BinaryExpression"
|
||||
},
|
||||
"end": 75,
|
||||
"start": 59,
|
||||
"type": "ReturnStatement",
|
||||
"type": "ReturnStatement"
|
||||
}
|
||||
],
|
||||
"end": 77,
|
||||
"start": 55
|
||||
},
|
||||
"end": 77,
|
||||
"params": [
|
||||
{
|
||||
"type": "Parameter",
|
||||
"identifier": {
|
||||
"end": 46,
|
||||
"name": "x",
|
||||
"start": 45,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"labeled": false
|
||||
},
|
||||
{
|
||||
"type": "Parameter",
|
||||
"identifier": {
|
||||
"end": 53,
|
||||
"name": "delta",
|
||||
"start": 48,
|
||||
"type": "Identifier"
|
||||
}
|
||||
}
|
||||
],
|
||||
"start": 43,
|
||||
"type": "FunctionExpression"
|
||||
},
|
||||
"memory": {
|
||||
"environments": [
|
||||
{
|
||||
"bindings": {
|
||||
"HALF_TURN": {
|
||||
"type": "Number",
|
||||
"value": 180.0,
|
||||
"__meta": []
|
||||
},
|
||||
"QUARTER_TURN": {
|
||||
"type": "Number",
|
||||
"value": 90.0,
|
||||
"__meta": []
|
||||
},
|
||||
"THREE_QUARTER_TURN": {
|
||||
"type": "Number",
|
||||
"value": 270.0,
|
||||
"__meta": []
|
||||
},
|
||||
"ZERO": {
|
||||
"type": "Number",
|
||||
"value": 0.0,
|
||||
"__meta": []
|
||||
},
|
||||
"increment": {
|
||||
"type": "Function",
|
||||
"expression": {
|
||||
"body": {
|
||||
"body": [
|
||||
{
|
||||
"argument": {
|
||||
"end": 33,
|
||||
"left": {
|
||||
"end": 29,
|
||||
"name": "x",
|
||||
"start": 28,
|
||||
"type": "Identifier",
|
||||
"type": "Identifier"
|
||||
},
|
||||
"operator": "+",
|
||||
"right": {
|
||||
"end": 33,
|
||||
"raw": "1",
|
||||
"start": 32,
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"value": 1.0
|
||||
},
|
||||
"start": 28,
|
||||
"type": "BinaryExpression",
|
||||
"type": "BinaryExpression"
|
||||
},
|
||||
"end": 33,
|
||||
"start": 21,
|
||||
"type": "ReturnStatement",
|
||||
"type": "ReturnStatement"
|
||||
}
|
||||
],
|
||||
"end": 35,
|
||||
"start": 17
|
||||
},
|
||||
"end": 35,
|
||||
"params": [
|
||||
{
|
||||
"type": "Parameter",
|
||||
"identifier": {
|
||||
"end": 15,
|
||||
"name": "x",
|
||||
"start": 14,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"labeled": false
|
||||
}
|
||||
],
|
||||
"start": 12,
|
||||
"type": "FunctionExpression"
|
||||
},
|
||||
"memory": {
|
||||
"environments": [
|
||||
{
|
||||
"bindings": {
|
||||
"HALF_TURN": {
|
||||
"type": "Number",
|
||||
"value": 180.0,
|
||||
"__meta": []
|
||||
},
|
||||
"QUARTER_TURN": {
|
||||
"type": "Number",
|
||||
"value": 90.0,
|
||||
"__meta": []
|
||||
},
|
||||
"THREE_QUARTER_TURN": {
|
||||
"type": "Number",
|
||||
"value": 270.0,
|
||||
"__meta": []
|
||||
},
|
||||
"ZERO": {
|
||||
"type": "Number",
|
||||
"value": 0.0,
|
||||
"__meta": []
|
||||
}
|
||||
},
|
||||
"parent": null
|
||||
}
|
||||
],
|
||||
"currentEnv": 0,
|
||||
"return": null
|
||||
},
|
||||
"__meta": [
|
||||
{
|
||||
"sourceRange": [
|
||||
12,
|
||||
35,
|
||||
0
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"parent": null
|
||||
}
|
||||
],
|
||||
"currentEnv": 0,
|
||||
"return": null
|
||||
},
|
||||
"__meta": [
|
||||
{
|
||||
"sourceRange": [
|
||||
43,
|
||||
77,
|
||||
0
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"increment": {
|
||||
"type": "Function",
|
||||
"expression": {
|
||||
@ -121,14 +317,34 @@ snapshot_kind: text
|
||||
}
|
||||
]
|
||||
},
|
||||
"three": {
|
||||
"type": "Number",
|
||||
"value": 3.0,
|
||||
"__meta": [
|
||||
{
|
||||
"sourceRange": [
|
||||
110,
|
||||
111,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"sourceRange": [
|
||||
120,
|
||||
121,
|
||||
0
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"two": {
|
||||
"type": "Number",
|
||||
"value": 2.0,
|
||||
"__meta": [
|
||||
{
|
||||
"sourceRange": [
|
||||
53,
|
||||
54,
|
||||
95,
|
||||
96,
|
||||
0
|
||||
]
|
||||
},
|
||||
|
153
src/wasm-lib/kcl/tests/kw_fn_too_few_args/ast.snap
Normal file
@ -0,0 +1,153 @@
|
||||
---
|
||||
source: kcl/src/simulation_tests.rs
|
||||
description: Result of parsing kw_fn_too_few_args.kcl
|
||||
snapshot_kind: text
|
||||
---
|
||||
{
|
||||
"Ok": {
|
||||
"body": [
|
||||
{
|
||||
"declaration": {
|
||||
"end": 31,
|
||||
"id": {
|
||||
"end": 6,
|
||||
"name": "add",
|
||||
"start": 3,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"init": {
|
||||
"body": {
|
||||
"body": [
|
||||
{
|
||||
"argument": {
|
||||
"end": 29,
|
||||
"left": {
|
||||
"end": 25,
|
||||
"name": "x",
|
||||
"start": 24,
|
||||
"type": "Identifier",
|
||||
"type": "Identifier"
|
||||
},
|
||||
"operator": "+",
|
||||
"right": {
|
||||
"end": 29,
|
||||
"name": "y",
|
||||
"start": 28,
|
||||
"type": "Identifier",
|
||||
"type": "Identifier"
|
||||
},
|
||||
"start": 24,
|
||||
"type": "BinaryExpression",
|
||||
"type": "BinaryExpression"
|
||||
},
|
||||
"end": 29,
|
||||
"start": 17,
|
||||
"type": "ReturnStatement",
|
||||
"type": "ReturnStatement"
|
||||
}
|
||||
],
|
||||
"end": 31,
|
||||
"start": 13
|
||||
},
|
||||
"end": 31,
|
||||
"params": [
|
||||
{
|
||||
"type": "Parameter",
|
||||
"identifier": {
|
||||
"end": 8,
|
||||
"name": "x",
|
||||
"start": 7,
|
||||
"type": "Identifier"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Parameter",
|
||||
"identifier": {
|
||||
"end": 11,
|
||||
"name": "y",
|
||||
"start": 10,
|
||||
"type": "Identifier"
|
||||
}
|
||||
}
|
||||
],
|
||||
"start": 6,
|
||||
"type": "FunctionExpression",
|
||||
"type": "FunctionExpression"
|
||||
},
|
||||
"start": 3,
|
||||
"type": "VariableDeclarator"
|
||||
},
|
||||
"end": 31,
|
||||
"kind": "fn",
|
||||
"start": 0,
|
||||
"type": "VariableDeclaration",
|
||||
"type": "VariableDeclaration"
|
||||
},
|
||||
{
|
||||
"declaration": {
|
||||
"end": 50,
|
||||
"id": {
|
||||
"end": 38,
|
||||
"name": "three",
|
||||
"start": 33,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"init": {
|
||||
"arguments": [
|
||||
{
|
||||
"type": "LabeledArg",
|
||||
"label": {
|
||||
"type": "Identifier",
|
||||
"name": "x"
|
||||
},
|
||||
"arg": {
|
||||
"end": 49,
|
||||
"raw": "1",
|
||||
"start": 48,
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"value": 1.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"callee": {
|
||||
"end": 44,
|
||||
"name": "add",
|
||||
"start": 41,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 50,
|
||||
"start": 41,
|
||||
"type": "CallExpressionKw",
|
||||
"type": "CallExpressionKw",
|
||||
"unlabeled": null
|
||||
},
|
||||
"start": 33,
|
||||
"type": "VariableDeclarator"
|
||||
},
|
||||
"end": 50,
|
||||
"kind": "const",
|
||||
"start": 33,
|
||||
"type": "VariableDeclaration",
|
||||
"type": "VariableDeclaration"
|
||||
}
|
||||
],
|
||||
"end": 51,
|
||||
"nonCodeMeta": {
|
||||
"nonCodeNodes": {
|
||||
"0": [
|
||||
{
|
||||
"end": 33,
|
||||
"start": 31,
|
||||
"type": "NonCodeNode",
|
||||
"value": {
|
||||
"type": "newLine"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"startNodes": []
|
||||
},
|
||||
"start": 0
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
---
|
||||
source: kcl/src/simulation_tests.rs
|
||||
description: Error from executing kw_fn_too_few_args.kcl
|
||||
snapshot_kind: text
|
||||
---
|
||||
KCL Semantic error
|
||||
|
||||
× semantic: This function requires a parameter y, but you haven't passed
|
||||
│ it one.
|
||||
╭─[1:7]
|
||||
1 │ ╭─▶ fn add(x, y) {
|
||||
2 │ │ return x + y
|
||||
3 │ ╰─▶ }
|
||||
4 │
|
||||
5 │ three = add(x: 1)
|
||||
· ─────────
|
||||
╰────
|
5
src/wasm-lib/kcl/tests/kw_fn_too_few_args/input.kcl
Normal file
@ -0,0 +1,5 @@
|
||||
fn add(x, y) {
|
||||
return x + y
|
||||
}
|
||||
|
||||
three = add(x: 1)
|
146
src/wasm-lib/kcl/tests/kw_fn_unlabeled_but_has_label/ast.snap
Normal file
@ -0,0 +1,146 @@
|
||||
---
|
||||
source: kcl/src/simulation_tests.rs
|
||||
description: Result of parsing kw_fn_unlabeled_but_has_label.kcl
|
||||
snapshot_kind: text
|
||||
---
|
||||
{
|
||||
"Ok": {
|
||||
"body": [
|
||||
{
|
||||
"declaration": {
|
||||
"end": 29,
|
||||
"id": {
|
||||
"end": 6,
|
||||
"name": "add",
|
||||
"start": 3,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"init": {
|
||||
"body": {
|
||||
"body": [
|
||||
{
|
||||
"argument": {
|
||||
"end": 27,
|
||||
"left": {
|
||||
"end": 23,
|
||||
"name": "x",
|
||||
"start": 22,
|
||||
"type": "Identifier",
|
||||
"type": "Identifier"
|
||||
},
|
||||
"operator": "+",
|
||||
"right": {
|
||||
"end": 27,
|
||||
"raw": "1",
|
||||
"start": 26,
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"value": 1.0
|
||||
},
|
||||
"start": 22,
|
||||
"type": "BinaryExpression",
|
||||
"type": "BinaryExpression"
|
||||
},
|
||||
"end": 27,
|
||||
"start": 15,
|
||||
"type": "ReturnStatement",
|
||||
"type": "ReturnStatement"
|
||||
}
|
||||
],
|
||||
"end": 29,
|
||||
"start": 11
|
||||
},
|
||||
"end": 29,
|
||||
"params": [
|
||||
{
|
||||
"type": "Parameter",
|
||||
"identifier": {
|
||||
"end": 9,
|
||||
"name": "x",
|
||||
"start": 8,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"labeled": false
|
||||
}
|
||||
],
|
||||
"start": 6,
|
||||
"type": "FunctionExpression",
|
||||
"type": "FunctionExpression"
|
||||
},
|
||||
"start": 3,
|
||||
"type": "VariableDeclarator"
|
||||
},
|
||||
"end": 29,
|
||||
"kind": "fn",
|
||||
"start": 0,
|
||||
"type": "VariableDeclaration",
|
||||
"type": "VariableDeclaration"
|
||||
},
|
||||
{
|
||||
"declaration": {
|
||||
"end": 46,
|
||||
"id": {
|
||||
"end": 34,
|
||||
"name": "two",
|
||||
"start": 31,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"init": {
|
||||
"arguments": [
|
||||
{
|
||||
"type": "LabeledArg",
|
||||
"label": {
|
||||
"type": "Identifier",
|
||||
"name": "x"
|
||||
},
|
||||
"arg": {
|
||||
"end": 45,
|
||||
"raw": "1",
|
||||
"start": 44,
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"value": 1.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"callee": {
|
||||
"end": 40,
|
||||
"name": "add",
|
||||
"start": 37,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 46,
|
||||
"start": 37,
|
||||
"type": "CallExpressionKw",
|
||||
"type": "CallExpressionKw",
|
||||
"unlabeled": null
|
||||
},
|
||||
"start": 31,
|
||||
"type": "VariableDeclarator"
|
||||
},
|
||||
"end": 46,
|
||||
"kind": "const",
|
||||
"start": 31,
|
||||
"type": "VariableDeclaration",
|
||||
"type": "VariableDeclaration"
|
||||
}
|
||||
],
|
||||
"end": 47,
|
||||
"nonCodeMeta": {
|
||||
"nonCodeNodes": {
|
||||
"0": [
|
||||
{
|
||||
"end": 31,
|
||||
"start": 29,
|
||||
"type": "NonCodeNode",
|
||||
"value": {
|
||||
"type": "newLine"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"startNodes": []
|
||||
},
|
||||
"start": 0
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
---
|
||||
source: kcl/src/simulation_tests.rs
|
||||
description: Error from executing kw_fn_unlabeled_but_has_label.kcl
|
||||
snapshot_kind: text
|
||||
---
|
||||
KCL Semantic error
|
||||
|
||||
× semantic: The function does declare a parameter named 'x', but this
|
||||
│ parameter doesn't use a label. Try removing the `x:`
|
||||
╭─[1:7]
|
||||
1 │ ╭─▶ fn add(@x) {
|
||||
2 │ │ return x + 1
|
||||
3 │ ╰─▶ }
|
||||
4 │
|
||||
5 │ two = add(x: 1)
|
||||
· ─────────
|
||||
╰────
|
@ -0,0 +1,5 @@
|
||||
fn add(@x) {
|
||||
return x + 1
|
||||
}
|
||||
|
||||
two = add(x: 1)
|
207
src/wasm-lib/kcl/tests/kw_fn_with_defaults/ast.snap
Normal file
@ -0,0 +1,207 @@
|
||||
---
|
||||
source: kcl/src/simulation_tests.rs
|
||||
description: Result of parsing kw_fn_with_defaults.kcl
|
||||
snapshot_kind: text
|
||||
---
|
||||
{
|
||||
"Ok": {
|
||||
"body": [
|
||||
{
|
||||
"declaration": {
|
||||
"end": 45,
|
||||
"id": {
|
||||
"end": 12,
|
||||
"name": "increment",
|
||||
"start": 3,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"init": {
|
||||
"body": {
|
||||
"body": [
|
||||
{
|
||||
"argument": {
|
||||
"end": 43,
|
||||
"left": {
|
||||
"end": 38,
|
||||
"name": "x",
|
||||
"start": 37,
|
||||
"type": "Identifier",
|
||||
"type": "Identifier"
|
||||
},
|
||||
"operator": "+",
|
||||
"right": {
|
||||
"end": 43,
|
||||
"name": "by",
|
||||
"start": 41,
|
||||
"type": "Identifier",
|
||||
"type": "Identifier"
|
||||
},
|
||||
"start": 37,
|
||||
"type": "BinaryExpression",
|
||||
"type": "BinaryExpression"
|
||||
},
|
||||
"end": 43,
|
||||
"start": 30,
|
||||
"type": "ReturnStatement",
|
||||
"type": "ReturnStatement"
|
||||
}
|
||||
],
|
||||
"end": 45,
|
||||
"start": 26
|
||||
},
|
||||
"end": 45,
|
||||
"params": [
|
||||
{
|
||||
"type": "Parameter",
|
||||
"identifier": {
|
||||
"end": 15,
|
||||
"name": "x",
|
||||
"start": 14,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"labeled": false
|
||||
},
|
||||
{
|
||||
"type": "Parameter",
|
||||
"identifier": {
|
||||
"end": 19,
|
||||
"name": "by",
|
||||
"start": 17,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"default_value": {
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"value": 1.0,
|
||||
"raw": "1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"start": 12,
|
||||
"type": "FunctionExpression",
|
||||
"type": "FunctionExpression"
|
||||
},
|
||||
"start": 3,
|
||||
"type": "VariableDeclarator"
|
||||
},
|
||||
"end": 45,
|
||||
"kind": "fn",
|
||||
"start": 0,
|
||||
"type": "VariableDeclaration",
|
||||
"type": "VariableDeclaration"
|
||||
},
|
||||
{
|
||||
"declaration": {
|
||||
"end": 65,
|
||||
"id": {
|
||||
"end": 50,
|
||||
"name": "two",
|
||||
"start": 47,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"init": {
|
||||
"arguments": [
|
||||
{
|
||||
"end": 64,
|
||||
"raw": "1",
|
||||
"start": 63,
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"value": 1.0
|
||||
}
|
||||
],
|
||||
"callee": {
|
||||
"end": 62,
|
||||
"name": "increment",
|
||||
"start": 53,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 65,
|
||||
"start": 53,
|
||||
"type": "CallExpression",
|
||||
"type": "CallExpression"
|
||||
},
|
||||
"start": 47,
|
||||
"type": "VariableDeclarator"
|
||||
},
|
||||
"end": 65,
|
||||
"kind": "const",
|
||||
"start": 47,
|
||||
"type": "VariableDeclaration",
|
||||
"type": "VariableDeclaration"
|
||||
},
|
||||
{
|
||||
"declaration": {
|
||||
"end": 98,
|
||||
"id": {
|
||||
"end": 75,
|
||||
"name": "twentyOne",
|
||||
"start": 66,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"init": {
|
||||
"arguments": [
|
||||
{
|
||||
"type": "LabeledArg",
|
||||
"label": {
|
||||
"type": "Identifier",
|
||||
"name": "by"
|
||||
},
|
||||
"arg": {
|
||||
"end": 97,
|
||||
"raw": "20",
|
||||
"start": 95,
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"value": 20.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"callee": {
|
||||
"end": 87,
|
||||
"name": "increment",
|
||||
"start": 78,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"end": 98,
|
||||
"start": 78,
|
||||
"type": "CallExpressionKw",
|
||||
"type": "CallExpressionKw",
|
||||
"unlabeled": {
|
||||
"end": 89,
|
||||
"raw": "1",
|
||||
"start": 88,
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"value": 1.0
|
||||
}
|
||||
},
|
||||
"start": 66,
|
||||
"type": "VariableDeclarator"
|
||||
},
|
||||
"end": 98,
|
||||
"kind": "const",
|
||||
"start": 66,
|
||||
"type": "VariableDeclaration",
|
||||
"type": "VariableDeclaration"
|
||||
}
|
||||
],
|
||||
"end": 99,
|
||||
"nonCodeMeta": {
|
||||
"nonCodeNodes": {
|
||||
"0": [
|
||||
{
|
||||
"end": 47,
|
||||
"start": 45,
|
||||
"type": "NonCodeNode",
|
||||
"value": {
|
||||
"type": "newLine"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"startNodes": []
|
||||
},
|
||||
"start": 0
|
||||
}
|
||||
}
|
6
src/wasm-lib/kcl/tests/kw_fn_with_defaults/input.kcl
Normal file
@ -0,0 +1,6 @@
|
||||
fn increment(@x, by? = 1) {
|
||||
return x + by
|
||||
}
|
||||
|
||||
two = increment(1)
|
||||
twentyOne = increment(1, by: 20)
|
177
src/wasm-lib/kcl/tests/kw_fn_with_defaults/program_memory.snap
Normal file
@ -0,0 +1,177 @@
|
||||
---
|
||||
source: kcl/src/simulation_tests.rs
|
||||
description: Program memory after executing kw_fn_with_defaults.kcl
|
||||
snapshot_kind: text
|
||||
---
|
||||
{
|
||||
"environments": [
|
||||
{
|
||||
"bindings": {
|
||||
"HALF_TURN": {
|
||||
"type": "Number",
|
||||
"value": 180.0,
|
||||
"__meta": []
|
||||
},
|
||||
"QUARTER_TURN": {
|
||||
"type": "Number",
|
||||
"value": 90.0,
|
||||
"__meta": []
|
||||
},
|
||||
"THREE_QUARTER_TURN": {
|
||||
"type": "Number",
|
||||
"value": 270.0,
|
||||
"__meta": []
|
||||
},
|
||||
"ZERO": {
|
||||
"type": "Number",
|
||||
"value": 0.0,
|
||||
"__meta": []
|
||||
},
|
||||
"increment": {
|
||||
"type": "Function",
|
||||
"expression": {
|
||||
"body": {
|
||||
"body": [
|
||||
{
|
||||
"argument": {
|
||||
"end": 43,
|
||||
"left": {
|
||||
"end": 38,
|
||||
"name": "x",
|
||||
"start": 37,
|
||||
"type": "Identifier",
|
||||
"type": "Identifier"
|
||||
},
|
||||
"operator": "+",
|
||||
"right": {
|
||||
"end": 43,
|
||||
"name": "by",
|
||||
"start": 41,
|
||||
"type": "Identifier",
|
||||
"type": "Identifier"
|
||||
},
|
||||
"start": 37,
|
||||
"type": "BinaryExpression",
|
||||
"type": "BinaryExpression"
|
||||
},
|
||||
"end": 43,
|
||||
"start": 30,
|
||||
"type": "ReturnStatement",
|
||||
"type": "ReturnStatement"
|
||||
}
|
||||
],
|
||||
"end": 45,
|
||||
"start": 26
|
||||
},
|
||||
"end": 45,
|
||||
"params": [
|
||||
{
|
||||
"type": "Parameter",
|
||||
"identifier": {
|
||||
"end": 15,
|
||||
"name": "x",
|
||||
"start": 14,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"labeled": false
|
||||
},
|
||||
{
|
||||
"type": "Parameter",
|
||||
"identifier": {
|
||||
"end": 19,
|
||||
"name": "by",
|
||||
"start": 17,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"default_value": {
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
"value": 1.0,
|
||||
"raw": "1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"start": 12,
|
||||
"type": "FunctionExpression"
|
||||
},
|
||||
"memory": {
|
||||
"environments": [
|
||||
{
|
||||
"bindings": {
|
||||
"HALF_TURN": {
|
||||
"type": "Number",
|
||||
"value": 180.0,
|
||||
"__meta": []
|
||||
},
|
||||
"QUARTER_TURN": {
|
||||
"type": "Number",
|
||||
"value": 90.0,
|
||||
"__meta": []
|
||||
},
|
||||
"THREE_QUARTER_TURN": {
|
||||
"type": "Number",
|
||||
"value": 270.0,
|
||||
"__meta": []
|
||||
},
|
||||
"ZERO": {
|
||||
"type": "Number",
|
||||
"value": 0.0,
|
||||
"__meta": []
|
||||
}
|
||||
},
|
||||
"parent": null
|
||||
}
|
||||
],
|
||||
"currentEnv": 0,
|
||||
"return": null
|
||||
},
|
||||
"__meta": [
|
||||
{
|
||||
"sourceRange": [
|
||||
12,
|
||||
45,
|
||||
0
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"twentyOne": {
|
||||
"type": "Number",
|
||||
"value": 21.0,
|
||||
"__meta": [
|
||||
{
|
||||
"sourceRange": [
|
||||
88,
|
||||
89,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"sourceRange": [
|
||||
95,
|
||||
97,
|
||||
0
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"two": {
|
||||
"type": "Number",
|
||||
"value": 2.0,
|
||||
"__meta": [
|
||||
{
|
||||
"sourceRange": [
|
||||
63,
|
||||
64,
|
||||
0
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"parent": null
|
||||
}
|
||||
],
|
||||
"currentEnv": 0,
|
||||
"return": null
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
//! Wasm bindings for `kcl`.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
|
||||
use futures::stream::TryStreamExt;
|
||||
use gloo_utils::format::JsValueSerdeExt;
|
||||
@ -56,10 +56,10 @@ pub async fn clear_scene_and_bust_cache(
|
||||
|
||||
// wasm_bindgen wrapper for execute
|
||||
#[wasm_bindgen]
|
||||
pub async fn execute(
|
||||
pub async fn execute_wasm(
|
||||
program_ast_json: &str,
|
||||
program_memory_override_str: &str,
|
||||
settings: &str,
|
||||
units: &str,
|
||||
engine_manager: kcl_lib::wasm_engine::EngineCommandManager,
|
||||
fs_manager: kcl_lib::wasm_engine::FileSystemManager,
|
||||
) -> Result<JsValue, String> {
|
||||
@ -73,11 +73,11 @@ pub async fn execute(
|
||||
// You cannot override the memory in non-mock mode.
|
||||
let is_mock = program_memory_override.is_some();
|
||||
|
||||
let settings: kcl_lib::Configuration = serde_json::from_str(settings).map_err(|e| e.to_string())?;
|
||||
let units = kcl_lib::UnitLength::from_str(units).map_err(|e| e.to_string())?;
|
||||
let ctx = if is_mock {
|
||||
kcl_lib::ExecutorContext::new_mock(fs_manager, settings.into()).await?
|
||||
kcl_lib::ExecutorContext::new_mock(fs_manager, units).await?
|
||||
} else {
|
||||
kcl_lib::ExecutorContext::new(engine_manager, fs_manager, settings.into()).await?
|
||||
kcl_lib::ExecutorContext::new(engine_manager, fs_manager, units).await?
|
||||
};
|
||||
|
||||
let mut exec_state = ExecState::default();
|
||||
@ -168,6 +168,23 @@ pub async fn make_default_planes(
|
||||
JsValue::from_serde(&default_planes).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// wasm_bindgen wrapper for modifying the grid
|
||||
#[wasm_bindgen]
|
||||
pub async fn modify_grid(
|
||||
engine_manager: kcl_lib::wasm_engine::EngineCommandManager,
|
||||
hidden: bool,
|
||||
) -> Result<(), String> {
|
||||
console_error_panic_hook::set_once();
|
||||
// deserialize the ast from a stringified json
|
||||
|
||||
let engine = kcl_lib::wasm_engine::EngineConnection::new(engine_manager)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))?;
|
||||
engine.modify_grid(hidden).await.map_err(String::from)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// wasm_bindgen wrapper for execute
|
||||
#[wasm_bindgen]
|
||||
pub async fn modify_ast_for_sketch_wasm(
|
||||
@ -279,7 +296,7 @@ impl ServerConfig {
|
||||
pub async fn kcl_lsp_run(
|
||||
config: ServerConfig,
|
||||
engine_manager: Option<kcl_lib::wasm_engine::EngineCommandManager>,
|
||||
settings: Option<String>,
|
||||
units: &str,
|
||||
token: String,
|
||||
baseurl: String,
|
||||
) -> Result<(), JsValue> {
|
||||
@ -292,12 +309,8 @@ pub async fn kcl_lsp_run(
|
||||
} = config;
|
||||
|
||||
let executor_ctx = if let Some(engine_manager) = engine_manager {
|
||||
let settings: kcl_lib::Configuration = if let Some(settings) = settings {
|
||||
serde_json::from_str(&settings).map_err(|e| e.to_string())?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
Some(kcl_lib::ExecutorContext::new(engine_manager, fs.clone(), settings.into()).await?)
|
||||
let units = kcl_lib::UnitLength::from_str(units).map_err(|e| e.to_string())?;
|
||||
Some(kcl_lib::ExecutorContext::new(engine_manager, fs.clone(), units).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
@ -1,216 +0,0 @@
|
||||
//! Cache testing framework.
|
||||
|
||||
use anyhow::Result;
|
||||
use kcl_lib::ExecError;
|
||||
|
||||
struct Variation<'a> {
|
||||
code: &'a str,
|
||||
settings: &'a kcl_lib::ExecutorSettings,
|
||||
}
|
||||
|
||||
async fn cache_test(test_name: &str, variations: Vec<Variation<'_>>) -> Result<Vec<(String, image::DynamicImage)>> {
|
||||
let first = variations
|
||||
.first()
|
||||
.ok_or_else(|| anyhow::anyhow!("No variations provided for test '{}'", test_name))?;
|
||||
|
||||
let mut ctx = kcl_lib::ExecutorContext::new_with_client(first.settings.clone(), None, None).await?;
|
||||
let mut exec_state = kcl_lib::ExecState::default();
|
||||
|
||||
let mut old_ast_state = None;
|
||||
let mut img_results = Vec::new();
|
||||
for (index, variation) in variations.iter().enumerate() {
|
||||
let program = kcl_lib::Program::parse_no_errs(variation.code)?;
|
||||
|
||||
// set the new settings.
|
||||
ctx.settings = variation.settings.clone();
|
||||
|
||||
ctx.run(
|
||||
kcl_lib::CacheInformation {
|
||||
old: old_ast_state,
|
||||
new_ast: program.ast.clone(),
|
||||
},
|
||||
&mut exec_state,
|
||||
)
|
||||
.await?;
|
||||
let snapshot_png_bytes = ctx.prepare_snapshot().await?.contents.0;
|
||||
|
||||
// Decode the snapshot, return it.
|
||||
let img = image::ImageReader::new(std::io::Cursor::new(snapshot_png_bytes))
|
||||
.with_guessed_format()
|
||||
.map_err(|e| ExecError::BadPng(e.to_string()))
|
||||
.and_then(|x| x.decode().map_err(|e| ExecError::BadPng(e.to_string())))?;
|
||||
// Save the snapshot.
|
||||
let path = crate::assert_out(&format!("cache_{}_{}", test_name, index), &img);
|
||||
|
||||
img_results.push((path, img));
|
||||
|
||||
// Prepare the last state.
|
||||
old_ast_state = Some(kcl_lib::OldAstState {
|
||||
ast: program.ast,
|
||||
exec_state: exec_state.clone(),
|
||||
settings: variation.settings.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(img_results)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn kcl_test_cache_change_units_changes_output() {
|
||||
let code = r#"part001 = startSketchOn('XY')
|
||||
|> startProfileAt([5.5229, 5.25217], %)
|
||||
|> line([10.50433, -1.19122], %)
|
||||
|> line([8.01362, -5.48731], %)
|
||||
|> line([-1.02877, -6.76825], %)
|
||||
|> line([-11.53311, 2.81559], %)
|
||||
|> close(%)
|
||||
|> extrude(4, %)
|
||||
"#;
|
||||
|
||||
let result = cache_test(
|
||||
"change_units_changes_output",
|
||||
vec![
|
||||
Variation {
|
||||
code,
|
||||
settings: &kcl_lib::ExecutorSettings {
|
||||
units: kcl_lib::UnitLength::In,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
Variation {
|
||||
code,
|
||||
settings: &kcl_lib::ExecutorSettings {
|
||||
units: kcl_lib::UnitLength::Mm,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let first = result.first().unwrap();
|
||||
let second = result.last().unwrap();
|
||||
|
||||
assert!(first.1 != second.1);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn kcl_test_cache_change_grid_visualizes_grid_off_to_on() {
|
||||
let code = r#"part001 = startSketchOn('XY')
|
||||
|> startProfileAt([5.5229, 5.25217], %)
|
||||
|> line([10.50433, -1.19122], %)
|
||||
|> line([8.01362, -5.48731], %)
|
||||
|> line([-1.02877, -6.76825], %)
|
||||
|> line([-11.53311, 2.81559], %)
|
||||
|> close(%)
|
||||
|> extrude(4, %)
|
||||
"#;
|
||||
|
||||
let result = cache_test(
|
||||
"change_grid_visualizes_grid_off_to_on",
|
||||
vec![
|
||||
Variation {
|
||||
code,
|
||||
settings: &kcl_lib::ExecutorSettings {
|
||||
show_grid: false,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
Variation {
|
||||
code,
|
||||
settings: &kcl_lib::ExecutorSettings {
|
||||
show_grid: true,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let first = result.first().unwrap();
|
||||
let second = result.last().unwrap();
|
||||
|
||||
assert!(first.1 != second.1);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn kcl_test_cache_change_grid_visualizes_grid_on_to_off() {
|
||||
let code = r#"part001 = startSketchOn('XY')
|
||||
|> startProfileAt([5.5229, 5.25217], %)
|
||||
|> line([10.50433, -1.19122], %)
|
||||
|> line([8.01362, -5.48731], %)
|
||||
|> line([-1.02877, -6.76825], %)
|
||||
|> line([-11.53311, 2.81559], %)
|
||||
|> close(%)
|
||||
|> extrude(4, %)
|
||||
"#;
|
||||
|
||||
let result = cache_test(
|
||||
"change_grid_visualizes_grid_on_to_off",
|
||||
vec![
|
||||
Variation {
|
||||
code,
|
||||
settings: &kcl_lib::ExecutorSettings {
|
||||
show_grid: true,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
Variation {
|
||||
code,
|
||||
settings: &kcl_lib::ExecutorSettings {
|
||||
show_grid: false,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let first = result.first().unwrap();
|
||||
let second = result.last().unwrap();
|
||||
|
||||
assert!(first.1 != second.1);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn kcl_test_cache_change_highlight_edges_changes_visual() {
|
||||
let code = r#"part001 = startSketchOn('XY')
|
||||
|> startProfileAt([5.5229, 5.25217], %)
|
||||
|> line([10.50433, -1.19122], %)
|
||||
|> line([8.01362, -5.48731], %)
|
||||
|> line([-1.02877, -6.76825], %)
|
||||
|> line([-11.53311, 2.81559], %)
|
||||
|> close(%)
|
||||
|> extrude(4, %)
|
||||
"#;
|
||||
|
||||
let result = cache_test(
|
||||
"change_highlight_edges_changes_visual",
|
||||
vec![
|
||||
Variation {
|
||||
code,
|
||||
settings: &kcl_lib::ExecutorSettings {
|
||||
highlight_edges: true,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
Variation {
|
||||
code,
|
||||
settings: &kcl_lib::ExecutorSettings {
|
||||
highlight_edges: false,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let first = result.first().unwrap();
|
||||
let second = result.last().unwrap();
|
||||
|
||||
assert!(first.1 != second.1);
|
||||
}
|
@ -1,5 +1,3 @@
|
||||
mod cache;
|
||||
|
||||
use kcl_lib::{
|
||||
test_server::{execute_and_snapshot, execute_and_snapshot_no_auth},
|
||||
UnitLength,
|
||||
@ -7,7 +5,7 @@ use kcl_lib::{
|
||||
|
||||
/// The minimum permissible difference between asserted twenty-twenty images.
|
||||
/// i.e. how different the current model snapshot can be from the previous saved one.
|
||||
pub(crate) const MIN_DIFF: f64 = 0.99;
|
||||
const MIN_DIFF: f64 = 0.99;
|
||||
|
||||
macro_rules! kcl_input {
|
||||
($file:literal) => {
|
||||
@ -15,11 +13,8 @@ macro_rules! kcl_input {
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn assert_out(test_name: &str, result: &image::DynamicImage) -> String {
|
||||
let path = format!("tests/executor/outputs/{test_name}.png");
|
||||
twenty_twenty::assert_image(&path, result, MIN_DIFF);
|
||||
|
||||
path
|
||||
fn assert_out(test_name: &str, result: &image::DynamicImage) {
|
||||
twenty_twenty::assert_image(format!("tests/executor/outputs/{test_name}.png"), result, MIN_DIFF);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
|
Before Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 114 KiB |
Before Width: | Height: | Size: 114 KiB |
Before Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 47 KiB |