Compare commits
31 Commits
nightly-v2
...
paultag/di
Author | SHA1 | Date | |
---|---|---|---|
23df7e5429 | |||
7ed26e21c6 | |||
c668d40efc | |||
f38c6b90b7 | |||
7bc8bae0ec | |||
3804aca27e | |||
b127680f2f | |||
b7de8e60cf | |||
058fccb5e1 | |||
0006d72973 | |||
00e97257ae | |||
aeb656d176 | |||
ac49ebd6e0 | |||
b40f03ad25 | |||
a8ad86e645 | |||
87f50cd5e9 | |||
0400e6228e | |||
26f150fd6c | |||
3049f405f5 | |||
53d40301dc | |||
671c01e36f | |||
e80151979b | |||
668e2afb99 | |||
ea57de0074 | |||
548c664db0 | |||
d3a3f4410c | |||
22eb343171 | |||
f2cfa4d5cf | |||
3f1f40eeba | |||
ff2d161606 | |||
210c78029d |
3
.github/workflows/build-apps.yml
vendored
@ -165,7 +165,6 @@ jobs:
|
||||
- name: Build the app (release)
|
||||
if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }}
|
||||
env:
|
||||
PUBLISH_FOR_PULL_REQUEST: true
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
@ -173,7 +172,6 @@ jobs:
|
||||
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
CSC_FOR_PULL_REQUEST: true
|
||||
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
|
||||
run: yarn electron-builder --config --publish always
|
||||
|
||||
@ -229,7 +227,6 @@ jobs:
|
||||
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
CSC_FOR_PULL_REQUEST: true
|
||||
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
|
||||
run: yarn electron-builder --config --publish always
|
||||
|
||||
|
2
.github/workflows/cargo-test.yml
vendored
@ -71,7 +71,7 @@ jobs:
|
||||
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
|
||||
RUST_MIN_STACK: 10485760000
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{secrets.CODECOV_TOKEN}}
|
||||
fail_ci_if_error: true
|
||||
|
@ -22,3 +22,5 @@ once fixed in engine will just start working here with no language changes.
|
||||
|
||||
- **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple
|
||||
chamfer cases work currently.
|
||||
|
||||
- **Appearance**: Changing the appearance on a loft does not work.
|
||||
|
239
docs/kcl/appearance.md
Normal file
@ -19,6 +19,7 @@ layout: manual
|
||||
* [`angledLineThatIntersects`](kcl/angledLineThatIntersects)
|
||||
* [`angledLineToX`](kcl/angledLineToX)
|
||||
* [`angledLineToY`](kcl/angledLineToY)
|
||||
* [`appearance`](kcl/appearance)
|
||||
* [`arc`](kcl/arc)
|
||||
* [`arcTo`](kcl/arcTo)
|
||||
* [`asin`](kcl/asin)
|
||||
@ -101,6 +102,7 @@ layout: manual
|
||||
* [`startProfileAt`](kcl/startProfileAt)
|
||||
* [`startSketchAt`](kcl/startSketchAt)
|
||||
* [`startSketchOn`](kcl/startSketchOn)
|
||||
* [`sweep`](kcl/sweep)
|
||||
* [`tan`](kcl/tan)
|
||||
* [`tangentToEnd`](kcl/tangentToEnd)
|
||||
* [`tangentialArc`](kcl/tangentialArc)
|
||||
|
7144
docs/kcl/std.json
55
docs/kcl/sweep.md
Normal file
23
docs/kcl/types/AppearanceData.md
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
title: "AppearanceData"
|
||||
excerpt: "Data for appearance."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
Data for appearance.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `color` |`string`| Color of the new material, a hex string like "#ff0000". | No |
|
||||
| `metalness` |`number` (**maximum:** 100.0)| Metalness of the new material, a percentage like 95.7. | No |
|
||||
| `roughness` |`number` (**maximum:** 100.0)| Roughness of the new material, a percentage like 95.7. | No |
|
||||
|
||||
|
@ -12,5 +12,10 @@ KCL value for an optional parameter which was not given an argument. (remember,
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
|
||||
|
||||
|
||||
|
23
docs/kcl/types/SweepData.md
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
title: "SweepData"
|
||||
excerpt: "Data for a sweep."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
Data for a sweep.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `path` |[`Sketch`](/docs/kcl/types/Sketch)| The path to sweep along. | No |
|
||||
| `sectional` |`boolean`| If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components. | No |
|
||||
| `tolerance` |`number`| Tolerance for the sweep operation. | No |
|
||||
|
||||
|
@ -1164,3 +1164,109 @@ test.fixme('theme persists', async ({ page, context }) => {
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('code color goober', { tag: '@snapshot' }, () => {
|
||||
test('code color goober', async ({ page, context }) => {
|
||||
const u = await getUtils(page)
|
||||
await context.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`// Create a pipe using a sweep.
|
||||
|
||||
// Create a path for the sweep.
|
||||
sweepPath = startSketchOn('XZ')
|
||||
|> startProfileAt([0.05, 0.05], %)
|
||||
|> line([0, 7], %)
|
||||
|> tangentialArc({ offset = 90, radius = 5 }, %)
|
||||
|> line([-3, 0], %)
|
||||
|> tangentialArc({ offset = -90, radius = 5 }, %)
|
||||
|> line([0, 7], %)
|
||||
|
||||
sweepSketch = startSketchOn('XY')
|
||||
|> startProfileAt([2, 0], %)
|
||||
|> arc({
|
||||
angleEnd = 360,
|
||||
angleStart = 0,
|
||||
radius = 2
|
||||
}, %)
|
||||
|> sweep({
|
||||
path = sweepPath,
|
||||
}, %)
|
||||
|> appearance({
|
||||
color = "#bb00ff",
|
||||
metalness = 90,
|
||||
roughness = 90
|
||||
}, %)
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 1000 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.clearAndCloseDebugPanel()
|
||||
|
||||
await expect(page, 'expect small color widget').toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
})
|
||||
|
||||
test('code color goober opening window', async ({ page, context }) => {
|
||||
const u = await getUtils(page)
|
||||
await context.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`// Create a pipe using a sweep.
|
||||
|
||||
// Create a path for the sweep.
|
||||
sweepPath = startSketchOn('XZ')
|
||||
|> startProfileAt([0.05, 0.05], %)
|
||||
|> line([0, 7], %)
|
||||
|> tangentialArc({ offset = 90, radius = 5 }, %)
|
||||
|> line([-3, 0], %)
|
||||
|> tangentialArc({ offset = -90, radius = 5 }, %)
|
||||
|> line([0, 7], %)
|
||||
|
||||
sweepSketch = startSketchOn('XY')
|
||||
|> startProfileAt([2, 0], %)
|
||||
|> arc({
|
||||
angleEnd = 360,
|
||||
angleStart = 0,
|
||||
radius = 2
|
||||
}, %)
|
||||
|> sweep({
|
||||
path = sweepPath,
|
||||
}, %)
|
||||
|> appearance({
|
||||
color = "#bb00ff",
|
||||
metalness = 90,
|
||||
roughness = 90
|
||||
}, %)
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 1000 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.clearAndCloseDebugPanel()
|
||||
|
||||
await expect(page.locator('.cm-css-color-picker-wrapper')).toBeVisible()
|
||||
|
||||
// Click the color widget
|
||||
await page.locator('.cm-css-color-picker-wrapper input').click()
|
||||
|
||||
await expect(
|
||||
page,
|
||||
'expect small color widget to have window open'
|
||||
).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 144 KiB |
After Width: | Height: | Size: 130 KiB |
After Width: | Height: | Size: 139 KiB |
After Width: | Height: | Size: 124 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
@ -14,7 +14,7 @@ export const TEST_SETTINGS = {
|
||||
},
|
||||
modeling: {
|
||||
defaultUnit: 'in',
|
||||
mouseControls: 'KittyCAD',
|
||||
mouseControls: 'Zoo',
|
||||
cameraProjection: 'perspective',
|
||||
showDebugPanel: true,
|
||||
},
|
||||
|
@ -479,4 +479,26 @@ test.describe('Testing Camera Movement', () => {
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test('Right-click opens context menu when not dragged', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await test.step(`The menu should not show if we drag the mouse`, async () => {
|
||||
await page.mouse.move(900, 200)
|
||||
await page.mouse.down({ button: 'right' })
|
||||
await page.mouse.move(900, 300)
|
||||
await page.mouse.up({ button: 'right' })
|
||||
|
||||
await expect(page.getByTestId('view-controls-menu')).not.toBeVisible()
|
||||
})
|
||||
|
||||
await test.step(`The menu should show if we don't drag the mouse`, async () => {
|
||||
await page.mouse.move(900, 200)
|
||||
await page.mouse.down({ button: 'right' })
|
||||
await page.mouse.up({ button: 'right' })
|
||||
|
||||
await expect(page.getByTestId('view-controls-menu')).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -1,20 +1,9 @@
|
||||
import type { ForgeConfig } from '@electron-forge/shared-types'
|
||||
import { MakerSquirrel } from '@electron-forge/maker-squirrel'
|
||||
import { MakerZIP } from '@electron-forge/maker-zip'
|
||||
import { MakerDeb } from '@electron-forge/maker-deb'
|
||||
import { MakerRpm } from '@electron-forge/maker-rpm'
|
||||
import { VitePlugin } from '@electron-forge/plugin-vite'
|
||||
import { MakerWix, MakerWixConfig } from '@electron-forge/maker-wix'
|
||||
import { FusesPlugin } from '@electron-forge/plugin-fuses'
|
||||
import { FuseV1Options, FuseVersion } from '@electron/fuses'
|
||||
import path from 'path'
|
||||
|
||||
interface ExtendedMakerWixConfig extends MakerWixConfig {
|
||||
// see https://github.com/electron/forge/issues/3673
|
||||
// this is an undocumented property of electron-wix-msi
|
||||
associateExtensions?: string
|
||||
}
|
||||
|
||||
const rootDir = process.cwd()
|
||||
|
||||
const config: ForgeConfig = {
|
||||
@ -39,26 +28,7 @@ const config: ForgeConfig = {
|
||||
extendInfo: 'Info.plist', // Information for file associations.
|
||||
},
|
||||
rebuildConfig: {},
|
||||
makers: [
|
||||
new MakerSquirrel({
|
||||
setupIcon: path.resolve(rootDir, 'assets', 'icon.ico'),
|
||||
}),
|
||||
new MakerWix({
|
||||
icon: path.resolve(rootDir, 'assets', 'icon.ico'),
|
||||
associateExtensions: 'kcl',
|
||||
} as ExtendedMakerWixConfig),
|
||||
new MakerZIP({}, ['darwin']),
|
||||
new MakerRpm({
|
||||
options: {
|
||||
icon: path.resolve(rootDir, 'assets', 'icon.png'),
|
||||
},
|
||||
}),
|
||||
new MakerDeb({
|
||||
options: {
|
||||
icon: path.resolve(rootDir, 'assets', 'icon.png'),
|
||||
},
|
||||
}),
|
||||
],
|
||||
makers: [],
|
||||
plugins: [
|
||||
new VitePlugin({
|
||||
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
|
||||
|
26
package.json
@ -39,7 +39,6 @@
|
||||
"chokidar": "^4.0.1",
|
||||
"codemirror": "^6.0.1",
|
||||
"decamelize": "^6.0.0",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"electron-updater": "6.3.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"html2canvas-pro": "^1.5.8",
|
||||
@ -69,7 +68,7 @@
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"start": "vite --port=3000 --host=0.0.0.0",
|
||||
"start:prod": "vite preview --port=3000",
|
||||
"serve": "vite serve --port=3000",
|
||||
"build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build",
|
||||
@ -104,8 +103,6 @@
|
||||
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
|
||||
"tron:start": "electron-forge start",
|
||||
"tron:package": "electron-forge package",
|
||||
"tron:make": "electron-forge make",
|
||||
"tron:publish": "electron-forge publish",
|
||||
"tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron",
|
||||
"tronb:vite": "vite build -c vite.main.config.ts && vite build -c vite.preload.config.ts && vite build -c vite.renderer.config.ts",
|
||||
"tronb:package": "electron-builder --config electron-builder.yml",
|
||||
@ -148,17 +145,10 @@
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@electron-forge/cli": "^7.4.0",
|
||||
"@electron-forge/maker-deb": "^7.4.0",
|
||||
"@electron-forge/maker-rpm": "^7.4.0",
|
||||
"@electron-forge/maker-squirrel": "^7.4.0",
|
||||
"@electron-forge/maker-wix": "^7.5.0",
|
||||
"@electron-forge/maker-zip": "^7.5.0",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "^7.4.0",
|
||||
"@electron-forge/plugin-fuses": "^7.4.0",
|
||||
"@electron-forge/plugin-vite": "^7.4.0",
|
||||
"@electron/fuses": "^1.8.0",
|
||||
"@electron/rebuild": "^3.6.0",
|
||||
"@electron-forge/cli": "7.4.0",
|
||||
"@electron-forge/plugin-fuses": "7.4.0",
|
||||
"@electron-forge/plugin-vite": "7.4.0",
|
||||
"@electron/fuses": "1.8.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@lezer/generator": "^1.7.1",
|
||||
"@nabla/vite-plugin-eslint": "^2.0.5",
|
||||
@ -188,9 +178,9 @@
|
||||
"@xstate/cli": "^0.5.17",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"d3-force": "^3.0.0",
|
||||
"electron": "^32.1.2",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-notarize": "^1.2.2",
|
||||
"electron": "32.1.2",
|
||||
"electron-builder": "24.13.3",
|
||||
"electron-notarize": "1.2.2",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-css-modules": "^2.12.0",
|
||||
|
@ -105,7 +105,7 @@ export class CameraControls {
|
||||
pendingZoom: number | null = null
|
||||
pendingRotation: Vector2 | null = null
|
||||
pendingPan: Vector2 | null = null
|
||||
interactionGuards: MouseGuard = cameraMouseDragGuards.KittyCAD
|
||||
interactionGuards: MouseGuard = cameraMouseDragGuards.Zoo
|
||||
isFovAnimationInProgress = false
|
||||
perspectiveFovBeforeOrtho = 45
|
||||
get isPerspective() {
|
||||
|
@ -1,13 +1,23 @@
|
||||
import toast from 'react-hot-toast'
|
||||
import { ActionIcon, ActionIconProps } from './ActionIcon'
|
||||
import { RefObject, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
MouseEvent,
|
||||
RefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { Dialog } from '@headlessui/react'
|
||||
|
||||
interface ContextMenuProps
|
||||
export interface ContextMenuProps
|
||||
extends Omit<React.HTMLAttributes<HTMLUListElement>, 'children'> {
|
||||
items?: React.ReactElement[]
|
||||
menuTargetElement?: RefObject<HTMLElement>
|
||||
guard?: (e: globalThis.MouseEvent) => boolean
|
||||
event?: 'contextmenu' | 'mouseup'
|
||||
}
|
||||
|
||||
const DefaultContextMenuItems = [
|
||||
@ -20,6 +30,8 @@ export function ContextMenu({
|
||||
items = DefaultContextMenuItems,
|
||||
menuTargetElement,
|
||||
className,
|
||||
guard,
|
||||
event = 'contextmenu',
|
||||
...props
|
||||
}: ContextMenuProps) {
|
||||
const dialogRef = useRef<HTMLDivElement>(null)
|
||||
@ -32,6 +44,15 @@ export function ContextMenu({
|
||||
useHotkeys('esc', () => setOpen(false), {
|
||||
enabled: open,
|
||||
})
|
||||
const handleContextMenu = useCallback(
|
||||
(e: globalThis.MouseEvent) => {
|
||||
if (guard && !guard(e)) return
|
||||
e.preventDefault()
|
||||
setPosition({ x: e.clientX, y: e.clientY })
|
||||
setOpen(true)
|
||||
},
|
||||
[guard, setPosition, setOpen]
|
||||
)
|
||||
|
||||
const dialogPositionStyle = useMemo(() => {
|
||||
if (!dialogRef.current)
|
||||
@ -78,21 +99,9 @@ export function ContextMenu({
|
||||
|
||||
// Add context menu listener to target once mounted
|
||||
useEffect(() => {
|
||||
const handleContextMenu = (e: MouseEvent) => {
|
||||
console.log('context menu', e)
|
||||
e.preventDefault()
|
||||
setPosition({ x: e.x, y: e.y })
|
||||
setOpen(true)
|
||||
}
|
||||
menuTargetElement?.current?.addEventListener(
|
||||
'contextmenu',
|
||||
handleContextMenu
|
||||
)
|
||||
menuTargetElement?.current?.addEventListener(event, handleContextMenu)
|
||||
return () => {
|
||||
menuTargetElement?.current?.removeEventListener(
|
||||
'contextmenu',
|
||||
handleContextMenu
|
||||
)
|
||||
menuTargetElement?.current?.removeEventListener(event, handleContextMenu)
|
||||
}
|
||||
}, [menuTargetElement?.current])
|
||||
|
||||
@ -100,7 +109,10 @@ export function ContextMenu({
|
||||
<Dialog open={open} onClose={() => setOpen(false)}>
|
||||
<div
|
||||
className="fixed inset-0 z-50 w-screen h-screen"
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault()
|
||||
setPosition({ x: e.clientX, y: e.clientY })
|
||||
}}
|
||||
>
|
||||
<Dialog.Backdrop className="fixed z-10 inset-0" />
|
||||
<Dialog.Panel
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { SceneInfra } from 'clientSideScene/sceneInfra'
|
||||
import { sceneInfra } from 'lib/singletons'
|
||||
import { MutableRefObject, useEffect, useMemo, useRef } from 'react'
|
||||
import { MutableRefObject, useEffect, useRef } from 'react'
|
||||
import {
|
||||
WebGLRenderer,
|
||||
Scene,
|
||||
@ -19,16 +19,14 @@ import {
|
||||
Intersection,
|
||||
Object3D,
|
||||
} from 'three'
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuDivider,
|
||||
ContextMenuItem,
|
||||
ContextMenuItemRefresh,
|
||||
} from './ContextMenu'
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import {
|
||||
useViewControlMenuItems,
|
||||
ViewControlContextMenu,
|
||||
} from './ViewControlMenu'
|
||||
import { AxisNames } from 'lib/constants'
|
||||
|
||||
const CANVAS_SIZE = 80
|
||||
const FRUSTUM_SIZE = 0.5
|
||||
@ -40,64 +38,14 @@ enum AxisColors {
|
||||
Z = '#6689ef',
|
||||
Gray = '#c6c7c2',
|
||||
}
|
||||
enum AxisNames {
|
||||
X = 'x',
|
||||
Y = 'y',
|
||||
Z = 'z',
|
||||
NEG_X = '-x',
|
||||
NEG_Y = '-y',
|
||||
NEG_Z = '-z',
|
||||
}
|
||||
const axisNamesSemantic: Record<AxisNames, string> = {
|
||||
[AxisNames.X]: 'Right',
|
||||
[AxisNames.Y]: 'Back',
|
||||
[AxisNames.Z]: 'Top',
|
||||
[AxisNames.NEG_X]: 'Left',
|
||||
[AxisNames.NEG_Y]: 'Front',
|
||||
[AxisNames.NEG_Z]: 'Bottom',
|
||||
}
|
||||
|
||||
export default function Gizmo() {
|
||||
const menuItems = useViewControlMenuItems()
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null)
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
||||
const raycasterIntersect = useRef<Intersection<Object3D> | null>(null)
|
||||
const cameraPassiveUpdateTimer = useRef(0)
|
||||
const raycasterPassiveUpdateTimer = useRef(0)
|
||||
const { send: modelingSend } = useModelingContext()
|
||||
const menuItems = useMemo(
|
||||
() => [
|
||||
...Object.entries(axisNamesSemantic).map(([axisName, axisSemantic]) => (
|
||||
<ContextMenuItem
|
||||
key={axisName}
|
||||
onClick={() => {
|
||||
sceneInfra.camControls
|
||||
.updateCameraToAxis(axisName as AxisNames)
|
||||
.catch(reportRejection)
|
||||
}}
|
||||
>
|
||||
{axisSemantic} view
|
||||
</ContextMenuItem>
|
||||
)),
|
||||
<ContextMenuDivider />,
|
||||
<ContextMenuItem
|
||||
onClick={() => {
|
||||
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
|
||||
}}
|
||||
>
|
||||
Reset view
|
||||
</ContextMenuItem>,
|
||||
<ContextMenuItem
|
||||
onClick={() => {
|
||||
modelingSend({ type: 'Center camera on selection' })
|
||||
}}
|
||||
>
|
||||
Center view on selection
|
||||
</ContextMenuItem>,
|
||||
<ContextMenuDivider />,
|
||||
<ContextMenuItemRefresh />,
|
||||
],
|
||||
[axisNamesSemantic]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!canvasRef.current) return
|
||||
@ -161,7 +109,7 @@ export default function Gizmo() {
|
||||
className="grid place-content-center rounded-full overflow-hidden border border-solid border-primary/50 pointer-events-auto bg-chalkboard-10/70 dark:bg-chalkboard-100/80 backdrop-blur-sm"
|
||||
>
|
||||
<canvas ref={canvasRef} />
|
||||
<ContextMenu menuTargetElement={wrapperRef} items={menuItems} />
|
||||
<ViewControlContextMenu menuTargetElement={wrapperRef} />
|
||||
</div>
|
||||
<GizmoDropdown items={menuItems} />
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { APP_VERSION, RELEASE_URL } from 'routes/Settings'
|
||||
import { APP_VERSION, getReleaseUrl } from 'routes/Settings'
|
||||
import { CustomIcon } from 'components/CustomIcon'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { PATHS } from 'lib/paths'
|
||||
@ -72,8 +72,8 @@ export function LowerRightControls({
|
||||
<menu className="flex items-center justify-end gap-3 pointer-events-auto">
|
||||
{!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />}
|
||||
<a
|
||||
onClick={openExternalBrowserIfDesktop(RELEASE_URL)}
|
||||
href={RELEASE_URL}
|
||||
onClick={openExternalBrowserIfDesktop(getReleaseUrl())}
|
||||
href={getReleaseUrl()}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={'!no-underline font-mono text-xs ' + linkOverrideClassName}
|
||||
|
@ -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={
|
||||
|
@ -13,7 +13,7 @@ import { isDesktop } from 'lib/isDesktop'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
import { SettingsFieldInput } from './SettingsFieldInput'
|
||||
import toast from 'react-hot-toast'
|
||||
import { APP_VERSION, IS_NIGHTLY, RELEASE_URL } from 'routes/Settings'
|
||||
import { APP_VERSION, IS_NIGHTLY, getReleaseUrl } from 'routes/Settings'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import {
|
||||
createAndOpenNewTutorialProject,
|
||||
@ -246,8 +246,8 @@ export const AllSettingsFields = forwardRef(
|
||||
to inject the version from package.json */}
|
||||
App version {APP_VERSION}.{' '}
|
||||
<a
|
||||
onClick={openExternalBrowserIfDesktop(RELEASE_URL)}
|
||||
href={RELEASE_URL}
|
||||
onClick={openExternalBrowserIfDesktop(getReleaseUrl())}
|
||||
href={getReleaseUrl()}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { trap } from 'lib/trap'
|
||||
import { useMachine } from '@xstate/react'
|
||||
import { useMachine, useSelector } from '@xstate/react'
|
||||
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
|
||||
import { PATHS, BROWSER_PATH } from 'lib/paths'
|
||||
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
|
||||
@ -54,11 +54,15 @@ type SettingsAuthContextType = {
|
||||
settings: MachineContext<typeof settingsMachine>
|
||||
}
|
||||
|
||||
// a little hacky for sure, open to changing it
|
||||
// this implies that we should only even have one instance of this provider mounted at any one time
|
||||
// but I think that's a safe assumption
|
||||
let settingsStateRef: ContextFrom<typeof settingsMachine> | undefined
|
||||
export const getSettingsState = () => settingsStateRef
|
||||
/**
|
||||
* This variable is used to store the last snapshot of the settings context
|
||||
* for use outside of React, such as in `wasm.ts`. It is updated every time
|
||||
* the settings machine changes with `useSelector`.
|
||||
* TODO: when we decouple XState from React, we can just subscribe to the actor directly from `wasm.ts`
|
||||
*/
|
||||
export let lastSettingsContextSnapshot:
|
||||
| ContextFrom<typeof settingsMachine>
|
||||
| undefined
|
||||
|
||||
export const SettingsAuthContext = createContext({} as SettingsAuthContextType)
|
||||
|
||||
@ -207,7 +211,10 @@ export const SettingsAuthProviderBase = ({
|
||||
}),
|
||||
{ input: loadedSettings }
|
||||
)
|
||||
settingsStateRef = settingsState.context
|
||||
// Any time the actor changes, update the settings state for external use
|
||||
useSelector(settingsActor, (s) => {
|
||||
lastSettingsContextSnapshot = s.context
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDesktop()) return
|
||||
|
@ -20,6 +20,7 @@ import { IndexLoaderData } from 'lib/types'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { err, reportRejection } from 'lib/trap'
|
||||
import { getArtifactOfTypes } from 'lang/std/artifactGraph'
|
||||
import { ViewControlContextMenu } from './ViewControlMenu'
|
||||
|
||||
enum StreamState {
|
||||
Playing = 'playing',
|
||||
@ -30,6 +31,7 @@ enum StreamState {
|
||||
|
||||
export const Stream = () => {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const videoWrapperRef = useRef<HTMLDivElement>(null)
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const { state, send } = useModelingContext()
|
||||
@ -258,7 +260,7 @@ export const Stream = () => {
|
||||
setIsLoading(false)
|
||||
}, [mediaStream])
|
||||
|
||||
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
const handleClick: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
// If we've got no stream or connection, don't do anything
|
||||
if (!isNetworkOkay) return
|
||||
if (!videoRef.current) return
|
||||
@ -320,10 +322,11 @@ export const Stream = () => {
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={videoWrapperRef}
|
||||
className="absolute inset-0 z-0"
|
||||
id="stream"
|
||||
data-testid="stream"
|
||||
onClick={handleMouseUp}
|
||||
onClick={handleClick}
|
||||
onDoubleClick={enterSketchModeIfSelectingSketch}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
onContextMenuCapture={(e) => e.preventDefault()}
|
||||
@ -384,6 +387,14 @@ export const Stream = () => {
|
||||
</Loading>
|
||||
</div>
|
||||
)}
|
||||
<ViewControlContextMenu
|
||||
event="mouseup"
|
||||
guard={(e) =>
|
||||
sceneInfra.camControls.wasDragging === false &&
|
||||
btnName(e).right === true
|
||||
}
|
||||
menuTargetElement={videoWrapperRef}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import toast from 'react-hot-toast'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||
import { Marked } from '@ts-stack/markdown'
|
||||
import { getReleaseUrl } from 'routes/Settings'
|
||||
|
||||
export function ToastUpdate({
|
||||
version,
|
||||
@ -32,10 +33,8 @@ export function ToastUpdate({
|
||||
A new update has downloaded and will be available next time you
|
||||
start the app. You can view the release notes{' '}
|
||||
<a
|
||||
onClick={openExternalBrowserIfDesktop(
|
||||
`https://github.com/KittyCAD/modeling-app/releases/tag/v${version}`
|
||||
)}
|
||||
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${version}`}
|
||||
onClick={openExternalBrowserIfDesktop(getReleaseUrl(version))}
|
||||
href={getReleaseUrl(version)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
|
@ -41,7 +41,10 @@ export function UnitsMenu() {
|
||||
close()
|
||||
}}
|
||||
>
|
||||
{baseUnitLabels[unit]}
|
||||
<span className="flex-1">{baseUnitLabels[unit]}</span>
|
||||
{unit === settings.context.modeling.defaultUnit.current && (
|
||||
<span className="text-chalkboard-60">current</span>
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
|
66
src/components/ViewControlMenu.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuDivider,
|
||||
ContextMenuItem,
|
||||
ContextMenuItemRefresh,
|
||||
ContextMenuProps,
|
||||
} from './ContextMenu'
|
||||
import { AxisNames, VIEW_NAMES_SEMANTIC } from 'lib/constants'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { useMemo } from 'react'
|
||||
import { sceneInfra } from 'lib/singletons'
|
||||
|
||||
export function useViewControlMenuItems() {
|
||||
const { send: modelingSend } = useModelingContext()
|
||||
const menuItems = useMemo(
|
||||
() => [
|
||||
...Object.entries(VIEW_NAMES_SEMANTIC).map(([axisName, axisSemantic]) => (
|
||||
<ContextMenuItem
|
||||
key={axisName}
|
||||
onClick={() => {
|
||||
sceneInfra.camControls
|
||||
.updateCameraToAxis(axisName as AxisNames)
|
||||
.catch(reportRejection)
|
||||
}}
|
||||
>
|
||||
{axisSemantic} view
|
||||
</ContextMenuItem>
|
||||
)),
|
||||
<ContextMenuDivider />,
|
||||
<ContextMenuItem
|
||||
onClick={() => {
|
||||
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
|
||||
}}
|
||||
>
|
||||
Reset view
|
||||
</ContextMenuItem>,
|
||||
<ContextMenuItem
|
||||
onClick={() => {
|
||||
modelingSend({ type: 'Center camera on selection' })
|
||||
}}
|
||||
>
|
||||
Center view on selection
|
||||
</ContextMenuItem>,
|
||||
<ContextMenuDivider />,
|
||||
<ContextMenuItemRefresh />,
|
||||
],
|
||||
[VIEW_NAMES_SEMANTIC]
|
||||
)
|
||||
return menuItems
|
||||
}
|
||||
|
||||
export function ViewControlContextMenu({
|
||||
menuTargetElement: wrapperRef,
|
||||
...props
|
||||
}: ContextMenuProps) {
|
||||
const menuItems = useViewControlMenuItems()
|
||||
return (
|
||||
<ContextMenu
|
||||
data-testid="view-controls-menu"
|
||||
menuTargetElement={wrapperRef}
|
||||
items={menuItems}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
327
src/editor/plugins/lsp/kcl/colors.ts
Normal file
@ -0,0 +1,327 @@
|
||||
import {
|
||||
EditorView,
|
||||
WidgetType,
|
||||
ViewUpdate,
|
||||
ViewPlugin,
|
||||
DecorationSet,
|
||||
Decoration,
|
||||
} from '@codemirror/view'
|
||||
import { Range, Extension, Text } from '@codemirror/state'
|
||||
import { NodeProp, Tree } from '@lezer/common'
|
||||
import { language, syntaxTree } from '@codemirror/language'
|
||||
|
||||
interface PickerState {
|
||||
from: number
|
||||
to: number
|
||||
alpha: string
|
||||
colorType: ColorType
|
||||
}
|
||||
|
||||
export interface WidgetOptions extends PickerState {
|
||||
color: string
|
||||
}
|
||||
|
||||
export type ColorData = Omit<WidgetOptions, 'from' | 'to'>
|
||||
|
||||
const pickerState = new WeakMap<HTMLInputElement, PickerState>()
|
||||
|
||||
export enum ColorType {
|
||||
hex = 'HEX',
|
||||
}
|
||||
|
||||
const hexRegex = /(^|\b)(#[0-9a-f]{3,9})(\b|$)/i
|
||||
|
||||
function discoverColorsInKCL(
|
||||
syntaxTree: Tree,
|
||||
from: number,
|
||||
to: number,
|
||||
typeName: string,
|
||||
doc: Text,
|
||||
language?: string
|
||||
): WidgetOptions | Array<WidgetOptions> | null {
|
||||
switch (typeName) {
|
||||
case 'Program':
|
||||
case 'VariableDeclaration':
|
||||
case 'CallExpression':
|
||||
case 'ObjectExpression':
|
||||
case 'ObjectProperty':
|
||||
case 'ArgumentList':
|
||||
case 'PipeExpression': {
|
||||
let innerTree = syntaxTree.resolveInner(from, 0).tree
|
||||
|
||||
if (!innerTree) {
|
||||
innerTree = syntaxTree.resolveInner(from, 1).tree
|
||||
if (!innerTree) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const overlayTree = innerTree.prop(NodeProp.mounted)?.tree
|
||||
|
||||
if (overlayTree?.type.name !== 'Styles') {
|
||||
return null
|
||||
}
|
||||
|
||||
const ret: Array<WidgetOptions> = []
|
||||
overlayTree.iterate({
|
||||
from: 0,
|
||||
to: overlayTree.length,
|
||||
enter: ({ type, from: overlayFrom, to: overlayTo }) => {
|
||||
const maybeWidgetOptions = discoverColorsInKCL(
|
||||
syntaxTree,
|
||||
// We add one because the tree doesn't include the
|
||||
// quotation mark from the style tag
|
||||
from + 1 + overlayFrom,
|
||||
from + 1 + overlayTo,
|
||||
type.name,
|
||||
doc,
|
||||
language
|
||||
)
|
||||
|
||||
if (maybeWidgetOptions) {
|
||||
if (Array.isArray(maybeWidgetOptions)) {
|
||||
console.error('Unexpected nested overlays')
|
||||
ret.push(...maybeWidgetOptions)
|
||||
} else {
|
||||
ret.push(maybeWidgetOptions)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
case 'String': {
|
||||
const result = parseColorLiteral(doc.sliceString(from, to))
|
||||
if (!result) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
from,
|
||||
to,
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function parseColorLiteral(colorLiteral: string): ColorData | null {
|
||||
const literal = colorLiteral.replace(/"/g, '')
|
||||
const match = hexRegex.exec(literal)
|
||||
if (!match) {
|
||||
return null
|
||||
}
|
||||
const [color, alpha] = toFullHex(literal)
|
||||
|
||||
return {
|
||||
colorType: ColorType.hex,
|
||||
color,
|
||||
alpha,
|
||||
}
|
||||
}
|
||||
|
||||
function colorPickersDecorations(
|
||||
view: EditorView,
|
||||
discoverColors: typeof discoverColorsInKCL
|
||||
) {
|
||||
const widgets: Array<Range<Decoration>> = []
|
||||
|
||||
const st = syntaxTree(view.state)
|
||||
|
||||
for (const range of view.visibleRanges) {
|
||||
st.iterate({
|
||||
from: range.from,
|
||||
to: range.to,
|
||||
enter: ({ type, from, to }) => {
|
||||
const maybeWidgetOptions = discoverColors(
|
||||
st,
|
||||
from,
|
||||
to,
|
||||
type.name,
|
||||
view.state.doc,
|
||||
view.state.facet(language)?.name
|
||||
)
|
||||
|
||||
if (!maybeWidgetOptions) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!Array.isArray(maybeWidgetOptions)) {
|
||||
widgets.push(
|
||||
Decoration.widget({
|
||||
widget: new ColorPickerWidget(maybeWidgetOptions),
|
||||
side: 1,
|
||||
}).range(maybeWidgetOptions.from)
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for (const wo of maybeWidgetOptions) {
|
||||
widgets.push(
|
||||
Decoration.widget({
|
||||
widget: new ColorPickerWidget(wo),
|
||||
side: 1,
|
||||
}).range(wo.from)
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return Decoration.set(widgets)
|
||||
}
|
||||
|
||||
function toFullHex(color: string): string[] {
|
||||
if (color.length === 4) {
|
||||
// 3-char hex
|
||||
return [
|
||||
`#${color[1].repeat(2)}${color[2].repeat(2)}${color[3].repeat(2)}`,
|
||||
'',
|
||||
]
|
||||
}
|
||||
|
||||
if (color.length === 5) {
|
||||
// 4-char hex (alpha)
|
||||
return [
|
||||
`#${color[1].repeat(2)}${color[2].repeat(2)}${color[3].repeat(2)}`,
|
||||
color[4].repeat(2),
|
||||
]
|
||||
}
|
||||
|
||||
if (color.length === 9) {
|
||||
// 8-char hex (alpha)
|
||||
return [`#${color.slice(1, -2)}`, color.slice(-2)]
|
||||
}
|
||||
|
||||
return [color, '']
|
||||
}
|
||||
|
||||
export const wrapperClassName = 'cm-css-color-picker-wrapper'
|
||||
|
||||
class ColorPickerWidget extends WidgetType {
|
||||
private readonly state: PickerState
|
||||
private readonly color: string
|
||||
|
||||
constructor({ color, ...state }: WidgetOptions) {
|
||||
super()
|
||||
this.state = state
|
||||
this.color = color
|
||||
}
|
||||
|
||||
eq(other: ColorPickerWidget) {
|
||||
return (
|
||||
other.state.colorType === this.state.colorType &&
|
||||
other.color === this.color &&
|
||||
other.state.from === this.state.from &&
|
||||
other.state.to === this.state.to &&
|
||||
other.state.alpha === this.state.alpha
|
||||
)
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
const picker = document.createElement('input')
|
||||
pickerState.set(picker, this.state)
|
||||
picker.type = 'color'
|
||||
picker.value = this.color
|
||||
|
||||
const wrapper = document.createElement('span')
|
||||
wrapper.appendChild(picker)
|
||||
wrapper.className = wrapperClassName
|
||||
|
||||
return wrapper
|
||||
}
|
||||
|
||||
ignoreEvent() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const colorPickerTheme = EditorView.baseTheme({
|
||||
[`.${wrapperClassName}`]: {
|
||||
display: 'inline-block',
|
||||
outline: '1px solid #eee',
|
||||
marginRight: '0.6ch',
|
||||
height: '1em',
|
||||
width: '1em',
|
||||
transform: 'translateY(1px)',
|
||||
},
|
||||
[`.${wrapperClassName} input[type="color"]`]: {
|
||||
cursor: 'pointer',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
padding: 0,
|
||||
border: 'none',
|
||||
'&::-webkit-color-swatch-wrapper': {
|
||||
padding: 0,
|
||||
},
|
||||
'&::-webkit-color-swatch': {
|
||||
border: 'none',
|
||||
},
|
||||
'&::-moz-color-swatch': {
|
||||
border: 'none',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
interface IFactoryOptions {
|
||||
discoverColors: typeof discoverColorsInKCL
|
||||
}
|
||||
|
||||
export const makeColorPicker = (options: IFactoryOptions) =>
|
||||
ViewPlugin.fromClass(
|
||||
class ColorPickerViewPlugin {
|
||||
decorations: DecorationSet
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = colorPickersDecorations(view, options.discoverColors)
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged || update.viewportChanged) {
|
||||
this.decorations = colorPickersDecorations(
|
||||
update.view,
|
||||
options.discoverColors
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (v) => v.decorations,
|
||||
eventHandlers: {
|
||||
change: (e, view) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
if (
|
||||
target.nodeName !== 'INPUT' ||
|
||||
!target.parentElement ||
|
||||
!target.parentElement.classList.contains(wrapperClassName)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
const data = pickerState.get(target)!
|
||||
|
||||
let converted = '"' + target.value + data.alpha + '"'
|
||||
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: data.from,
|
||||
to: data.to,
|
||||
insert: converted,
|
||||
},
|
||||
})
|
||||
|
||||
return true
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export const colorPicker: Extension = [
|
||||
makeColorPicker({ discoverColors: discoverColorsInKCL }),
|
||||
colorPickerTheme,
|
||||
]
|
@ -17,6 +17,7 @@ import { kclPlugin } from '.'
|
||||
import type * as LSP from 'vscode-languageserver-protocol'
|
||||
// @ts-ignore: No types available
|
||||
import { parser } from './kcl.grammar'
|
||||
import { colorPicker } from './colors'
|
||||
|
||||
export interface LanguageOptions {
|
||||
workspaceFolders: LSP.WorkspaceFolder[]
|
||||
@ -54,14 +55,14 @@ export const KclLanguage = LRLanguage.define({
|
||||
})
|
||||
|
||||
export function kcl(options: LanguageOptions) {
|
||||
return new LanguageSupport(
|
||||
KclLanguage,
|
||||
return new LanguageSupport(KclLanguage, [
|
||||
colorPicker,
|
||||
kclPlugin({
|
||||
documentUri: options.documentUri,
|
||||
workspaceFolders: options.workspaceFolders,
|
||||
allowHTMLContent: true,
|
||||
client: options.client,
|
||||
processLspNotification: options.processLspNotification,
|
||||
})
|
||||
)
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
@ -317,3 +317,8 @@ code {
|
||||
#code-mirror-override .cm-editor {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
/* Can't use #code-mirror-override here as we're outside of this div */
|
||||
.body-bg .cm-diagnosticAction {
|
||||
@apply bg-primary;
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
@ -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.`
|
||||
|
@ -42,7 +42,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'
|
||||
import { getAllCurrentSettings } from 'lib/settings/settingsUtils'
|
||||
|
||||
export type { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||
export type { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||
@ -93,12 +93,26 @@ export type { Solid } from '../wasm-lib/kcl/bindings/Solid'
|
||||
export type { KclValue } from '../wasm-lib/kcl/bindings/KclValue'
|
||||
export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'
|
||||
|
||||
/**
|
||||
* The first two items are the start and end points (byte offsets from the start of the file).
|
||||
* The third item is whether the source range belongs to the 'main' file, i.e., the file currently
|
||||
* being rendered/displayed in the editor (TODO we need to handle modules better in the frontend).
|
||||
*/
|
||||
export type SourceRange = [number, number, boolean]
|
||||
|
||||
/**
|
||||
* Convert a SourceRange as used inside the KCL interpreter into the above one for use in the
|
||||
* frontend (essentially we're eagerly checking whether the frontend should care about the SourceRange
|
||||
* so as not to expose details of the interpreter's current representation of module ids throughout
|
||||
* the frontend).
|
||||
*/
|
||||
export function sourceRangeFromRust(s: RustSourceRange): SourceRange {
|
||||
return [s[0], s[1], s[2] === 0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a default SourceRange for testing or as a placeholder.
|
||||
*/
|
||||
export function defaultSourceRange(): SourceRange {
|
||||
return [0, 0, true]
|
||||
}
|
||||
@ -123,7 +137,7 @@ const initialise = async () => {
|
||||
const fullUrl = wasmUrl()
|
||||
const input = await fetch(fullUrl)
|
||||
const buffer = await input.arrayBuffer()
|
||||
return await init(buffer)
|
||||
return await init({ module_or_path: buffer })
|
||||
} catch (e) {
|
||||
console.log('Error initialising WASM', e)
|
||||
return Promise.reject(e)
|
||||
@ -164,6 +178,10 @@ export class ParseResult {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsing was successful. There is guaranteed to be an AST and no fatal errors. There may or may
|
||||
* not be warnings or non-fatal errors.
|
||||
*/
|
||||
class SuccessParseResult extends ParseResult {
|
||||
program: Node<Program>
|
||||
|
||||
@ -496,12 +514,11 @@ export const _executor = async (
|
||||
try {
|
||||
let jsAppSettings = default_app_settings()
|
||||
if (!TEST) {
|
||||
const getSettingsState = import('components/SettingsAuthProvider').then(
|
||||
(module) => module.getSettingsState
|
||||
)
|
||||
const settings = (await getSettingsState)()
|
||||
if (settings) {
|
||||
jsAppSettings = getChangedSettingsAtLevel(settings, 'user')
|
||||
const lastSettingsSnapshot = await import(
|
||||
'components/SettingsAuthProvider'
|
||||
).then((module) => module.lastSettingsContextSnapshot)
|
||||
if (lastSettingsSnapshot) {
|
||||
jsAppSettings = getAllCurrentSettings(lastSettingsSnapshot)
|
||||
}
|
||||
}
|
||||
const execState: RawExecState = await execute(
|
||||
|
@ -10,7 +10,7 @@ const noModifiersPressed = (e: MouseEvent) =>
|
||||
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
|
||||
|
||||
export type CameraSystem =
|
||||
| 'KittyCAD'
|
||||
| 'Zoo'
|
||||
| 'OnShape'
|
||||
| 'Trackpad Friendly'
|
||||
| 'Solidworks'
|
||||
@ -19,7 +19,7 @@ export type CameraSystem =
|
||||
| 'AutoCAD'
|
||||
|
||||
export const cameraSystems: CameraSystem[] = [
|
||||
'KittyCAD',
|
||||
'Zoo',
|
||||
'OnShape',
|
||||
'Trackpad Friendly',
|
||||
'Solidworks',
|
||||
@ -34,9 +34,8 @@ export function mouseControlsToCameraSystem(
|
||||
switch (mouseControl) {
|
||||
// TODO: understand why the values come back without underscores and fix the root cause
|
||||
// @ts-ignore: TS2678
|
||||
case 'kittycad':
|
||||
case 'kitty_cad':
|
||||
return 'KittyCAD'
|
||||
case 'zoo':
|
||||
return 'Zoo'
|
||||
// TODO: understand why the values come back without underscores and fix the root cause
|
||||
// @ts-ignore: TS2678
|
||||
case 'onshape':
|
||||
@ -86,7 +85,7 @@ export const btnName = (e: MouseEvent) => ({
|
||||
})
|
||||
|
||||
export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
|
||||
KittyCAD: {
|
||||
Zoo: {
|
||||
pan: {
|
||||
description: 'Shift + Right click drag or middle click drag',
|
||||
callback: (e) =>
|
||||
|
@ -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,
|
||||
|
106
src/lib/commandBarConfigs/validators.ts
Normal file
@ -0,0 +1,106 @@
|
||||
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'
|
||||
|
||||
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 (!('pathId' in 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,28 @@ 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'
|
||||
|
||||
export enum AxisNames {
|
||||
X = 'x',
|
||||
Y = 'y',
|
||||
Z = 'z',
|
||||
NEG_X = '-x',
|
||||
NEG_Y = '-y',
|
||||
NEG_Z = '-z',
|
||||
}
|
||||
/** Semantic names of views from AxisNames */
|
||||
export const VIEW_NAMES_SEMANTIC = {
|
||||
[AxisNames.X]: 'Right',
|
||||
[AxisNames.Y]: 'Back',
|
||||
[AxisNames.Z]: 'Top',
|
||||
[AxisNames.NEG_X]: 'Left',
|
||||
[AxisNames.NEG_Y]: 'Front',
|
||||
[AxisNames.NEG_Z]: 'Bottom',
|
||||
} as const
|
||||
|
@ -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)
|
||||
|
@ -283,7 +283,7 @@ export function createSettings() {
|
||||
* The controls for how to navigate the 3D view
|
||||
*/
|
||||
mouseControls: new Setting<CameraSystem>({
|
||||
defaultValue: 'KittyCAD',
|
||||
defaultValue: 'Zoo',
|
||||
description: 'The controls for how to navigate the 3D view',
|
||||
validate: (v) => cameraSystems.includes(v as CameraSystem),
|
||||
hideOnLevel: 'project',
|
||||
|
@ -2,6 +2,7 @@ import { DeepPartial } from 'lib/types'
|
||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||
import {
|
||||
configurationToSettingsPayload,
|
||||
getAllCurrentSettings,
|
||||
projectConfigurationToSettingsPayload,
|
||||
setSettingsAtLevel,
|
||||
} from './settingsUtils'
|
||||
@ -65,3 +66,48 @@ describe(`testing settings initialization`, () => {
|
||||
expect(settings.app.themeColor.current).toBe('200')
|
||||
})
|
||||
})
|
||||
|
||||
describe(`testing getAllCurrentSettings`, () => {
|
||||
it(`returns the correct settings`, () => {
|
||||
// Set up the settings
|
||||
let settings = createSettings()
|
||||
const appConfiguration: DeepPartial<Configuration> = {
|
||||
settings: {
|
||||
app: {
|
||||
appearance: {
|
||||
theme: 'dark',
|
||||
color: 190,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
const projectConfiguration: DeepPartial<Configuration> = {
|
||||
settings: {
|
||||
app: {
|
||||
appearance: {
|
||||
theme: 'light',
|
||||
color: 200,
|
||||
},
|
||||
},
|
||||
modeling: {
|
||||
base_unit: 'ft',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const appSettingsPayload = configurationToSettingsPayload(appConfiguration)
|
||||
const projectSettingsPayload =
|
||||
projectConfigurationToSettingsPayload(projectConfiguration)
|
||||
|
||||
setSettingsAtLevel(settings, 'user', appSettingsPayload)
|
||||
setSettingsAtLevel(settings, 'project', projectSettingsPayload)
|
||||
|
||||
// Now the test: get all the settings' current resolved values
|
||||
const allCurrentSettings = getAllCurrentSettings(settings)
|
||||
// This one gets the 'user'-level theme because it's ignored at the project level
|
||||
// (see the test "doesn't read theme from project settings")
|
||||
expect(allCurrentSettings.app.theme).toBe('dark')
|
||||
expect(allCurrentSettings.app.themeColor).toBe('200')
|
||||
expect(allCurrentSettings.modeling.defaultUnit).toBe('ft')
|
||||
})
|
||||
})
|
||||
|
@ -286,6 +286,27 @@ export function getChangedSettingsAtLevel(
|
||||
return changedSettings
|
||||
}
|
||||
|
||||
export function getAllCurrentSettings(
|
||||
allSettings: typeof settings
|
||||
): SaveSettingsPayload {
|
||||
const currentSettings = {} as SaveSettingsPayload
|
||||
Object.entries(allSettings).forEach(([category, settingsCategory]) => {
|
||||
const categoryKey = category as keyof typeof settings
|
||||
Object.entries(settingsCategory).forEach(
|
||||
([setting, settingValue]: [string, Setting]) => {
|
||||
const settingKey =
|
||||
setting as keyof (typeof settings)[typeof categoryKey]
|
||||
currentSettings[categoryKey] = {
|
||||
...currentSettings[categoryKey],
|
||||
[settingKey]: settingValue.current,
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return currentSettings
|
||||
}
|
||||
|
||||
export function setSettingsAtLevel(
|
||||
allSettings: typeof settings,
|
||||
level: SettingsLevel,
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -44,11 +44,6 @@ process.env.VITE_KC_SITE_BASE_URL ??= 'https://zoo.dev'
|
||||
process.env.VITE_KC_SKIP_AUTH ??= 'false'
|
||||
process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??= '15000'
|
||||
|
||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||
if (require('electron-squirrel-startup')) {
|
||||
app.quit()
|
||||
}
|
||||
|
||||
const ZOO_STUDIO_PROTOCOL = 'zoo-studio'
|
||||
|
||||
/// Register our application to handle all "electron-fiddle://" protocols.
|
||||
@ -256,6 +251,9 @@ export function getAutoUpdater(): AppUpdater {
|
||||
|
||||
app.on('ready', () => {
|
||||
const autoUpdater = getAutoUpdater()
|
||||
// TODO: we're getting `Error: Response ends without calling any handlers` with our setup,
|
||||
// so at the moment this isn't worth enabling
|
||||
autoUpdater.disableDifferentialDownload = true
|
||||
setTimeout(() => {
|
||||
autoUpdater.checkForUpdates().catch(reportRejection)
|
||||
}, 1000)
|
||||
|
@ -32,9 +32,11 @@ export const PACKAGE_NAME = isDesktop()
|
||||
|
||||
export const IS_NIGHTLY = PACKAGE_NAME.indexOf('-nightly') > -1
|
||||
|
||||
export const RELEASE_URL = `https://github.com/KittyCAD/modeling-app/releases/tag/${
|
||||
IS_NIGHTLY ? 'nightly-' : ''
|
||||
}v${APP_VERSION}`
|
||||
export function getReleaseUrl(version: string = APP_VERSION) {
|
||||
return `https://github.com/KittyCAD/modeling-app/releases/tag/${
|
||||
IS_NIGHTLY ? 'nightly-' : ''
|
||||
}v${version}`
|
||||
}
|
||||
|
||||
export const Settings = () => {
|
||||
const navigate = useNavigate()
|
||||
|
8
src/wasm-lib/Cargo.lock
generated
@ -1721,7 +1721,9 @@ dependencies = [
|
||||
"parse-display 0.9.1",
|
||||
"pretty_assertions",
|
||||
"pyo3",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"rgba_simple",
|
||||
"ropey",
|
||||
"schemars",
|
||||
"serde",
|
||||
@ -2971,6 +2973,12 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rgba_simple"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6cd655523701785087f69900df39892fb7b9b0721aa67682f571c38c32ac58a"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.8"
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -40,10 +40,12 @@ miette = "7.2.0"
|
||||
mime_guess = "2.0.5"
|
||||
parse-display = "0.9.1"
|
||||
pyo3 = { version = "0.22.6", optional = true }
|
||||
regex = "1.11.1"
|
||||
reqwest = { version = "0.12", default-features = false, features = [
|
||||
"stream",
|
||||
"rustls-tls",
|
||||
] }
|
||||
rgba_simple = "0.10.0"
|
||||
ropey = "1.6.1"
|
||||
schemars = { version = "0.8.17", features = [
|
||||
"impl_json_schema",
|
||||
|
@ -13,6 +13,8 @@ use tower_lsp::lsp_types::{
|
||||
MarkupKind, ParameterInformation, ParameterLabel, SignatureHelp, SignatureInformation,
|
||||
};
|
||||
|
||||
use crate::execution::Sketch;
|
||||
|
||||
use crate::std::Primitive;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
|
||||
@ -232,6 +234,11 @@ pub trait StdLibFn: std::fmt::Debug + Send + Sync {
|
||||
}
|
||||
|
||||
fn to_autocomplete_snippet(&self) -> Result<String> {
|
||||
if self.name() == "loft" {
|
||||
return Ok("loft([${0:sketch000}, ${1:sketch001}])${}".to_string());
|
||||
} else if self.name() == "hole" {
|
||||
return Ok("hole(${0:holeSketch}, ${1:%})${}".to_string());
|
||||
}
|
||||
let mut args = Vec::new();
|
||||
let mut index = 0;
|
||||
for arg in self.args(true).iter() {
|
||||
@ -451,6 +458,16 @@ fn get_autocomplete_snippet_from_schema(
|
||||
) -> Result<Option<(usize, String)>> {
|
||||
match schema {
|
||||
schemars::schema::Schema::Object(o) => {
|
||||
// Check if the schema is the same as a Sketch.
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
// We set this so we can recurse them later.
|
||||
settings.inline_subschemas = true;
|
||||
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||
let sketch_schema = generator.root_schema_for::<Sketch>().schema;
|
||||
if sketch_schema.object == o.object {
|
||||
return Ok(Some((index, format!("${{{}:sketch{}}}", index, "000"))));
|
||||
}
|
||||
|
||||
if let Some(serde_json::Value::Bool(nullable)) = o.extensions.get("nullable") {
|
||||
if *nullable {
|
||||
return Ok(None);
|
||||
@ -489,6 +506,12 @@ fn get_autocomplete_snippet_from_schema(
|
||||
continue;
|
||||
}
|
||||
|
||||
if prop_name == "color" {
|
||||
fn_docs.push_str(&format!("\t{}: ${{{}:\"#ff0000\"}},\n", prop_name, i));
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some((new_index, snippet)) = get_autocomplete_snippet_from_schema(prop, i)? {
|
||||
fn_docs.push_str(&format!("\t{}: {},\n", prop_name, snippet));
|
||||
i = new_index + 1;
|
||||
@ -946,6 +969,47 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_autocomplete_snippet_appearance() {
|
||||
let appearance_fn: Box<dyn StdLibFn> = Box::new(crate::std::appearance::Appearance);
|
||||
let snippet = appearance_fn.to_autocomplete_snippet().unwrap();
|
||||
assert_eq!(
|
||||
snippet,
|
||||
r#"appearance({
|
||||
color: ${0:"#
|
||||
.to_owned()
|
||||
+ "\"#"
|
||||
+ r#"ff0000"},
|
||||
}, ${1:%})${}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_autocomplete_snippet_loft() {
|
||||
let loft_fn: Box<dyn StdLibFn> = Box::new(crate::std::loft::Loft);
|
||||
let snippet = loft_fn.to_autocomplete_snippet().unwrap();
|
||||
assert_eq!(snippet, r#"loft([${0:sketch000}, ${1:sketch001}])${}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_autocomplete_snippet_sweep() {
|
||||
let sweep_fn: Box<dyn StdLibFn> = Box::new(crate::std::sweep::Sweep);
|
||||
let snippet = sweep_fn.to_autocomplete_snippet().unwrap();
|
||||
assert_eq!(
|
||||
snippet,
|
||||
r#"sweep({
|
||||
path: ${0:sketch000},
|
||||
}, ${1:%})${}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_autocomplete_snippet_hole() {
|
||||
let hole_fn: Box<dyn StdLibFn> = Box::new(crate::std::sketch::Hole);
|
||||
let snippet = hole_fn.to_autocomplete_snippet().unwrap();
|
||||
assert_eq!(snippet, r#"hole(${0:holeSketch}, ${1:%})${}"#);
|
||||
}
|
||||
|
||||
// We want to test the snippets we compile at lsp start.
|
||||
#[test]
|
||||
fn get_all_stdlib_autocomplete_snippets() {
|
||||
|
@ -326,29 +326,12 @@ async fn inner_execute_pipe_body(
|
||||
ctx: &ExecutorContext,
|
||||
) -> Result<KclValue, KclError> {
|
||||
for expression in body {
|
||||
match expression {
|
||||
Expr::TagDeclarator(_) => {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("This cannot be in a PipeExpression: {:?}", expression),
|
||||
source_ranges: vec![expression.into()],
|
||||
}));
|
||||
}
|
||||
Expr::Literal(_)
|
||||
| Expr::Identifier(_)
|
||||
| Expr::BinaryExpression(_)
|
||||
| Expr::FunctionExpression(_)
|
||||
| Expr::CallExpression(_)
|
||||
| Expr::CallExpressionKw(_)
|
||||
| Expr::PipeExpression(_)
|
||||
| Expr::PipeSubstitution(_)
|
||||
| Expr::ArrayExpression(_)
|
||||
| Expr::ArrayRangeExpression(_)
|
||||
| Expr::ObjectExpression(_)
|
||||
| Expr::MemberExpression(_)
|
||||
| Expr::UnaryExpression(_)
|
||||
| Expr::IfExpression(_)
|
||||
| Expr::None(_) => {}
|
||||
};
|
||||
if let Expr::TagDeclarator(_) = expression {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("This cannot be in a PipeExpression: {:?}", expression),
|
||||
source_ranges: vec![expression.into()],
|
||||
}));
|
||||
}
|
||||
let metadata = Metadata {
|
||||
source_range: SourceRange::from(expression),
|
||||
};
|
||||
@ -366,6 +349,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 +396,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2117,7 +2117,8 @@ impl ExecutorContext {
|
||||
Ok((module_memory, module_exports))
|
||||
}
|
||||
|
||||
pub async fn execute_expr<'a>(
|
||||
#[async_recursion]
|
||||
pub async fn execute_expr<'a: 'async_recursion>(
|
||||
&self,
|
||||
init: &Expr,
|
||||
exec_state: &mut ExecState,
|
||||
@ -2174,6 +2175,14 @@ impl ExecutorContext {
|
||||
Expr::MemberExpression(member_expression) => member_expression.get_result(exec_state)?,
|
||||
Expr::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, self).await?,
|
||||
Expr::IfExpression(expr) => expr.get_result(exec_state, self).await?,
|
||||
Expr::LabelledExpression(expr) => {
|
||||
let result = self
|
||||
.execute_expr(&expr.expr, exec_state, metadata, statement_kind)
|
||||
.await?;
|
||||
exec_state.memory.add(&expr.label.name, result.clone(), init.into())?;
|
||||
// TODO this lets us use the label as a variable name, but not as a tag in most cases
|
||||
result
|
||||
}
|
||||
};
|
||||
Ok(item)
|
||||
}
|
||||
@ -2285,6 +2294,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 +2377,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,
|
||||
|
@ -1,10 +1,10 @@
|
||||
use sha2::{Digest as DigestTrait, Sha256};
|
||||
|
||||
use super::types::{DefaultParamVal, ItemVisibility, VariableKind};
|
||||
use super::types::{DefaultParamVal, ItemVisibility, LabelledExpression, VariableKind};
|
||||
use crate::parsing::ast::types::{
|
||||
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, CallExpressionKw,
|
||||
CommentStyle, ElseIf, Expr, ExpressionStatement, FnArgType, FunctionExpression, Identifier, IfExpression,
|
||||
ImportItem, ImportSelector, ImportStatement, Literal, LiteralIdentifier, MemberExpression, MemberObject,
|
||||
ImportItem, ImportSelector, ImportStatement, KclNone, Literal, LiteralIdentifier, MemberExpression, MemberObject,
|
||||
NonCodeMeta, NonCodeNode, NonCodeValue, ObjectExpression, ObjectProperty, Parameter, PipeExpression,
|
||||
PipeSubstitution, Program, ReturnStatement, TagDeclarator, UnaryExpression, VariableDeclaration,
|
||||
VariableDeclarator,
|
||||
@ -115,6 +115,7 @@ impl Expr {
|
||||
Expr::MemberExpression(me) => me.compute_digest(),
|
||||
Expr::UnaryExpression(ue) => ue.compute_digest(),
|
||||
Expr::IfExpression(e) => e.compute_digest(),
|
||||
Expr::LabelledExpression(e) => e.compute_digest(),
|
||||
Expr::None(_) => {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(b"Value::None");
|
||||
@ -202,6 +203,12 @@ impl Parameter {
|
||||
});
|
||||
}
|
||||
|
||||
impl KclNone {
|
||||
compute_digest!(|slf, hasher| {
|
||||
hasher.update(b"KclNone");
|
||||
});
|
||||
}
|
||||
|
||||
impl FunctionExpression {
|
||||
compute_digest!(|slf, hasher| {
|
||||
hasher.update(slf.params.len().to_ne_bytes());
|
||||
@ -396,6 +403,13 @@ impl UnaryExpression {
|
||||
});
|
||||
}
|
||||
|
||||
impl LabelledExpression {
|
||||
compute_digest!(|slf, hasher| {
|
||||
hasher.update(slf.expr.compute_digest());
|
||||
hasher.update(slf.label.compute_digest());
|
||||
});
|
||||
}
|
||||
|
||||
impl PipeExpression {
|
||||
compute_digest!(|slf, hasher| {
|
||||
hasher.update(slf.body.len().to_ne_bytes());
|
||||
|
@ -36,6 +36,7 @@ impl Expr {
|
||||
Expr::MemberExpression(member_expression) => member_expression.module_id,
|
||||
Expr::UnaryExpression(unary_expression) => unary_expression.module_id,
|
||||
Expr::IfExpression(expr) => expr.module_id,
|
||||
Expr::LabelledExpression(expr) => expr.expr.module_id(),
|
||||
Expr::None(none) => none.module_id,
|
||||
}
|
||||
}
|
||||
|
@ -598,6 +598,7 @@ pub enum Expr {
|
||||
MemberExpression(BoxNode<MemberExpression>),
|
||||
UnaryExpression(BoxNode<UnaryExpression>),
|
||||
IfExpression(BoxNode<IfExpression>),
|
||||
LabelledExpression(BoxNode<LabelledExpression>),
|
||||
None(Node<KclNone>),
|
||||
}
|
||||
|
||||
@ -640,6 +641,7 @@ impl Expr {
|
||||
Expr::UnaryExpression(_unary_exp) => None,
|
||||
Expr::PipeSubstitution(_pipe_substitution) => None,
|
||||
Expr::IfExpression(_) => None,
|
||||
Expr::LabelledExpression(expr) => expr.expr.get_non_code_meta(),
|
||||
Expr::None(_none) => None,
|
||||
}
|
||||
}
|
||||
@ -666,6 +668,7 @@ impl Expr {
|
||||
Expr::UnaryExpression(ref mut unary_exp) => unary_exp.replace_value(source_range, new_value),
|
||||
Expr::IfExpression(_) => {}
|
||||
Expr::PipeSubstitution(_) => {}
|
||||
Expr::LabelledExpression(expr) => expr.expr.replace_value(source_range, new_value),
|
||||
Expr::None(_) => {}
|
||||
}
|
||||
}
|
||||
@ -687,6 +690,7 @@ impl Expr {
|
||||
Expr::MemberExpression(member_expression) => member_expression.start,
|
||||
Expr::UnaryExpression(unary_expression) => unary_expression.start,
|
||||
Expr::IfExpression(expr) => expr.start,
|
||||
Expr::LabelledExpression(expr) => expr.start,
|
||||
Expr::None(none) => none.start,
|
||||
}
|
||||
}
|
||||
@ -708,6 +712,7 @@ impl Expr {
|
||||
Expr::MemberExpression(member_expression) => member_expression.end,
|
||||
Expr::UnaryExpression(unary_expression) => unary_expression.end,
|
||||
Expr::IfExpression(expr) => expr.end,
|
||||
Expr::LabelledExpression(expr) => expr.end,
|
||||
Expr::None(none) => none.end,
|
||||
}
|
||||
}
|
||||
@ -734,6 +739,8 @@ impl Expr {
|
||||
Expr::Literal(_) => None,
|
||||
Expr::Identifier(_) => None,
|
||||
Expr::TagDeclarator(_) => None,
|
||||
// TODO LSP hover info for tag
|
||||
Expr::LabelledExpression(expr) => expr.expr.get_hover_value_for_position(pos, code),
|
||||
// TODO: LSP hover information for symbols. https://github.com/KittyCAD/modeling-app/issues/1127
|
||||
Expr::PipeSubstitution(_) => None,
|
||||
}
|
||||
@ -763,6 +770,7 @@ impl Expr {
|
||||
}
|
||||
Expr::UnaryExpression(ref mut unary_expression) => unary_expression.rename_identifiers(old_name, new_name),
|
||||
Expr::IfExpression(ref mut expr) => expr.rename_identifiers(old_name, new_name),
|
||||
Expr::LabelledExpression(expr) => expr.expr.rename_identifiers(old_name, new_name),
|
||||
Expr::None(_) => {}
|
||||
}
|
||||
}
|
||||
@ -788,9 +796,19 @@ impl Expr {
|
||||
Expr::MemberExpression(member_expression) => member_expression.get_constraint_level(),
|
||||
Expr::UnaryExpression(unary_expression) => unary_expression.get_constraint_level(),
|
||||
Expr::IfExpression(expr) => expr.get_constraint_level(),
|
||||
Expr::LabelledExpression(expr) => expr.expr.get_constraint_level(),
|
||||
Expr::None(none) => none.get_constraint_level(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_substitution_arg(&self) -> bool {
|
||||
match self {
|
||||
Expr::CallExpression(call_expression) => call_expression.has_substitution_arg(),
|
||||
Expr::CallExpressionKw(call_expression) => call_expression.has_substitution_arg(),
|
||||
Expr::LabelledExpression(expr) => expr.expr.has_substitution_arg(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Expr> for SourceRange {
|
||||
@ -805,6 +823,36 @@ impl From<&Expr> for SourceRange {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
pub struct LabelledExpression {
|
||||
pub expr: Expr,
|
||||
pub label: Node<Identifier>,
|
||||
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub digest: Option<Digest>,
|
||||
}
|
||||
|
||||
impl LabelledExpression {
|
||||
pub(crate) fn new(expr: Expr, label: Node<Identifier>) -> Node<LabelledExpression> {
|
||||
let start = expr.start();
|
||||
let end = label.end;
|
||||
let module_id = expr.module_id();
|
||||
Node::new(
|
||||
LabelledExpression {
|
||||
expr,
|
||||
label,
|
||||
digest: None,
|
||||
},
|
||||
start,
|
||||
end,
|
||||
module_id,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
@ -2810,7 +2858,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?
|
||||
|
@ -3,7 +3,7 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::Node;
|
||||
use super::{super::digest::Digest, Node};
|
||||
use crate::{execution::KclValue, parsing::ast::types::ConstraintLevel};
|
||||
|
||||
const KCL_NONE_ID: &str = "KCL_NONE_ID";
|
||||
@ -19,11 +19,18 @@ pub struct KclNone {
|
||||
#[ts(skip)]
|
||||
#[schemars(skip)]
|
||||
__private: Private,
|
||||
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub digest: Option<Digest>,
|
||||
}
|
||||
|
||||
impl KclNone {
|
||||
pub fn new() -> Self {
|
||||
Self { __private: Private {} }
|
||||
Self {
|
||||
__private: Private {},
|
||||
digest: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,8 @@ use crate::{
|
||||
SourceRange,
|
||||
};
|
||||
|
||||
use super::ast::types::LabelledExpression;
|
||||
|
||||
thread_local! {
|
||||
/// The current `ParseContext`. `None` if parsing is not currently happening on this thread.
|
||||
static CTXT: RefCell<Option<ParseContext>> = const { RefCell::new(None) };
|
||||
@ -337,7 +339,7 @@ fn pipe_expression(i: &mut TokenSlice) -> PResult<Node<PipeExpression>> {
|
||||
let mut values = vec![head];
|
||||
let value_surrounded_by_comments = (
|
||||
repeat(0.., preceded(opt(whitespace), non_code_node)), // Before the expression.
|
||||
preceded(opt(whitespace), fn_call), // The expression.
|
||||
preceded(opt(whitespace), labelled_fn_call), // The expression.
|
||||
repeat(0.., noncode_just_after_code), // After the expression.
|
||||
);
|
||||
let tail: Vec<(Vec<_>, _, Vec<_>)> = repeat(
|
||||
@ -353,7 +355,7 @@ fn pipe_expression(i: &mut TokenSlice) -> PResult<Node<PipeExpression>> {
|
||||
// First, ensure they all have a % in their args.
|
||||
let calls_without_substitution = tail.iter().find_map(|(_nc, call_expr, _nc2)| {
|
||||
if !call_expr.has_substitution_arg() {
|
||||
Some(call_expr.as_source_range())
|
||||
Some(call_expr.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -373,7 +375,7 @@ fn pipe_expression(i: &mut TokenSlice) -> PResult<Node<PipeExpression>> {
|
||||
max_noncode_end = nc.end.max(max_noncode_end);
|
||||
non_code_meta.insert(code_count, nc);
|
||||
}
|
||||
values.push(Expr::CallExpression(Box::new(code)));
|
||||
values.push(code);
|
||||
code_count += 1;
|
||||
for nc in noncode_after {
|
||||
max_noncode_end = nc.end.max(max_noncode_end);
|
||||
@ -527,7 +529,8 @@ fn operand(i: &mut TokenSlice) -> PResult<BinaryPart> {
|
||||
| Expr::PipeSubstitution(_)
|
||||
| Expr::ArrayExpression(_)
|
||||
| Expr::ArrayRangeExpression(_)
|
||||
| Expr::ObjectExpression(_) => return Err(CompilationError::fatal(source_range, TODO_783)),
|
||||
| Expr::ObjectExpression(_)
|
||||
| Expr::LabelledExpression(..) => return Err(CompilationError::fatal(source_range, TODO_783)),
|
||||
Expr::None(_) => {
|
||||
return Err(CompilationError::fatal(
|
||||
source_range,
|
||||
@ -1628,13 +1631,34 @@ fn expression(i: &mut TokenSlice) -> PResult<Expr> {
|
||||
}
|
||||
|
||||
fn expression_but_not_pipe(i: &mut TokenSlice) -> PResult<Expr> {
|
||||
alt((
|
||||
let expr = alt((
|
||||
binary_expression.map(Box::new).map(Expr::BinaryExpression),
|
||||
unary_expression.map(Box::new).map(Expr::UnaryExpression),
|
||||
expr_allowed_in_pipe_expr,
|
||||
))
|
||||
.context(expected("a KCL value"))
|
||||
.parse_next(i)
|
||||
.parse_next(i)?;
|
||||
|
||||
let label = opt(label).parse_next(i)?;
|
||||
match label {
|
||||
Some(label) => Ok(Expr::LabelledExpression(Box::new(LabelledExpression::new(expr, label)))),
|
||||
None => Ok(expr),
|
||||
}
|
||||
}
|
||||
|
||||
fn label(i: &mut TokenSlice) -> PResult<Node<Identifier>> {
|
||||
let result = preceded(
|
||||
(whitespace, import_as_keyword, whitespace),
|
||||
identifier.context(expected("an identifier")),
|
||||
)
|
||||
.parse_next(i)?;
|
||||
|
||||
ParseContext::warn(CompilationError::err(
|
||||
SourceRange::new(result.start, result.end, result.module_id),
|
||||
"Using `as` for tagging expressions is experimental, likely to be buggy, and likely to change",
|
||||
));
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn unnecessarily_bracketed(i: &mut TokenSlice) -> PResult<Expr> {
|
||||
@ -2280,23 +2304,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 +2350,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 +2358,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,
|
||||
})
|
||||
@ -2443,6 +2474,17 @@ fn typecheck(spec_arg: &crate::docs::StdLibFnArg, arg: &&Expr) -> PResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn labelled_fn_call(i: &mut TokenSlice) -> PResult<Expr> {
|
||||
let call = fn_call.parse_next(i)?;
|
||||
let expr = Expr::CallExpression(Box::new(call));
|
||||
|
||||
let label = opt(label).parse_next(i)?;
|
||||
match label {
|
||||
Some(label) => Ok(Expr::LabelledExpression(Box::new(LabelledExpression::new(expr, label)))),
|
||||
None => Ok(expr),
|
||||
}
|
||||
}
|
||||
|
||||
fn fn_call(i: &mut TokenSlice) -> PResult<Node<CallExpression>> {
|
||||
let fn_name = identifier(i)?;
|
||||
opt(whitespace).parse_next(i)?;
|
||||
@ -4459,6 +4501,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
|
||||
}
|
@ -5,6 +5,7 @@ use std::{fmt, iter::Enumerate, num::NonZeroUsize};
|
||||
|
||||
use anyhow::Result;
|
||||
use parse_display::Display;
|
||||
use tokeniser::Input;
|
||||
use tower_lsp::lsp_types::SemanticTokenType;
|
||||
use winnow::{
|
||||
self,
|
||||
@ -17,7 +18,6 @@ use crate::{
|
||||
parsing::ast::types::{ItemVisibility, VariableKind},
|
||||
source_range::{ModuleId, SourceRange},
|
||||
};
|
||||
use tokeniser::Input;
|
||||
|
||||
mod tokeniser;
|
||||
|
||||
|
@ -10,13 +10,12 @@ use winnow::{
|
||||
Located, Stateful,
|
||||
};
|
||||
|
||||
use super::TokenStream;
|
||||
use crate::{
|
||||
parsing::token::{Token, TokenType},
|
||||
source_range::ModuleId,
|
||||
};
|
||||
|
||||
use super::TokenStream;
|
||||
|
||||
lazy_static! {
|
||||
pub(crate) static ref RESERVED_WORDS: FnvHashMap<&'static str, TokenType> = {
|
||||
let mut set = FnvHashMap::default();
|
||||
@ -365,9 +364,8 @@ fn keyword_type_or_word(i: &mut Input<'_>) -> PResult<Token> {
|
||||
mod tests {
|
||||
use winnow::Located;
|
||||
|
||||
use crate::parsing::token::TokenSlice;
|
||||
|
||||
use super::*;
|
||||
use crate::parsing::token::TokenSlice;
|
||||
fn assert_parse_err<'i, P, O, E>(mut p: P, s: &'i str)
|
||||
where
|
||||
O: std::fmt::Debug,
|
||||
|
@ -380,9 +380,9 @@ impl From<UnitLength> for kittycad_modeling_cmds::units::UnitLength {
|
||||
#[display(style = "snake_case")]
|
||||
pub enum MouseControlType {
|
||||
#[default]
|
||||
#[display("kittycad")]
|
||||
#[serde(rename = "kittycad", alias = "KittyCAD")]
|
||||
KittyCad,
|
||||
#[display("zoo")]
|
||||
#[serde(rename = "zoo", alias = "Zoo", alias = "KittyCAD")]
|
||||
Zoo,
|
||||
#[display("onshape")]
|
||||
#[serde(rename = "onshape", alias = "OnShape")]
|
||||
OnShape,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
303
src/wasm-lib/kcl/src/std/appearance.rs
Normal file
@ -0,0 +1,303 @@
|
||||
//! Standard library appearance.
|
||||
|
||||
use anyhow::Result;
|
||||
use derive_docs::stdlib;
|
||||
use kcmc::{each_cmd as mcmd, ModelingCmd};
|
||||
use kittycad_modeling_cmds::{self as kcmc, shared::Color};
|
||||
use regex::Regex;
|
||||
use rgba_simple::Hex;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
execution::{ExecState, KclValue, Solid, SolidSet},
|
||||
std::Args,
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref HEX_REGEX: Regex = Regex::new(r"^#[0-9a-fA-F]{6}$").unwrap();
|
||||
}
|
||||
|
||||
/// Data for appearance.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Validate)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppearanceData {
|
||||
/// Color of the new material, a hex string like "#ff0000".
|
||||
#[schemars(regex(pattern = "#[0-9a-fA-F]{6}"))]
|
||||
pub color: String,
|
||||
/// Metalness of the new material, a percentage like 95.7.
|
||||
#[validate(range(min = 0.0, max = 100.0))]
|
||||
pub metalness: Option<f64>,
|
||||
/// Roughness of the new material, a percentage like 95.7.
|
||||
#[validate(range(min = 0.0, max = 100.0))]
|
||||
pub roughness: Option<f64>,
|
||||
// TODO(jess): we can also ambient occlusion here I just don't know what it is.
|
||||
}
|
||||
|
||||
/// Set the appearance of a solid. This only works on solids, not sketches or individual paths.
|
||||
pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let (data, solid_set): (AppearanceData, SolidSet) = args.get_data_and_solid_set()?;
|
||||
|
||||
// Validate the data.
|
||||
data.validate().map_err(|err| {
|
||||
KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Invalid appearance data: {}", err),
|
||||
source_ranges: vec![args.source_range],
|
||||
})
|
||||
})?;
|
||||
|
||||
// Make sure the color if set is valid.
|
||||
if !HEX_REGEX.is_match(&data.color) {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Invalid hex color (`{}`), try something like `#fff000`", data.color),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
}
|
||||
|
||||
let result = inner_appearance(data, solid_set, args).await?;
|
||||
Ok(result.into())
|
||||
}
|
||||
|
||||
/// Set the appearance of a solid. This only works on solids, not sketches or individual paths.
|
||||
///
|
||||
/// This will work on any solid, including extruded solids, revolved solids, and shelled solids.
|
||||
/// ```no_run
|
||||
/// // Add color to an extruded solid.
|
||||
/// exampleSketch = startSketchOn("XZ")
|
||||
/// |> startProfileAt([0, 0], %)
|
||||
/// |> lineTo([10, 0], %)
|
||||
/// |> lineTo([0, 10], %)
|
||||
/// |> lineTo([-10, 0], %)
|
||||
/// |> close(%)
|
||||
///
|
||||
/// example = extrude(5, exampleSketch)
|
||||
/// |> appearance({color= '#ff0000', metalness= 50, roughness= 50}, %)
|
||||
/// ```
|
||||
///
|
||||
/// ```no_run
|
||||
/// // Add color to a revolved solid.
|
||||
/// sketch001 = startSketchOn('XY')
|
||||
/// |> circle({ center = [15, 0], radius = 5 }, %)
|
||||
/// |> revolve({ angle = 360, axis = 'y' }, %)
|
||||
/// |> appearance({
|
||||
/// color = '#ff0000',
|
||||
/// metalness = 90,
|
||||
/// roughness = 90
|
||||
/// }, %)
|
||||
/// ```
|
||||
///
|
||||
/// ```no_run
|
||||
/// // Add color to different solids.
|
||||
/// fn cube(center) {
|
||||
/// return startSketchOn('XY')
|
||||
/// |> startProfileAt([center[0] - 10, center[1] - 10], %)
|
||||
/// |> lineTo([center[0] + 10, center[1] - 10], %)
|
||||
/// |> lineTo([center[0] + 10, center[1] + 10], %)
|
||||
/// |> lineTo([center[0] - 10, center[1] + 10], %)
|
||||
/// |> close(%)
|
||||
/// |> extrude(10, %)
|
||||
/// }
|
||||
///
|
||||
/// example0 = cube([0, 0])
|
||||
/// example1 = cube([20, 0])
|
||||
/// example2 = cube([40, 0])
|
||||
///
|
||||
/// appearance({color= '#ff0000', metalness= 50, roughness= 50}, [example0, example1])
|
||||
/// appearance({color= '#00ff00', metalness= 50, roughness= 50}, example2)
|
||||
/// ```
|
||||
///
|
||||
/// ```no_run
|
||||
/// // You can set the appearance before or after you shell it will yield the same result.
|
||||
/// // This example shows setting the appearance _after_ the shell.
|
||||
/// firstSketch = startSketchOn('XY')
|
||||
/// |> startProfileAt([-12, 12], %)
|
||||
/// |> line([24, 0], %)
|
||||
/// |> line([0, -24], %)
|
||||
/// |> line([-24, 0], %)
|
||||
/// |> close(%)
|
||||
/// |> extrude(6, %)
|
||||
///
|
||||
/// shell({
|
||||
/// faces = ['end'],
|
||||
/// thickness = 0.25,
|
||||
/// }, firstSketch)
|
||||
/// |> appearance({
|
||||
/// color = '#ff0000',
|
||||
/// metalness = 90,
|
||||
/// roughness = 90
|
||||
/// }, %)
|
||||
/// ```
|
||||
///
|
||||
/// ```no_run
|
||||
/// // You can set the appearance before or after you shell it will yield the same result.
|
||||
/// // This example shows setting the appearance _before_ the shell.
|
||||
/// firstSketch = startSketchOn('XY')
|
||||
/// |> startProfileAt([-12, 12], %)
|
||||
/// |> line([24, 0], %)
|
||||
/// |> line([0, -24], %)
|
||||
/// |> line([-24, 0], %)
|
||||
/// |> close(%)
|
||||
/// |> extrude(6, %)
|
||||
/// |> appearance({
|
||||
/// color = '#ff0000',
|
||||
/// metalness = 90,
|
||||
/// roughness = 90
|
||||
/// }, %)
|
||||
///
|
||||
/// shell({
|
||||
/// faces = ['end'],
|
||||
/// thickness = 0.25,
|
||||
/// }, firstSketch)
|
||||
/// ```
|
||||
///
|
||||
/// ```no_run
|
||||
/// // Setting the appearance of a 3D pattern can be done _before_ or _after_ the pattern.
|
||||
/// // This example shows _before_ the pattern.
|
||||
/// exampleSketch = startSketchOn('XZ')
|
||||
/// |> startProfileAt([0, 0], %)
|
||||
/// |> line([0, 2], %)
|
||||
/// |> line([3, 1], %)
|
||||
/// |> line([0, -4], %)
|
||||
/// |> close(%)
|
||||
///
|
||||
/// example = extrude(1, exampleSketch)
|
||||
/// |> appearance({
|
||||
/// color = '#ff0000',
|
||||
/// metalness = 90,
|
||||
/// roughness = 90
|
||||
/// }, %)
|
||||
/// |> patternLinear3d({
|
||||
/// axis = [1, 0, 1],
|
||||
/// instances = 7,
|
||||
/// distance = 6
|
||||
/// }, %)
|
||||
/// ```
|
||||
///
|
||||
/// ```no_run
|
||||
/// // Setting the appearance of a 3D pattern can be done _before_ or _after_ the pattern.
|
||||
/// // This example shows _after_ the pattern.
|
||||
/// exampleSketch = startSketchOn('XZ')
|
||||
/// |> startProfileAt([0, 0], %)
|
||||
/// |> line([0, 2], %)
|
||||
/// |> line([3, 1], %)
|
||||
/// |> line([0, -4], %)
|
||||
/// |> close(%)
|
||||
///
|
||||
/// example = extrude(1, exampleSketch)
|
||||
/// |> patternLinear3d({
|
||||
/// axis = [1, 0, 1],
|
||||
/// instances = 7,
|
||||
/// distance = 6
|
||||
/// }, %)
|
||||
/// |> appearance({
|
||||
/// color = '#ff0000',
|
||||
/// metalness = 90,
|
||||
/// roughness = 90
|
||||
/// }, %)
|
||||
/// ```
|
||||
///
|
||||
/// ```no_run
|
||||
/// // Color the result of a 2D pattern that was extruded.
|
||||
/// exampleSketch = startSketchOn('XZ')
|
||||
/// |> startProfileAt([.5, 25], %)
|
||||
/// |> line([0, 5], %)
|
||||
/// |> line([-1, 0], %)
|
||||
/// |> line([0, -5], %)
|
||||
/// |> close(%)
|
||||
/// |> patternCircular2d({
|
||||
/// center = [0, 0],
|
||||
/// instances = 13,
|
||||
/// arcDegrees = 360,
|
||||
/// rotateDuplicates = true
|
||||
/// }, %)
|
||||
///
|
||||
/// example = extrude(1, exampleSketch)
|
||||
/// |> appearance({
|
||||
/// color = '#ff0000',
|
||||
/// metalness = 90,
|
||||
/// roughness = 90
|
||||
/// }, %)
|
||||
/// ```
|
||||
///
|
||||
/// ```no_run
|
||||
/// // Color the result of a sweep.
|
||||
///
|
||||
/// // Create a path for the sweep.
|
||||
/// sweepPath = startSketchOn('XZ')
|
||||
/// |> startProfileAt([0.05, 0.05], %)
|
||||
/// |> line([0, 7], %)
|
||||
/// |> tangentialArc({
|
||||
/// offset: 90,
|
||||
/// radius: 5
|
||||
/// }, %)
|
||||
/// |> line([-3, 0], %)
|
||||
/// |> tangentialArc({
|
||||
/// offset: -90,
|
||||
/// radius: 5
|
||||
/// }, %)
|
||||
/// |> line([0, 7], %)
|
||||
///
|
||||
/// pipeHole = startSketchOn('XY')
|
||||
/// |> circle({
|
||||
/// center = [0, 0],
|
||||
/// radius = 1.5,
|
||||
/// }, %)
|
||||
///
|
||||
/// sweepSketch = startSketchOn('XY')
|
||||
/// |> circle({
|
||||
/// center = [0, 0],
|
||||
/// radius = 2,
|
||||
/// }, %)
|
||||
/// |> hole(pipeHole, %)
|
||||
/// |> sweep({
|
||||
/// path: sweepPath,
|
||||
/// }, %)
|
||||
/// |> appearance({
|
||||
/// color: "#ff0000",
|
||||
/// metalness: 50,
|
||||
/// roughness: 50
|
||||
/// }, %)
|
||||
/// ```
|
||||
#[stdlib {
|
||||
name = "appearance",
|
||||
}]
|
||||
async fn inner_appearance(data: AppearanceData, solid_set: SolidSet, args: Args) -> Result<SolidSet, KclError> {
|
||||
let solids: Vec<Box<Solid>> = solid_set.into();
|
||||
|
||||
for solid in &solids {
|
||||
// Set the material properties.
|
||||
let rgb = rgba_simple::RGB::<f32>::from_hex(&data.color).map_err(|err| {
|
||||
KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Invalid hex color (`{}`): {}", data.color, err),
|
||||
source_ranges: vec![args.source_range],
|
||||
})
|
||||
})?;
|
||||
|
||||
let color = Color {
|
||||
r: rgb.red,
|
||||
g: rgb.green,
|
||||
b: rgb.blue,
|
||||
a: 100.0,
|
||||
};
|
||||
|
||||
args.batch_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
ModelingCmd::from(mcmd::ObjectSetMaterialParamsPbr {
|
||||
object_id: solid.id,
|
||||
color,
|
||||
metalness: data.metalness.unwrap_or_default() as f32 / 100.0,
|
||||
roughness: data.roughness.unwrap_or_default() as f32 / 100.0,
|
||||
ambient_occlusion: 0.0,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Idk if we want to actually modify the memory for the colors, but I'm not right now since
|
||||
// I can't think of a use case for it.
|
||||
}
|
||||
|
||||
Ok(SolidSet::from(solids))
|
||||
}
|
@ -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.
|
||||
@ -1089,6 +1096,34 @@ impl<'a> FromKclValue<'a> for super::fillet::FilletData {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromKclValue<'a> for super::sweep::SweepData {
|
||||
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
|
||||
let obj = arg.as_object()?;
|
||||
let_field_of!(obj, path);
|
||||
let_field_of!(obj, sectional?);
|
||||
let_field_of!(obj, tolerance?);
|
||||
Some(Self {
|
||||
path,
|
||||
sectional,
|
||||
tolerance,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromKclValue<'a> for super::appearance::AppearanceData {
|
||||
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
|
||||
let obj = arg.as_object()?;
|
||||
let_field_of!(obj, color);
|
||||
let_field_of!(obj, metalness?);
|
||||
let_field_of!(obj, roughness?);
|
||||
Some(Self {
|
||||
color,
|
||||
metalness,
|
||||
roughness,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromKclValue<'a> for super::helix::HelixData {
|
||||
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
|
||||
let obj = arg.as_object()?;
|
||||
|
@ -1,5 +1,6 @@
|
||||
//! Functions implemented for language execution.
|
||||
|
||||
pub mod appearance;
|
||||
pub mod args;
|
||||
pub mod array;
|
||||
pub mod assert;
|
||||
@ -20,6 +21,7 @@ pub mod segment;
|
||||
pub mod shapes;
|
||||
pub mod shell;
|
||||
pub mod sketch;
|
||||
pub mod sweep;
|
||||
pub mod types;
|
||||
pub mod units;
|
||||
pub mod utils;
|
||||
@ -50,6 +52,7 @@ lazy_static! {
|
||||
Box::new(LegLen),
|
||||
Box::new(LegAngX),
|
||||
Box::new(LegAngY),
|
||||
Box::new(crate::std::appearance::Appearance),
|
||||
Box::new(crate::std::convert::Int),
|
||||
Box::new(crate::std::extrude::Extrude),
|
||||
Box::new(crate::std::segment::SegEnd),
|
||||
@ -112,6 +115,7 @@ lazy_static! {
|
||||
Box::new(crate::std::shell::Shell),
|
||||
Box::new(crate::std::shell::Hollow),
|
||||
Box::new(crate::std::revolve::Revolve),
|
||||
Box::new(crate::std::sweep::Sweep),
|
||||
Box::new(crate::std::loft::Loft),
|
||||
Box::new(crate::std::planes::OffsetPlane),
|
||||
Box::new(crate::std::import::Import),
|
||||
|
102
src/wasm-lib/kcl/src/std/sweep.rs
Normal file
@ -0,0 +1,102 @@
|
||||
//! Standard library sweep.
|
||||
|
||||
use anyhow::Result;
|
||||
use derive_docs::stdlib;
|
||||
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, ModelingCmd};
|
||||
use kittycad_modeling_cmds::{self as kcmc};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
errors::KclError,
|
||||
execution::{ExecState, KclValue, Sketch, Solid},
|
||||
std::{extrude::do_post_extrude, fillet::default_tolerance, Args},
|
||||
};
|
||||
|
||||
/// Data for a sweep.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
pub struct SweepData {
|
||||
/// The path to sweep along.
|
||||
pub path: Sketch,
|
||||
/// If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components.
|
||||
pub sectional: Option<bool>,
|
||||
/// Tolerance for the sweep operation.
|
||||
#[serde(default)]
|
||||
pub tolerance: Option<f64>,
|
||||
}
|
||||
|
||||
/// Extrude a sketch along a path.
|
||||
pub async fn sweep(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let (data, sketch): (SweepData, Sketch) = args.get_data_and_sketch()?;
|
||||
|
||||
let solid = inner_sweep(data, sketch, exec_state, args).await?;
|
||||
Ok(KclValue::Solid(solid))
|
||||
}
|
||||
|
||||
/// Extrude a sketch along a path.
|
||||
///
|
||||
/// This, like extrude, is able to create a 3-dimensional solid from a
|
||||
/// 2-dimensional sketch. However, unlike extrude, this creates a solid
|
||||
/// by using the extent of the sketch as its path. This is useful for
|
||||
/// creating more complex shapes that can't be created with a simple
|
||||
/// extrusion.
|
||||
///
|
||||
/// ```no_run
|
||||
/// // Create a pipe using a sweep.
|
||||
///
|
||||
/// // Create a path for the sweep.
|
||||
/// sweepPath = startSketchOn('XZ')
|
||||
/// |> startProfileAt([0.05, 0.05], %)
|
||||
/// |> line([0, 7], %)
|
||||
/// |> tangentialArc({
|
||||
/// offset: 90,
|
||||
/// radius: 5
|
||||
/// }, %)
|
||||
/// |> line([-3, 0], %)
|
||||
/// |> tangentialArc({
|
||||
/// offset: -90,
|
||||
/// radius: 5
|
||||
/// }, %)
|
||||
/// |> line([0, 7], %)
|
||||
///
|
||||
/// // Create a hole for the pipe.
|
||||
/// pipeHole = startSketchOn('XY')
|
||||
/// |> circle({
|
||||
/// center = [0, 0],
|
||||
/// radius = 1.5,
|
||||
/// }, %)
|
||||
///
|
||||
/// sweepSketch = startSketchOn('XY')
|
||||
/// |> circle({
|
||||
/// center = [0, 0],
|
||||
/// radius = 2,
|
||||
/// }, %)
|
||||
/// |> hole(pipeHole, %)
|
||||
/// |> sweep({
|
||||
/// path: sweepPath,
|
||||
/// }, %)
|
||||
/// ```
|
||||
#[stdlib {
|
||||
name = "sweep",
|
||||
}]
|
||||
async fn inner_sweep(
|
||||
data: SweepData,
|
||||
sketch: Sketch,
|
||||
exec_state: &mut ExecState,
|
||||
args: Args,
|
||||
) -> Result<Box<Solid>, KclError> {
|
||||
let id = exec_state.id_generator.next_uuid();
|
||||
args.batch_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::from(mcmd::Sweep {
|
||||
target: sketch.id.into(),
|
||||
trajectory: data.path.id.into(),
|
||||
sectional: data.sectional.unwrap_or(false),
|
||||
tolerance: LengthUnit(data.tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
do_post_extrude(sketch, 0.0, exec_state, args).await
|
||||
}
|
@ -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,
|
||||
};
|
||||
@ -194,6 +194,12 @@ impl Expr {
|
||||
Expr::UnaryExpression(unary_exp) => unary_exp.recast(options),
|
||||
Expr::IfExpression(e) => e.recast(options, indentation_level, ctxt),
|
||||
Expr::PipeSubstitution(_) => crate::parsing::PIPE_SUBSTITUTION_OPERATOR.to_string(),
|
||||
Expr::LabelledExpression(e) => {
|
||||
let mut result = e.expr.recast(options, indentation_level, ctxt);
|
||||
result += " as ";
|
||||
result += &e.label.name;
|
||||
result
|
||||
}
|
||||
Expr::None(_) => {
|
||||
unimplemented!("there is no literal None, see https://github.com/KittyCAD/modeling-app/issues/1115")
|
||||
}
|
||||
@ -407,7 +413,8 @@ fn expr_is_trivial(expr: &Expr) -> bool {
|
||||
| Expr::ObjectExpression(_)
|
||||
| Expr::MemberExpression(_)
|
||||
| Expr::UnaryExpression(_)
|
||||
| Expr::IfExpression(_) => false,
|
||||
| Expr::IfExpression(_)
|
||||
| Expr::LabelledExpression(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
@ -662,15 +669,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
|
||||
}
|
||||
@ -1516,6 +1526,28 @@ tabs_l = startSketchOn({
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_as() {
|
||||
let some_program_string = r#"fn cube(pos, scale) {
|
||||
x = dfsfs + dfsfsd as y
|
||||
|
||||
sg = startSketchOn('XY')
|
||||
|> startProfileAt(pos, %) as foo
|
||||
|> line([0, scale], %)
|
||||
|> line([scale, 0], %) as bar
|
||||
|> line([0 as baz, -scale] as qux, %)
|
||||
|> close(%)
|
||||
|> extrude(scale, %)
|
||||
}
|
||||
|
||||
cube(0, 0) as cub
|
||||
"#;
|
||||
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
|
||||
|
||||
let recasted = program.recast(&Default::default(), 0);
|
||||
assert_eq!(recasted, some_program_string,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recast_with_bad_indentation() {
|
||||
let some_program_string = r#"part001 = startSketchOn('XY')
|
||||
|
@ -32,17 +32,52 @@ pub enum Node<'a> {
|
||||
UnaryExpression(NodeRef<'a, types::UnaryExpression>),
|
||||
IfExpression(NodeRef<'a, types::IfExpression>),
|
||||
ElseIf(&'a types::ElseIf),
|
||||
LabelledExpression(NodeRef<'a, types::LabelledExpression>),
|
||||
|
||||
Parameter(&'a types::Parameter),
|
||||
|
||||
ObjectProperty(NodeRef<'a, types::ObjectProperty>),
|
||||
|
||||
MemberObject(&'a types::MemberObject),
|
||||
LiteralIdentifier(&'a types::LiteralIdentifier),
|
||||
|
||||
KclNone(&'a types::KclNone),
|
||||
}
|
||||
|
||||
impl Node<'_> {
|
||||
/// Return the digest of the [Node], pulling the underlying Digest from
|
||||
/// the AST types.
|
||||
///
|
||||
/// The Digest type may change over time.
|
||||
pub fn digest(&self) -> Option<[u8; 32]> {
|
||||
match self {
|
||||
Node::Program(n) => n.digest,
|
||||
Node::ImportStatement(n) => n.digest,
|
||||
Node::ExpressionStatement(n) => n.digest,
|
||||
Node::VariableDeclaration(n) => n.digest,
|
||||
Node::ReturnStatement(n) => n.digest,
|
||||
Node::VariableDeclarator(n) => n.digest,
|
||||
Node::Literal(n) => n.digest,
|
||||
Node::TagDeclarator(n) => n.digest,
|
||||
Node::Identifier(n) => n.digest,
|
||||
Node::BinaryExpression(n) => n.digest,
|
||||
Node::FunctionExpression(n) => n.digest,
|
||||
Node::CallExpression(n) => n.digest,
|
||||
Node::CallExpressionKw(n) => n.digest,
|
||||
Node::PipeExpression(n) => n.digest,
|
||||
Node::PipeSubstitution(n) => n.digest,
|
||||
Node::ArrayExpression(n) => n.digest,
|
||||
Node::ArrayRangeExpression(n) => n.digest,
|
||||
Node::ObjectExpression(n) => n.digest,
|
||||
Node::MemberExpression(n) => n.digest,
|
||||
Node::UnaryExpression(n) => n.digest,
|
||||
Node::Parameter(p) => p.digest,
|
||||
Node::ObjectProperty(n) => n.digest,
|
||||
Node::IfExpression(n) => n.digest,
|
||||
Node::ElseIf(n) => n.digest,
|
||||
Node::KclNone(n) => n.digest,
|
||||
Node::LabelledExpression(n) => n.digest,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returned during source_range conversion.
|
||||
#[derive(Debug)]
|
||||
pub enum AstNodeError {
|
||||
@ -77,9 +112,8 @@ impl TryFrom<&Node<'_>> for SourceRange {
|
||||
Node::UnaryExpression(n) => SourceRange::from(*n),
|
||||
Node::Parameter(p) => SourceRange::from(&p.identifier),
|
||||
Node::ObjectProperty(n) => SourceRange::from(*n),
|
||||
Node::MemberObject(m) => SourceRange::new(m.start(), m.end(), m.module_id()),
|
||||
Node::IfExpression(n) => SourceRange::from(*n),
|
||||
Node::LiteralIdentifier(l) => SourceRange::new(l.start(), l.end(), l.module_id()),
|
||||
Node::LabelledExpression(n) => SourceRange::from(*n),
|
||||
|
||||
// This is broken too
|
||||
Node::ElseIf(n) => SourceRange::new(n.cond.start(), n.cond.end(), n.cond.module_id()),
|
||||
@ -120,6 +154,7 @@ impl<'tree> From<&'tree types::Expr> for Node<'tree> {
|
||||
types::Expr::MemberExpression(me) => me.as_ref().into(),
|
||||
types::Expr::UnaryExpression(ue) => ue.as_ref().into(),
|
||||
types::Expr::IfExpression(e) => e.as_ref().into(),
|
||||
types::Expr::LabelledExpression(e) => e.as_ref().into(),
|
||||
types::Expr::None(n) => n.into(),
|
||||
}
|
||||
}
|
||||
@ -140,6 +175,24 @@ impl<'tree> From<&'tree types::BinaryPart> for Node<'tree> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'tree> From<&'tree types::MemberObject> for Node<'tree> {
|
||||
fn from(node: &'tree types::MemberObject) -> Self {
|
||||
match node {
|
||||
types::MemberObject::MemberExpression(me) => me.as_ref().into(),
|
||||
types::MemberObject::Identifier(id) => id.as_ref().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'tree> From<&'tree types::LiteralIdentifier> for Node<'tree> {
|
||||
fn from(node: &'tree types::LiteralIdentifier) -> Self {
|
||||
match node {
|
||||
types::LiteralIdentifier::Identifier(id) => id.as_ref().into(),
|
||||
types::LiteralIdentifier::Literal(lit) => lit.as_ref().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_from {
|
||||
($node:ident, $t: ident) => {
|
||||
impl<'a> From<NodeRef<'a, types::$t>> for Node<'a> {
|
||||
@ -182,8 +235,7 @@ impl_from!(Node, MemberExpression);
|
||||
impl_from!(Node, UnaryExpression);
|
||||
impl_from!(Node, ObjectProperty);
|
||||
impl_from_ref!(Node, Parameter);
|
||||
impl_from_ref!(Node, MemberObject);
|
||||
impl_from!(Node, IfExpression);
|
||||
impl_from!(Node, ElseIf);
|
||||
impl_from_ref!(Node, LiteralIdentifier);
|
||||
impl_from!(Node, LabelledExpression);
|
||||
impl_from!(Node, KclNone);
|
||||
|
@ -127,12 +127,13 @@ impl<'tree> Visitable<'tree> for Node<'tree> {
|
||||
Node::ElseIf(n) => {
|
||||
vec![(&n.cond).into(), n.then_val.as_ref().into()]
|
||||
}
|
||||
Node::LabelledExpression(e) => {
|
||||
vec![(&e.expr).into(), (&e.label).into()]
|
||||
}
|
||||
Node::PipeSubstitution(_)
|
||||
| Node::TagDeclarator(_)
|
||||
| Node::Identifier(_)
|
||||
| Node::ImportStatement(_)
|
||||
| Node::MemberObject(_)
|
||||
| Node::LiteralIdentifier(_)
|
||||
| Node::KclNone(_)
|
||||
| Node::Literal(_) => vec![],
|
||||
}
|
||||
@ -141,9 +142,10 @@ impl<'tree> Visitable<'tree> for Node<'tree> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use super::*;
|
||||
|
||||
macro_rules! kcl {
|
||||
( $kcl:expr ) => {{
|
||||
$crate::parsing::top_level_parse($kcl).unwrap()
|
||||
|
@ -3,4 +3,5 @@ mod ast_visitor;
|
||||
mod ast_walk;
|
||||
|
||||
pub use ast_node::Node;
|
||||
pub use ast_visitor::{Visitable, Visitor};
|
||||
pub use ast_walk::walk;
|
||||
|
@ -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
|
||||
}
|
After Width: | Height: | Size: 61 KiB |
After Width: | Height: | Size: 110 KiB |
After Width: | Height: | Size: 61 KiB |
After Width: | Height: | Size: 62 KiB |
After Width: | Height: | Size: 62 KiB |
After Width: | Height: | Size: 41 KiB |
After Width: | Height: | Size: 41 KiB |