Compare commits

..

1 Commits

Author SHA1 Message Date
34272b872d WIP: workaround coredump hotkey linux
Fixes #4059
2024-12-10 15:47:30 -05:00
109 changed files with 1410 additions and 10240 deletions

View File

@ -165,6 +165,7 @@ 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 }}
@ -172,6 +173,7 @@ 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
@ -227,6 +229,7 @@ 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

View File

@ -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@v5
uses: codecov/codecov-action@v4
with:
token: ${{secrets.CODECOV_TOKEN}}
fail_ci_if_error: true

View File

@ -22,5 +22,3 @@ 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.

File diff suppressed because one or more lines are too long

View File

@ -19,7 +19,6 @@ layout: manual
* [`angledLineThatIntersects`](kcl/angledLineThatIntersects)
* [`angledLineToX`](kcl/angledLineToX)
* [`angledLineToY`](kcl/angledLineToY)
* [`appearance`](kcl/appearance)
* [`arc`](kcl/arc)
* [`arcTo`](kcl/arcTo)
* [`asin`](kcl/asin)
@ -102,7 +101,6 @@ layout: manual
* [`startProfileAt`](kcl/startProfileAt)
* [`startSketchAt`](kcl/startSketchAt)
* [`startSketchOn`](kcl/startSketchOn)
* [`sweep`](kcl/sweep)
* [`tan`](kcl/tan)
* [`tangentToEnd`](kcl/tangentToEnd)
* [`tangentialArc`](kcl/tangentialArc)

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

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

View File

@ -12,10 +12,5 @@ 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 |

View File

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

View File

@ -950,75 +950,7 @@ test(
test.describe('Grid visibility', { tag: '@snapshot' }, () => {
// FIXME: Skip on macos its being weird.
// test.skip(process.platform === 'darwin', 'Skip on macos')
test('Grid turned off to on via command bar', async ({ page }) => {
const u = await getUtils(page)
const stream = page.getByTestId('stream')
const mask = [
page.locator('#app-header'),
page.locator('#sidebar-top-ribbon'),
page.locator('#sidebar-bottom-ribbon'),
]
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
// wait for execution done
await expect(
page.locator('[data-message-type="execution-done"]')
).toHaveCount(1)
await u.closeDebugPanel()
await u.closeKclCodePanel()
// TODO: Find a way to truly know that the objects have finished
// rendering, because an execution-done message is not sufficient.
await page.waitForTimeout(1000)
// Open the command bar.
await page
.getByRole('button', { name: 'Commands', exact: false })
.or(page.getByRole('button', { name: '⌘K' }))
.click()
const commandName = 'show scale grid'
const commandOption = page.getByRole('option', {
name: commandName,
exact: false,
})
const cmdSearchBar = page.getByPlaceholder('Search commands')
// This selector changes after we set the setting
await cmdSearchBar.fill(commandName)
await expect(commandOption).toBeVisible()
await commandOption.click()
const toggleInput = page.getByPlaceholder('Off')
await expect(toggleInput).toBeVisible()
await expect(toggleInput).toBeFocused()
// Select On
await page.keyboard.press('ArrowDown')
await expect(page.getByRole('option', { name: 'Off' })).toHaveAttribute(
'data-headlessui-state',
'active selected'
)
await page.keyboard.press('ArrowUp')
await expect(page.getByRole('option', { name: 'On' })).toHaveAttribute(
'data-headlessui-state',
'active'
)
await page.keyboard.press('Enter')
// Check the toast appeared
await expect(
page.getByText(`Set show scale grid to "true" as a user default`)
).toBeVisible()
await expect(stream).toHaveScreenshot({
maxDiffPixels: 100,
mask,
})
})
test.skip(process.platform === 'darwin', 'Skip on macos')
test('Grid turned off', async ({ page }) => {
const u = await getUtils(page)
@ -1164,109 +1096,3 @@ 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,
})
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -14,7 +14,7 @@ export const TEST_SETTINGS = {
},
modeling: {
defaultUnit: 'in',
mouseControls: 'Zoo',
mouseControls: 'KittyCAD',
cameraProjection: 'perspective',
showDebugPanel: true,
},

View File

@ -479,26 +479,4 @@ 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()
})
})
})

View File

@ -1,9 +1,20 @@
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 = {
@ -28,7 +39,26 @@ const config: ForgeConfig = {
extendInfo: 'Info.plist', // Information for file associations.
},
rebuildConfig: {},
makers: [],
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'),
},
}),
],
plugins: [
new VitePlugin({
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.

View File

@ -39,6 +39,7 @@
"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",
@ -68,7 +69,7 @@
"yargs": "^17.7.2"
},
"scripts": {
"start": "vite --port=3000 --host=0.0.0.0",
"start": "vite",
"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",
@ -103,6 +104,8 @@
"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",
@ -145,10 +148,17 @@
"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/plugin-fuses": "7.4.0",
"@electron-forge/plugin-vite": "7.4.0",
"@electron/fuses": "1.8.0",
"@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",
"@iarna/toml": "^2.2.5",
"@lezer/generator": "^1.7.1",
"@nabla/vite-plugin-eslint": "^2.0.5",
@ -178,9 +188,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",

View File

@ -200,7 +200,10 @@ function CoreDump() {
() => new CoreDumpManager(engineCommandManager, codeManager, token),
[]
)
useHotkeyWrapper(['mod + shift + .'], () => {
// TODO: revisit once progress is made on upstream issue
// https://github.com/JohannesKlauss/react-hotkeys-hook/issues/1064
// const hotkey = process.platform !== 'linux' ? 'mod + shift + .' : 'mod + shift + >'
useHotkeyWrapper(['mod + shift + .', 'mod + shift + >'], () => {
toast
.promise(
coreDump(coreDumpManager, true),

View File

@ -105,7 +105,7 @@ export class CameraControls {
pendingZoom: number | null = null
pendingRotation: Vector2 | null = null
pendingPan: Vector2 | null = null
interactionGuards: MouseGuard = cameraMouseDragGuards.Zoo
interactionGuards: MouseGuard = cameraMouseDragGuards.KittyCAD
isFovAnimationInProgress = false
perspectiveFovBeforeOrtho = 45
get isPerspective() {

View File

@ -1,23 +1,13 @@
import toast from 'react-hot-toast'
import { ActionIcon, ActionIconProps } from './ActionIcon'
import {
MouseEvent,
RefObject,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { RefObject, useEffect, useMemo, useRef, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { Dialog } from '@headlessui/react'
export interface ContextMenuProps
interface ContextMenuProps
extends Omit<React.HTMLAttributes<HTMLUListElement>, 'children'> {
items?: React.ReactElement[]
menuTargetElement?: RefObject<HTMLElement>
guard?: (e: globalThis.MouseEvent) => boolean
event?: 'contextmenu' | 'mouseup'
}
const DefaultContextMenuItems = [
@ -30,8 +20,6 @@ export function ContextMenu({
items = DefaultContextMenuItems,
menuTargetElement,
className,
guard,
event = 'contextmenu',
...props
}: ContextMenuProps) {
const dialogRef = useRef<HTMLDivElement>(null)
@ -44,15 +32,6 @@ 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)
@ -99,9 +78,21 @@ export function ContextMenu({
// Add context menu listener to target once mounted
useEffect(() => {
menuTargetElement?.current?.addEventListener(event, handleContextMenu)
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
)
return () => {
menuTargetElement?.current?.removeEventListener(event, handleContextMenu)
menuTargetElement?.current?.removeEventListener(
'contextmenu',
handleContextMenu
)
}
}, [menuTargetElement?.current])
@ -109,10 +100,7 @@ export function ContextMenu({
<Dialog open={open} onClose={() => setOpen(false)}>
<div
className="fixed inset-0 z-50 w-screen h-screen"
onContextMenu={(e) => {
e.preventDefault()
setPosition({ x: e.clientX, y: e.clientY })
}}
onContextMenu={(e) => e.preventDefault()}
>
<Dialog.Backdrop className="fixed z-10 inset-0" />
<Dialog.Panel

View File

@ -1,6 +1,6 @@
import { SceneInfra } from 'clientSideScene/sceneInfra'
import { sceneInfra } from 'lib/singletons'
import { MutableRefObject, useEffect, useRef } from 'react'
import { MutableRefObject, useEffect, useMemo, useRef } from 'react'
import {
WebGLRenderer,
Scene,
@ -19,14 +19,16 @@ 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 {
useViewControlMenuItems,
ViewControlContextMenu,
} from './ViewControlMenu'
import { AxisNames } from 'lib/constants'
import { useModelingContext } from 'hooks/useModelingContext'
const CANVAS_SIZE = 80
const FRUSTUM_SIZE = 0.5
@ -38,14 +40,64 @@ 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
@ -109,7 +161,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} />
<ViewControlContextMenu menuTargetElement={wrapperRef} />
<ContextMenu menuTargetElement={wrapperRef} items={menuItems} />
</div>
<GizmoDropdown items={menuItems} />
</div>

View File

@ -1,4 +1,4 @@
import { APP_VERSION, getReleaseUrl } from 'routes/Settings'
import { APP_VERSION, RELEASE_URL } 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(getReleaseUrl())}
href={getReleaseUrl()}
onClick={openExternalBrowserIfDesktop(RELEASE_URL)}
href={RELEASE_URL}
target="_blank"
rel="noopener noreferrer"
className={'!no-underline font-mono text-xs ' + linkOverrideClassName}

View File

@ -69,7 +69,14 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const [isKclLspReady, setIsKclLspReady] = useState(false)
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
const { auth } = useSettingsAuthContext()
const {
auth,
settings: {
context: {
modeling: { defaultUnit },
},
},
} = useSettingsAuthContext()
const token = auth?.context.token
const navigate = useNavigate()
@ -85,6 +92,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const initEvent: KclWorkerOptions = {
wasmUrl: wasmUrl(),
token: token,
baseUnit: defaultUnit.current,
apiBaseUrl: VITE_KC_API_BASE_URL,
}
lspWorker.postMessage({

View File

@ -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, getReleaseUrl } from 'routes/Settings'
import { APP_VERSION, IS_NIGHTLY, RELEASE_URL } 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(getReleaseUrl())}
href={getReleaseUrl()}
onClick={openExternalBrowserIfDesktop(RELEASE_URL)}
href={RELEASE_URL}
target="_blank"
rel="noopener noreferrer"
>

View File

@ -1,5 +1,5 @@
import { trap } from 'lib/trap'
import { useMachine, useSelector } from '@xstate/react'
import { useMachine } 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'
@ -23,6 +23,7 @@ import {
engineCommandManager,
sceneEntitiesManager,
} from 'lib/singletons'
import { uuidv4 } from 'lib/utils'
import { IndexLoaderData } from 'lib/types'
import { settings } from 'lib/settings/initialSettings'
import {
@ -54,15 +55,11 @@ type SettingsAuthContextType = {
settings: MachineContext<typeof settingsMachine>
}
/**
* 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
// 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
export const SettingsAuthContext = createContext({} as SettingsAuthContextType)
@ -132,11 +129,27 @@ export const SettingsAuthProviderBase = ({
.setTheme(context.app.theme.current)
.catch(reportRejection)
},
setEngineScaleGridVisibility: ({ context }) => {
engineCommandManager.setScaleGridVisibility(
context.modeling.showScaleGrid.current
)
},
setClientTheme: ({ context }) => {
const opposingTheme = getOppositeTheme(context.app.theme.current)
sceneInfra.theme = opposingTheme
sceneEntitiesManager.updateSegmentBaseColor(opposingTheme)
},
setEngineEdges: ({ context }) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.sendSceneCommand({
cmd_id: uuidv4(),
type: 'modeling_cmd_req',
cmd: {
type: 'edge_lines_visible' as any, // TODO update kittycad.ts to get this new command type
hidden: !context.modeling.highlightEdges.current,
},
})
},
toastSuccess: ({ event }) => {
if (!('data' in event)) return
const eventParts = event.type.replace(/^set./, '').split('.') as [
@ -162,27 +175,17 @@ export const SettingsAuthProviderBase = ({
},
'Execute AST': ({ context, event }) => {
try {
const relevantSetting = (s: typeof settings) => {
return (
s.modeling?.defaultUnit?.current !==
context.modeling.defaultUnit.current ||
s.modeling.showScaleGrid.current !==
context.modeling.showScaleGrid.current ||
s.modeling?.highlightEdges.current !==
context.modeling.highlightEdges.current
)
}
const allSettingsIncludesUnitChange =
event.type === 'Set all settings' &&
relevantSetting(event.settings)
event.settings?.modeling?.defaultUnit?.current !==
context.modeling.defaultUnit.current
const resetSettingsIncludesUnitChange =
event.type === 'Reset settings' && relevantSetting(settings)
event.type === 'Reset settings' &&
context.modeling.defaultUnit.current !==
settings?.modeling?.defaultUnit?.default
if (
event.type === 'set.modeling.defaultUnit' ||
event.type === 'set.modeling.showScaleGrid' ||
event.type === 'set.modeling.highlightEdges' ||
allSettingsIncludesUnitChange ||
resetSettingsIncludesUnitChange
) {
@ -211,10 +214,7 @@ export const SettingsAuthProviderBase = ({
}),
{ input: loadedSettings }
)
// Any time the actor changes, update the settings state for external use
useSelector(settingsActor, (s) => {
lastSettingsContextSnapshot = s.context
})
settingsStateRef = settingsState.context
useEffect(() => {
if (!isDesktop()) return

View File

@ -20,7 +20,6 @@ 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',
@ -31,7 +30,6 @@ 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()
@ -260,7 +258,7 @@ export const Stream = () => {
setIsLoading(false)
}, [mediaStream])
const handleClick: MouseEventHandler<HTMLDivElement> = (e) => {
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
// If we've got no stream or connection, don't do anything
if (!isNetworkOkay) return
if (!videoRef.current) return
@ -322,11 +320,10 @@ export const Stream = () => {
return (
<div
ref={videoWrapperRef}
className="absolute inset-0 z-0"
id="stream"
data-testid="stream"
onClick={handleClick}
onClick={handleMouseUp}
onDoubleClick={enterSketchModeIfSelectingSketch}
onContextMenu={(e) => e.preventDefault()}
onContextMenuCapture={(e) => e.preventDefault()}
@ -387,14 +384,6 @@ export const Stream = () => {
</Loading>
</div>
)}
<ViewControlContextMenu
event="mouseup"
guard={(e) =>
sceneInfra.camControls.wasDragging === false &&
btnName(e).right === true
}
menuTargetElement={videoWrapperRef}
/>
</div>
)
}

View File

@ -2,7 +2,6 @@ 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,
@ -33,8 +32,10 @@ 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(getReleaseUrl(version))}
href={getReleaseUrl(version)}
onClick={openExternalBrowserIfDesktop(
`https://github.com/KittyCAD/modeling-app/releases/tag/v${version}`
)}
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${version}`}
target="_blank"
rel="noreferrer"
>

View File

@ -41,10 +41,7 @@ export function UnitsMenu() {
close()
}}
>
<span className="flex-1">{baseUnitLabels[unit]}</span>
{unit === settings.context.modeling.defaultUnit.current && (
<span className="text-chalkboard-60">current</span>
)}
{baseUnitLabels[unit]}
</button>
</li>
))}

View File

@ -1,66 +0,0 @@
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}
/>
)
}

View File

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

View File

@ -17,7 +17,6 @@ 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[]
@ -55,14 +54,14 @@ export const KclLanguage = LRLanguage.define({
})
export function kcl(options: LanguageOptions) {
return new LanguageSupport(KclLanguage, [
colorPicker,
return new LanguageSupport(
KclLanguage,
kclPlugin({
documentUri: options.documentUri,
workspaceFolders: options.workspaceFolders,
allowHTMLContent: true,
client: options.client,
processLspNotification: options.processLspNotification,
}),
])
})
)
}

View File

@ -1,5 +1,7 @@
import { LspWorkerEventType } from '@kittycad/codemirror-lsp-client'
import { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
export enum LspWorker {
Kcl = 'kcl',
Copilot = 'copilot',
@ -7,6 +9,7 @@ export enum LspWorker {
export interface KclWorkerOptions {
wasmUrl: string
token: string
baseUnit: UnitLength
apiBaseUrl: string
}

View File

@ -17,6 +17,7 @@ import {
KclWorkerOptions,
CopilotWorkerOptions,
} from 'editor/plugins/lsp/types'
import { EngineCommandManager } from 'lang/std/engineConnection'
import { err, reportRejection } from 'lib/trap'
const intoServer: IntoServer = new IntoServer()
@ -45,12 +46,14 @@ export async function copilotLspRun(
export async function kclLspRun(
config: ServerConfig,
engineCommandManager: EngineCommandManager | null,
token: string,
baseUnit: string,
baseUrl: string
) {
try {
console.log('start kcl lsp')
await kcl_lsp_run(config, null, undefined, token, baseUrl)
await kcl_lsp_run(config, engineCommandManager, baseUnit, token, baseUrl)
} catch (e: any) {
console.log('kcl lsp failed', e)
// We can't restart here because a moved value, we should do this another way.
@ -79,7 +82,13 @@ onmessage = function (event: MessageEvent) {
switch (worker) {
case LspWorker.Kcl:
const kclData = eventData as KclWorkerOptions
await kclLspRun(config, kclData.token, kclData.apiBaseUrl)
await kclLspRun(
config,
null,
kclData.token,
kclData.baseUnit,
kclData.apiBaseUrl
)
break
case LspWorker.Copilot:
let copilotData = eventData as CopilotWorkerOptions

View File

@ -2,7 +2,7 @@ import { useLayoutEffect, useEffect, useRef } from 'react'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { deferExecution } from 'lib/utils'
import { Themes } from 'lib/theme'
import { makeDefaultPlanes } from 'lang/wasm'
import { makeDefaultPlanes, modifyGrid } from 'lang/wasm'
import { useModelingContext } from './useModelingContext'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { useAppState, useAppStream } from 'AppState'
@ -56,6 +56,9 @@ export function useSetupEngineManager(
makeDefaultPlanes: () => {
return makeDefaultPlanes(kclManager.engineCommandManager)
},
modifyGrid: (hidden: boolean) => {
return modifyGrid(kclManager.engineCommandManager, hidden)
},
})
hasSetNonZeroDimensions.current = true
}

View File

@ -317,8 +317,3 @@ 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;
}

View File

@ -40,6 +40,7 @@ beforeAll(async () => {
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
setMediaStream: () => {},
setIsStreamReady: () => {},
modifyGrid: async () => {},
callbackOnEngineLiteConnect: () => {
resolve(true)
},

View File

@ -139,6 +139,7 @@ beforeAll(async () => {
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
setMediaStream: () => {},
setIsStreamReady: () => {},
modifyGrid: async () => {},
// eslint-disable-next-line @typescript-eslint/no-misused-promises
callbackOnEngineLiteConnect: async () => {
const cacheEntries = Object.entries(codeToWriteCacheFor) as [

View File

@ -871,3 +871,15 @@ export function codeRefFromRange(range: SourceRange, ast: Program): CodeRef {
pathToNode: getNodePathFromSourceRange(ast, range),
}
}
export function isSolid2D(artifact: Artifact): artifact is solid2D {
return (artifact as solid2D).pathId !== undefined
}
export function isSegment(artifact: Artifact): artifact is SegmentArtifact {
return (artifact as SegmentArtifact).pathId !== undefined
}
export function isSweep(artifact: Artifact): artifact is SweepArtifact {
return (artifact as SweepArtifact).pathId !== undefined
}

View File

@ -1399,6 +1399,7 @@ export class EngineCommandManager extends EventTarget {
}
private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null
private modifyGrid: (hidden: boolean) => Promise<void> | null = () => null
private onEngineConnectionOpened = () => {}
private onEngineConnectionClosed = () => {}
@ -1431,6 +1432,7 @@ export class EngineCommandManager extends EventTarget {
height,
token,
makeDefaultPlanes,
modifyGrid,
settings = {
pool: null,
theme: Themes.Dark,
@ -1450,12 +1452,14 @@ export class EngineCommandManager extends EventTarget {
height: number
token?: string
makeDefaultPlanes: () => Promise<DefaultPlanes>
modifyGrid: (hidden: boolean) => Promise<void>
settings?: SettingsViaQueryString
}) {
if (settings) {
this.settings = settings
}
this.makeDefaultPlanes = makeDefaultPlanes
this.modifyGrid = modifyGrid
if (width === 0 || height === 0) {
return
}
@ -1535,15 +1539,21 @@ export class EngineCommandManager extends EventTarget {
type: 'default_camera_get_settings',
},
})
await this.initPlanes()
setIsStreamReady(true)
// We want modify the grid first because we don't want it to flash.
// Ideally these would already be default hidden in engine (TODO do
// that) https://github.com/KittyCAD/engine/issues/2282
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.modifyGrid(!this.settings.showScaleGrid)?.then(async () => {
await this.initPlanes()
setIsStreamReady(true)
// Other parts of the application should use this to react on scene ready.
this.dispatchEvent(
new CustomEvent(EngineCommandManagerEvents.SceneReady, {
detail: this.engineConnection,
})
)
// Other parts of the application should use this to react on scene ready.
this.dispatchEvent(
new CustomEvent(EngineCommandManagerEvents.SceneReady, {
detail: this.engineConnection,
})
)
})
}
this.engineConnection.addEventListener(
@ -2202,6 +2212,15 @@ export class EngineCommandManager extends EventTarget {
}).catch(reportRejection)
}
/**
* Set the visibility of the scale grid in the engine scene.
* @param visible - whether to show or hide the scale grid
*/
setScaleGridVisibility(visible: boolean) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.modifyGrid(!visible)
}
// Some "objects" have the same source range, such as sketch_mode_start and start_path.
// So when passing a range, we need to also specify the command type
mapRangeToObjectId(

View File

@ -1,13 +1,14 @@
import init, {
parse_wasm,
recast_wasm,
execute,
execute_wasm,
kcl_lint,
modify_ast_for_sketch_wasm,
is_points_ccw,
get_tangential_arc_to_info,
program_memory_init,
make_default_planes,
modify_grid,
coredump,
toml_stringify,
default_app_settings,
@ -42,9 +43,7 @@ import { Environment } from '../wasm-lib/kcl/bindings/Environment'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError'
import { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
import { getAllCurrentSettings } from 'lib/settings/settingsUtils'
export type { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
export type { ObjectExpression } from '../wasm-lib/kcl/bindings/ObjectExpression'
@ -93,26 +92,12 @@ 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]
}
@ -137,7 +122,7 @@ const initialise = async () => {
const fullUrl = wasmUrl()
const input = await fetch(fullUrl)
const buffer = await input.arrayBuffer()
return await init({ module_or_path: buffer })
return await init(buffer)
} catch (e) {
console.log('Error initialising WASM', e)
return Promise.reject(e)
@ -178,10 +163,6 @@ 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>
@ -512,19 +493,18 @@ export const _executor = async (
return Promise.reject(programMemoryOverride)
try {
let jsAppSettings = default_app_settings()
let baseUnit = 'mm'
if (!TEST) {
const lastSettingsSnapshot = await import(
'components/SettingsAuthProvider'
).then((module) => module.lastSettingsContextSnapshot)
if (lastSettingsSnapshot) {
jsAppSettings = getAllCurrentSettings(lastSettingsSnapshot)
}
const getSettingsState = import('components/SettingsAuthProvider').then(
(module) => module.getSettingsState
)
baseUnit =
(await getSettingsState)()?.modeling.defaultUnit.current || 'mm'
}
const execState: RawExecState = await execute(
const execState: RawExecState = await execute_wasm(
JSON.stringify(node),
JSON.stringify(programMemoryOverride?.toRaw() || null),
JSON.stringify({ settings: jsAppSettings }),
baseUnit,
engineCommandManager,
fileSystemManager
)
@ -572,6 +552,20 @@ export const makeDefaultPlanes = async (
}
}
export const modifyGrid = async (
engineCommandManager: EngineCommandManager,
hidden: boolean
): Promise<void> => {
try {
await modify_grid(engineCommandManager, hidden)
return
} catch (e) {
// TODO: do something real with the error.
console.log('modify grid error', e)
return Promise.reject(e)
}
}
export const modifyAstForSketch = async (
engineCommandManager: EngineCommandManager,
ast: Node<Program>,

View File

@ -10,7 +10,7 @@ const noModifiersPressed = (e: MouseEvent) =>
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
export type CameraSystem =
| 'Zoo'
| 'KittyCAD'
| 'OnShape'
| 'Trackpad Friendly'
| 'Solidworks'
@ -19,7 +19,7 @@ export type CameraSystem =
| 'AutoCAD'
export const cameraSystems: CameraSystem[] = [
'Zoo',
'KittyCAD',
'OnShape',
'Trackpad Friendly',
'Solidworks',
@ -34,8 +34,9 @@ export function mouseControlsToCameraSystem(
switch (mouseControl) {
// TODO: understand why the values come back without underscores and fix the root cause
// @ts-ignore: TS2678
case 'zoo':
return 'Zoo'
case 'kittycad':
case 'kitty_cad':
return 'KittyCAD'
// TODO: understand why the values come back without underscores and fix the root cause
// @ts-ignore: TS2678
case 'onshape':
@ -85,7 +86,7 @@ export const btnName = (e: MouseEvent) => ({
})
export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
Zoo: {
KittyCAD: {
pan: {
description: 'Shift + Right click drag or middle click drag',
callback: (e) =>

View File

@ -3,6 +3,7 @@ import { engineCommandManager } from 'lib/singletons'
import { uuidv4 } from 'lib/utils'
import { CommandBarContext } from 'machines/commandBarMachine'
import { Selections } from 'lib/selections'
import { isSolid2D, isSegment, isSweep } from 'lang/std/artifactGraph'
export const disableDryRunWithRetry = async (numberOfRetries = 3) => {
for (let tries = 0; tries < numberOfRetries; tries++) {
@ -63,7 +64,7 @@ export const revolveAxisValidator = async ({
return 'Unable to revolve, sketch not found'
}
if (!('pathId' in artifact)) {
if (!(isSolid2D(artifact) || isSegment(artifact) || isSweep(artifact))) {
return 'Unable to revolve, sketch has no path'
}

View File

@ -118,21 +118,3 @@ 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

View File

@ -283,7 +283,7 @@ export function createSettings() {
* The controls for how to navigate the 3D view
*/
mouseControls: new Setting<CameraSystem>({
defaultValue: 'Zoo',
defaultValue: 'KittyCAD',
description: 'The controls for how to navigate the 3D view',
validate: (v) => cameraSystems.includes(v as CameraSystem),
hideOnLevel: 'project',

View File

@ -2,7 +2,6 @@ import { DeepPartial } from 'lib/types'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import {
configurationToSettingsPayload,
getAllCurrentSettings,
projectConfigurationToSettingsPayload,
setSettingsAtLevel,
} from './settingsUtils'
@ -66,48 +65,3 @@ 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')
})
})

View File

@ -286,27 +286,6 @@ 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,

View File

@ -112,6 +112,9 @@ export async function executor(
makeDefaultPlanes: () => {
return new Promise((resolve) => resolve(defaultPlanes))
},
modifyGrid: (hidden: boolean) => {
return new Promise((resolve) => resolve())
},
})
return new Promise((resolve) => {

View File

@ -42,6 +42,8 @@ export const settingsMachine = setup({
setClientTheme: () => {},
'Execute AST': () => {},
toastSuccess: () => {},
setEngineEdges: () => {},
setEngineScaleGridVisibility: () => {},
setClientSideSceneUnits: () => {},
persistSettings: () => {},
resetSettings: assign(({ context, event }) => {
@ -170,7 +172,7 @@ export const settingsMachine = setup({
'set.modeling.highlightEdges': {
target: 'persisting settings',
actions: ['setSettingAtLevel', 'toastSuccess', 'Execute AST'],
actions: ['setSettingAtLevel', 'toastSuccess', 'setEngineEdges'],
},
'Reset settings': {
@ -199,7 +201,11 @@ export const settingsMachine = setup({
'set.modeling.showScaleGrid': {
target: 'persisting settings',
actions: ['setSettingAtLevel', 'toastSuccess', 'Execute AST'],
actions: [
'setSettingAtLevel',
'toastSuccess',
'setEngineScaleGridVisibility',
],
},
},
},

View File

@ -23,6 +23,15 @@ import argvFromYargs from './commandLineArgs'
let mainWindow: BrowserWindow | null = null
// Supporting multiple instances instead of multiple applications
let cmdQPressed = false
const instances: BrowserWindow[] = []
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
app.quit()
process.exit(0)
}
// Check the command line arguments for a project path
const args = parseCLIArgs()
@ -44,6 +53,11 @@ 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.
@ -112,16 +126,34 @@ const createWindow = (filePath?: string): BrowserWindow => {
newWindow.show()
instances.push(newWindow)
return newWindow
}
// before-quit with multiple instances
if (process.platform === 'darwin') {
// Quit from the dock context menu should quit the application directly
app.on('before-quit', () => {
cmdQPressed = true
})
}
// Quit when all windows are closed, even on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q, but it is a really weird behavior with our app.
// app.on('window-all-closed', () => {
// app.quit()
// })
app.on('window-all-closed', () => {
app.quit()
if (cmdQPressed || process.platform !== 'darwin') {
app.quit()
}
})
// Various actions can trigger this event, such as launching the application for the first time,
// attempting to re-launch the application when it's already running, or clicking on the application's dock or taskbar icon.
app.on('activate', () => createWindow())
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
@ -130,6 +162,10 @@ app.on('ready', (event, data) => {
mainWindow = createWindow()
})
// This event will be emitted inside the primary instance of your application when a second instance
// has been executed and calls app.requestSingleInstanceLock().
app.on('second-instance', (event, argv, workingDirectory) => createWindow())
// For now there is no good reason to separate these out to another file(s)
// There is just not enough code to warrant it and further abstracts everything
// which is already quite abstracted
@ -251,9 +287,6 @@ 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)

View File

@ -32,11 +32,9 @@ export const PACKAGE_NAME = isDesktop()
export const IS_NIGHTLY = PACKAGE_NAME.indexOf('-nightly') > -1
export function getReleaseUrl(version: string = APP_VERSION) {
return `https://github.com/KittyCAD/modeling-app/releases/tag/${
IS_NIGHTLY ? 'nightly-' : ''
}v${version}`
}
export const RELEASE_URL = `https://github.com/KittyCAD/modeling-app/releases/tag/${
IS_NIGHTLY ? 'nightly-' : ''
}v${APP_VERSION}`
export const Settings = () => {
const navigate = useNavigate()

View File

@ -1721,9 +1721,7 @@ dependencies = [
"parse-display 0.9.1",
"pretty_assertions",
"pyo3",
"regex",
"reqwest",
"rgba_simple",
"ropey",
"schemars",
"serde",
@ -2973,12 +2971,6 @@ 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"

View File

@ -40,12 +40,10 @@ 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",

View File

@ -13,8 +13,6 @@ 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)]
@ -234,11 +232,6 @@ 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() {
@ -458,16 +451,6 @@ 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);
@ -506,12 +489,6 @@ 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;
@ -969,47 +946,6 @@ 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() {

View File

@ -120,61 +120,6 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
Ok(())
}
/// Set the visibility of edges.
async fn set_edge_visibility(
&self,
visible: bool,
source_range: SourceRange,
) -> Result<(), crate::errors::KclError> {
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
source_range,
&ModelingCmd::from(mcmd::EdgeLinesVisible { hidden: !visible }),
)
.await?;
Ok(())
}
async fn set_units(
&self,
units: crate::UnitLength,
source_range: SourceRange,
) -> Result<(), crate::errors::KclError> {
// Before we even start executing the program, set the units.
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
source_range,
&ModelingCmd::from(mcmd::SetSceneUnits { unit: units.into() }),
)
.await?;
Ok(())
}
/// Re-run the command to apply the settings.
async fn reapply_settings(
&self,
settings: &crate::ExecutorSettings,
source_range: SourceRange,
) -> Result<(), crate::errors::KclError> {
// Set the edge visibility.
self.set_edge_visibility(settings.highlight_edges, source_range).await?;
// Change the units.
self.set_units(settings.units, source_range).await?;
// Send the command to show the grid.
self.modify_grid(!settings.show_grid, source_range).await?;
// We do not have commands for changing ssao on the fly.
// Flush the batch queue, so the settings are applied right away.
self.flush_batch(false, source_range).await?;
Ok(())
}
// Add a modeling command to the batch but don't fire it right away.
async fn batch_modeling_cmd(
&self,
@ -559,11 +504,11 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
}))
}
async fn modify_grid(&self, hidden: bool, source_range: SourceRange) -> Result<(), KclError> {
async fn modify_grid(&self, hidden: bool) -> Result<(), KclError> {
// Hide/show the grid.
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
source_range,
Default::default(),
&ModelingCmd::from(mcmd::ObjectVisible {
hidden,
object_id: *GRID_OBJECT_ID,
@ -574,7 +519,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
// Hide/show the grid scale text.
self.batch_modeling_cmd(
uuid::Uuid::new_v4(),
source_range,
Default::default(),
&ModelingCmd::from(mcmd::ObjectVisible {
hidden,
object_id: *GRID_SCALE_TEXT_OBJECT_ID,
@ -582,6 +527,8 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
)
.await?;
self.flush_batch(false, Default::default()).await?;
Ok(())
}

View File

@ -1,50 +0,0 @@
//! Functions for helping with caching an ast and finding the parts the changed.
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
execution::ExecState,
parsing::ast::types::{Node, Program},
};
/// Information for the caching an AST and smartly re-executing it if we can.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct CacheInformation {
/// The old information.
pub old: Option<OldAstState>,
/// The new ast to executed.
pub new_ast: Node<Program>,
}
/// The old ast and program memory.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct OldAstState {
/// The ast.
pub ast: Node<Program>,
/// The exec state.
pub exec_state: ExecState,
/// The last settings used for execution.
pub settings: crate::execution::ExecutorSettings,
}
impl From<crate::Program> for CacheInformation {
fn from(program: crate::Program) -> Self {
CacheInformation {
old: None,
new_ast: program.ast,
}
}
}
/// The result of a cache check.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct CacheResult {
/// Should we clear the scene and start over?
pub clear_scene: bool,
/// The program that needs to be executed.
pub program: Node<Program>,
}

View File

@ -326,12 +326,29 @@ async fn inner_execute_pipe_body(
ctx: &ExecutorContext,
) -> Result<KclValue, KclError> {
for expression in body {
if let Expr::TagDeclarator(_) = expression {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("This cannot be in a PipeExpression: {:?}", expression),
source_ranges: vec![expression.into()],
}));
}
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(_) => {}
};
let metadata = Metadata {
source_range: SourceRange::from(expression),
};

View File

@ -23,18 +23,15 @@ type Point3D = kcmc::shared::Point3d<f64>;
pub use function_param::FunctionParam;
pub use kcl_value::{KclObjectFields, KclValue};
pub(crate) mod cache;
mod exec_ast;
mod function_param;
mod kcl_value;
use crate::{
engine::{EngineManager, ExecutionKind},
errors::{KclError, KclErrorDetails},
execution::cache::{CacheInformation, CacheResult},
fs::{FileManager, FileSystem},
parsing::ast::types::{
BodyItem, Expr, FunctionExpression, ImportSelector, ItemVisibility, Node, NodeRef, TagDeclarator, TagNode,
parsing::ast::{
cache::{get_changed_program, CacheInformation},
types::{
BodyItem, Expr, FunctionExpression, ImportSelector, ItemVisibility, Node, NodeRef, TagDeclarator, TagNode,
},
},
settings::types::UnitLength,
source_range::{ModuleId, SourceRange},
@ -42,6 +39,10 @@ use crate::{
ExecError, Program,
};
mod exec_ast;
mod function_param;
mod kcl_value;
/// State for executing a program.
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
@ -1659,6 +1660,17 @@ impl ExecutorContext {
let engine: Arc<Box<dyn EngineManager>> =
Arc::new(Box::new(crate::engine::conn::EngineConnection::new(ws).await?));
// Set the edge visibility.
engine
.batch_modeling_cmd(
uuid::Uuid::new_v4(),
SourceRange::default(),
&ModelingCmd::from(mcmd::EdgeLinesVisible {
hidden: !settings.highlight_edges,
}),
)
.await?;
Ok(Self {
engine,
fs: Arc::new(FileManager::new()),
@ -1685,7 +1697,7 @@ impl ExecutorContext {
pub async fn new(
engine_manager: crate::engine::conn_wasm::EngineCommandManager,
fs_manager: crate::fs::wasm::FileSystemManager,
settings: ExecutorSettings,
units: UnitLength,
) -> Result<Self, String> {
Ok(ExecutorContext {
engine: Arc::new(Box::new(
@ -1695,16 +1707,16 @@ impl ExecutorContext {
)),
fs: Arc::new(FileManager::new(fs_manager)),
stdlib: Arc::new(StdLib::new()),
settings,
settings: ExecutorSettings {
units,
..Default::default()
},
context_type: ContextType::Live,
})
}
#[cfg(target_arch = "wasm32")]
pub async fn new_mock(
fs_manager: crate::fs::wasm::FileSystemManager,
settings: ExecutorSettings,
) -> Result<Self, String> {
pub async fn new_mock(fs_manager: crate::fs::wasm::FileSystemManager, units: UnitLength) -> Result<Self, String> {
Ok(ExecutorContext {
engine: Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
@ -1713,7 +1725,10 @@ impl ExecutorContext {
)),
fs: Arc::new(FileManager::new(fs_manager)),
stdlib: Arc::new(StdLib::new()),
settings,
settings: ExecutorSettings {
units,
..Default::default()
},
context_type: ContextType::Mock,
})
}
@ -1802,71 +1817,6 @@ impl ExecutorContext {
// AND if we aren't in wasm it doesn't really matter.
Ok(())
}
// Given an old ast, old program memory and new ast, find the parts of the code that need to be
// re-executed.
// This function should never error, because in the case of any internal error, we should just pop
// the cache.
pub async fn get_changed_program(&self, info: CacheInformation) -> Option<CacheResult> {
let Some(old) = info.old else {
// We have no old info, we need to re-execute the whole thing.
return Some(CacheResult {
clear_scene: true,
program: info.new_ast,
});
};
// If the settings are different we might need to bust the cache.
// We specifically do this before checking if they are the exact same.
if old.settings != self.settings {
// If the units are different we need to re-execute the whole thing.
if old.settings.units != self.settings.units {
return Some(CacheResult {
clear_scene: true,
program: info.new_ast,
});
}
// If anything else is different we do not need to re-execute, but rather just
// run the settings again.
if self
.engine
.reapply_settings(&self.settings, Default::default())
.await
.is_err()
{
// Bust the cache, we errored.
return Some(CacheResult {
clear_scene: true,
program: info.new_ast,
});
}
}
// If the ASTs are the EXACT same we return None.
// We don't even need to waste time computing the digests.
if old.ast == info.new_ast {
return None;
}
let mut old_ast = old.ast.inner;
old_ast.compute_digest();
let mut new_ast = info.new_ast.inner.clone();
new_ast.compute_digest();
// Check if the digest is the same.
if old_ast.digest == new_ast.digest {
return None;
}
// Check if the changes were only to Non-code areas, like comments or whitespace.
// For any unhandled cases just re-execute the whole thing.
Some(CacheResult {
clear_scene: true,
program: info.new_ast,
})
}
/// Perform the execution of a program.
/// You can optionally pass in some initialization memory.
@ -1887,7 +1837,7 @@ impl ExecutorContext {
let _stats = crate::log::LogPerfStats::new("Interpretation");
// Get the program that actually changed from the old and new information.
let cache_result = self.get_changed_program(cache_info.clone()).await;
let cache_result = get_changed_program(cache_info.clone(), &self.settings);
// Check if we don't need to re-execute.
let Some(cache_result) = cache_result else {
@ -1904,9 +1854,23 @@ impl ExecutorContext {
// TODO: Use the top-level file's path.
exec_state.add_module(std::path::PathBuf::from(""));
// Re-apply the settings, in case the cache was busted.
self.engine.reapply_settings(&self.settings, Default::default()).await?;
// Before we even start executing the program, set the units.
self.engine
.batch_modeling_cmd(
exec_state.id_generator.next_uuid(),
SourceRange::default(),
&ModelingCmd::from(mcmd::SetSceneUnits {
unit: match self.settings.units {
UnitLength::Cm => kcmc::units::UnitLength::Centimeters,
UnitLength::Ft => kcmc::units::UnitLength::Feet,
UnitLength::In => kcmc::units::UnitLength::Inches,
UnitLength::M => kcmc::units::UnitLength::Meters,
UnitLength::Mm => kcmc::units::UnitLength::Millimeters,
UnitLength::Yd => kcmc::units::UnitLength::Yards,
},
}),
)
.await?;
self.inner_execute(&cache_result.program, exec_state, crate::execution::BodyType::Root)
.await?;
@ -2117,8 +2081,7 @@ impl ExecutorContext {
Ok((module_memory, module_exports))
}
#[async_recursion]
pub async fn execute_expr<'a: 'async_recursion>(
pub async fn execute_expr<'a>(
&self,
init: &Expr,
exec_state: &mut ExecState,
@ -2175,14 +2138,6 @@ 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)
}
@ -2192,8 +2147,23 @@ impl ExecutorContext {
self.settings.units = units;
}
/// Get a snapshot of the current scene.
pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
/// Execute the program, then get a PNG screenshot.
pub async fn execute_and_prepare_snapshot(
&self,
program: &Program,
exec_state: &mut ExecState,
) -> std::result::Result<TakeSnapshot, ExecError> {
self.execute_and_prepare(program, exec_state).await
}
/// Execute the program, return the interpreter and outputs.
pub async fn execute_and_prepare(
&self,
program: &Program,
exec_state: &mut ExecState,
) -> std::result::Result<TakeSnapshot, ExecError> {
self.run(program.clone().into(), exec_state).await?;
// Zoom to fit.
self.engine
.send_modeling_cmd(
@ -2229,17 +2199,6 @@ impl ExecutorContext {
};
Ok(contents)
}
/// Execute the program, then get a PNG screenshot.
pub async fn execute_and_prepare_snapshot(
&self,
program: &Program,
exec_state: &mut ExecState,
) -> std::result::Result<TakeSnapshot, ExecError> {
self.run(program.clone().into(), exec_state).await?;
self.prepare_snapshot().await
}
}
/// For each argument given,
@ -2419,12 +2378,9 @@ mod tests {
use pretty_assertions::assert_eq;
use super::*;
use crate::{
parsing::ast::types::{DefaultParamVal, Identifier, Node, Parameter},
OldAstState,
};
use crate::parsing::ast::types::{DefaultParamVal, Identifier, Node, Parameter};
pub async fn parse_execute(code: &str) -> Result<(Program, ExecutorContext, ExecState)> {
pub async fn parse_execute(code: &str) -> Result<ProgramMemory> {
let program = Program::parse_no_errs(code)?;
let ctx = ExecutorContext {
@ -2435,9 +2391,9 @@ mod tests {
context_type: ContextType::Mock,
};
let mut exec_state = ExecState::default();
ctx.run(program.clone().into(), &mut exec_state).await?;
ctx.run(program.into(), &mut exec_state).await?;
Ok((program, ctx, exec_state))
Ok(exec_state.memory)
}
/// Convenience function to get a JSON value from memory and unwrap.
@ -2848,39 +2804,36 @@ let shape = layer() |> patternTransform(10, transform, %)
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_with_functions() {
let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#;
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
assert_eq!(5.0, mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap());
let memory = parse_execute(ast).await.unwrap();
assert_eq!(5.0, mem_get_json(&memory, "myVar").as_f64().unwrap());
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute() {
let ast = r#"const myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
assert_eq!(7.4, mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap());
let memory = parse_execute(ast).await.unwrap();
assert_eq!(7.4, mem_get_json(&memory, "myVar").as_f64().unwrap());
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_start_negative() {
let ast = r#"const myVar = -5 + 6"#;
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
assert_eq!(1.0, mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap());
let memory = parse_execute(ast).await.unwrap();
assert_eq!(1.0, mem_get_json(&memory, "myVar").as_f64().unwrap());
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_with_pi() {
let ast = r#"const myVar = pi() * 2"#;
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
assert_eq!(
std::f64::consts::TAU,
mem_get_json(&exec_state.memory, "myVar").as_f64().unwrap()
);
let memory = parse_execute(ast).await.unwrap();
assert_eq!(std::f64::consts::TAU, mem_get_json(&memory, "myVar").as_f64().unwrap());
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_define_decimal_without_leading_zero() {
let ast = r#"let thing = .4 + 7"#;
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
assert_eq!(7.4, mem_get_json(&exec_state.memory, "thing").as_f64().unwrap());
let memory = parse_execute(ast).await.unwrap();
assert_eq!(7.4, mem_get_json(&memory, "thing").as_f64().unwrap());
}
#[tokio::test(flavor = "multi_thread")]
@ -2919,11 +2872,11 @@ fn check = (x) => {
}
check(false)
"#;
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
assert_eq!(false, mem_get_json(&exec_state.memory, "notTrue").as_bool().unwrap());
assert_eq!(true, mem_get_json(&exec_state.memory, "notFalse").as_bool().unwrap());
assert_eq!(true, mem_get_json(&exec_state.memory, "c").as_bool().unwrap());
assert_eq!(false, mem_get_json(&exec_state.memory, "d").as_bool().unwrap());
let mem = parse_execute(ast).await.unwrap();
assert_eq!(false, mem_get_json(&mem, "notTrue").as_bool().unwrap());
assert_eq!(true, mem_get_json(&mem, "notFalse").as_bool().unwrap());
assert_eq!(true, mem_get_json(&mem, "c").as_bool().unwrap());
assert_eq!(false, mem_get_json(&mem, "d").as_bool().unwrap());
}
#[tokio::test(flavor = "multi_thread")]
@ -3305,310 +3258,4 @@ let w = f() + f()
let json = serde_json::to_string(&mem).unwrap();
assert_eq!(json, r#"{"type":"Solids","value":[]}"#);
}
// Easy case where we have no old ast and memory.
// We need to re-execute everything.
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_no_old_information() {
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let (program, ctx, _) = parse_execute(new).await.unwrap();
let result = ctx
.get_changed_program(CacheInformation {
old: None,
new_ast: program.ast.clone(),
})
.await;
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.program, program.ast);
assert!(result.clear_scene);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code() {
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let (program, ctx, exec_state) = parse_execute(new).await.unwrap();
let result = ctx
.get_changed_program(CacheInformation {
old: Some(OldAstState {
ast: program.ast.clone(),
exec_state,
settings: Default::default(),
}),
new_ast: program.ast.clone(),
})
.await;
assert_eq!(result, None);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_changed_whitespace() {
let old = r#" // Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let (program_old, ctx, exec_state) = parse_execute(old).await.unwrap();
let program_new = crate::Program::parse_no_errs(new).unwrap();
let result = ctx
.get_changed_program(CacheInformation {
old: Some(OldAstState {
ast: program_old.ast.clone(),
exec_state,
settings: Default::default(),
}),
new_ast: program_new.ast.clone(),
})
.await;
assert_eq!(result, None);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
let old = r#" // Removed the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let (program, ctx, exec_state) = parse_execute(old).await.unwrap();
let program_new = crate::Program::parse_no_errs(new).unwrap();
let result = ctx
.get_changed_program(CacheInformation {
old: Some(OldAstState {
ast: program.ast.clone(),
exec_state,
settings: Default::default(),
}),
new_ast: program_new.ast.clone(),
})
.await;
assert_eq!(result, None);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_changed_code_comments() {
let old = r#" // Removed the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %) // my thing
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let (program, ctx, exec_state) = parse_execute(old).await.unwrap();
let program_new = crate::Program::parse_no_errs(new).unwrap();
let result = ctx
.get_changed_program(CacheInformation {
old: Some(OldAstState {
ast: program.ast.clone(),
exec_state,
settings: Default::default(),
}),
new_ast: program_new.ast.clone(),
})
.await;
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.program, program_new.ast);
assert!(result.clear_scene);
}
// Changing the units with the exact same file should bust the cache.
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_but_different_units() {
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let (program, mut ctx, exec_state) = parse_execute(new).await.unwrap();
// Change the settings to cm.
ctx.settings.units = crate::UnitLength::Cm;
let result = ctx
.get_changed_program(CacheInformation {
old: Some(OldAstState {
ast: program.ast.clone(),
exec_state,
settings: Default::default(),
}),
new_ast: program.ast.clone(),
})
.await;
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.program, program.ast);
assert!(result.clear_scene);
}
// Changing the grid settings with the exact same file should NOT bust the cache.
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_but_different_grid_setting() {
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let (program, mut ctx, exec_state) = parse_execute(new).await.unwrap();
// Change the settings.
ctx.settings.show_grid = !ctx.settings.show_grid;
let result = ctx
.get_changed_program(CacheInformation {
old: Some(OldAstState {
ast: program.ast.clone(),
exec_state,
settings: Default::default(),
}),
new_ast: program.ast.clone(),
})
.await;
assert_eq!(result, None);
}
// Changing the edge visibility settings with the exact same file should NOT bust the cache.
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_but_different_edge_visiblity_setting() {
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let (program, mut ctx, exec_state) = parse_execute(new).await.unwrap();
// Change the settings.
ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
let result = ctx
.get_changed_program(CacheInformation {
old: Some(OldAstState {
ast: program.ast.clone(),
exec_state,
settings: Default::default(),
}),
new_ast: program.ast.clone(),
})
.await;
assert_eq!(result, None);
}
}

View File

@ -82,15 +82,16 @@ mod wasm;
pub use coredump::CoreDump;
pub use engine::{EngineManager, ExecutionKind};
pub use errors::{CompilationError, ConnectionError, ExecError, KclError};
pub use execution::{
cache::{CacheInformation, OldAstState},
ExecState, ExecutorContext, ExecutorSettings,
};
pub use execution::{ExecState, ExecutorContext, ExecutorSettings};
pub use lsp::{
copilot::Backend as CopilotLspBackend,
kcl::{Backend as KclLspBackend, Server as KclLspServerSubCommand},
};
pub use parsing::ast::{modify::modify_ast_for_sketch, types::FormatOptions};
pub use parsing::ast::{
cache::{CacheInformation, OldAstState},
modify::modify_ast_for_sketch,
types::FormatOptions,
};
pub use settings::types::{project::ProjectConfiguration, Configuration, UnitLength};
pub use source_range::{ModuleId, SourceRange};

View File

@ -45,11 +45,14 @@ use crate::{
errors::Suggestion,
lsp::{backend::Backend as _, util::IntoDiagnostic},
parsing::{
ast::types::{Expr, Node, VariableKind},
ast::{
cache::{CacheInformation, OldAstState},
types::{Expr, Node, VariableKind},
},
token::TokenStream,
PIPE_OPERATOR,
},
CacheInformation, ModuleId, OldAstState, Program, SourceRange,
ModuleId, Program, SourceRange,
};
const SEMANTIC_TOKEN_TYPES: [SemanticTokenType; 10] = [
SemanticTokenType::NUMBER,

View File

@ -0,0 +1,373 @@
//! Functions for helping with caching an ast and finding the parts the changed.
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
execution::ExecState,
parsing::ast::types::{Node, Program},
};
/// Information for the caching an AST and smartly re-executing it if we can.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct CacheInformation {
/// The old information.
pub old: Option<OldAstState>,
/// The new ast to executed.
pub new_ast: Node<Program>,
}
/// The old ast and program memory.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct OldAstState {
/// The ast.
pub ast: Node<Program>,
/// The exec state.
pub exec_state: ExecState,
/// The last settings used for execution.
pub settings: crate::execution::ExecutorSettings,
}
impl From<crate::Program> for CacheInformation {
fn from(program: crate::Program) -> Self {
CacheInformation {
old: None,
new_ast: program.ast,
}
}
}
/// The result of a cache check.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct CacheResult {
/// Should we clear the scene and start over?
pub clear_scene: bool,
/// The program that needs to be executed.
pub program: Node<Program>,
}
// Given an old ast, old program memory and new ast, find the parts of the code that need to be
// re-executed.
// This function should never error, because in the case of any internal error, we should just pop
// the cache.
pub fn get_changed_program(
info: CacheInformation,
new_settings: &crate::execution::ExecutorSettings,
) -> Option<CacheResult> {
let Some(old) = info.old else {
// We have no old info, we need to re-execute the whole thing.
return Some(CacheResult {
clear_scene: true,
program: info.new_ast,
});
};
// If the settings are different we need to bust the cache.
// We specifically do this before checking if they are the exact same.
if old.settings != *new_settings {
return Some(CacheResult {
clear_scene: true,
program: info.new_ast,
});
}
// If the ASTs are the EXACT same we return None.
// We don't even need to waste time computing the digests.
if old.ast == info.new_ast {
return None;
}
let mut old_ast = old.ast.inner;
old_ast.compute_digest();
let mut new_ast = info.new_ast.inner.clone();
new_ast.compute_digest();
// Check if the digest is the same.
if old_ast.digest == new_ast.digest {
return None;
}
// Check if the changes were only to Non-code areas, like comments or whitespace.
// For any unhandled cases just re-execute the whole thing.
Some(CacheResult {
clear_scene: true,
program: info.new_ast,
})
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use anyhow::Result;
use pretty_assertions::assert_eq;
use super::*;
async fn execute(program: &crate::Program) -> Result<ExecState> {
let ctx = crate::execution::ExecutorContext {
engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await?)),
fs: Arc::new(crate::fs::FileManager::new()),
stdlib: Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
context_type: crate::execution::ContextType::Mock,
};
let mut exec_state = crate::execution::ExecState::default();
ctx.run(program.clone().into(), &mut exec_state).await?;
Ok(exec_state)
}
// Easy case where we have no old ast and memory.
// We need to re-execute everything.
#[test]
fn test_get_changed_program_no_old_information() {
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let program = crate::Program::parse_no_errs(new).unwrap().ast;
let result = get_changed_program(
CacheInformation {
old: None,
new_ast: program.clone(),
},
&Default::default(),
);
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.program, program);
assert!(result.clear_scene);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code() {
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let program = crate::Program::parse_no_errs(new).unwrap();
let executed = execute(&program).await.unwrap();
let result = get_changed_program(
CacheInformation {
old: Some(OldAstState {
ast: program.ast.clone(),
exec_state: executed,
settings: Default::default(),
}),
new_ast: program.ast.clone(),
},
&Default::default(),
);
assert_eq!(result, None);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_changed_whitespace() {
let old = r#" // Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let program_old = crate::Program::parse_no_errs(old).unwrap();
let executed = execute(&program_old).await.unwrap();
let program_new = crate::Program::parse_no_errs(new).unwrap();
let result = get_changed_program(
CacheInformation {
old: Some(OldAstState {
ast: program_old.ast.clone(),
exec_state: executed,
settings: Default::default(),
}),
new_ast: program_new.ast.clone(),
},
&Default::default(),
);
assert_eq!(result, None);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
let old = r#" // Removed the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let program_old = crate::Program::parse_no_errs(old).unwrap();
let executed = execute(&program_old).await.unwrap();
let program_new = crate::Program::parse_no_errs(new).unwrap();
let result = get_changed_program(
CacheInformation {
old: Some(OldAstState {
ast: program_old.ast.clone(),
exec_state: executed,
settings: Default::default(),
}),
new_ast: program_new.ast.clone(),
},
&Default::default(),
);
assert_eq!(result, None);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_changed_code_comments() {
let old = r#" // Removed the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %) // my thing
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let program_old = crate::Program::parse_no_errs(old).unwrap();
let executed = execute(&program_old).await.unwrap();
let program_new = crate::Program::parse_no_errs(new).unwrap();
let result = get_changed_program(
CacheInformation {
old: Some(OldAstState {
ast: program_old.ast.clone(),
exec_state: executed,
settings: Default::default(),
}),
new_ast: program_new.ast.clone(),
},
&Default::default(),
);
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.program, program_new.ast);
assert!(result.clear_scene);
}
// Changing the units with the exact same file should bust the cache.
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_but_different_units() {
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line([24, 0], %)
|> line([0, -24], %)
|> line([-24, 0], %)
|> close(%)
|> extrude(6, %)
// Remove the end face for the extrusion.
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
let program = crate::Program::parse_no_errs(new).unwrap();
let executed = execute(&program).await.unwrap();
let result = get_changed_program(
CacheInformation {
old: Some(OldAstState {
ast: program.ast.clone(),
exec_state: executed,
settings: Default::default(),
}),
new_ast: program.ast.clone(),
},
&crate::ExecutorSettings {
units: crate::UnitLength::Cm,
..Default::default()
},
);
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.program, program.ast);
assert!(result.clear_scene);
}
}

View File

@ -1,10 +1,10 @@
use sha2::{Digest as DigestTrait, Sha256};
use super::types::{DefaultParamVal, ItemVisibility, LabelledExpression, VariableKind};
use super::types::{DefaultParamVal, ItemVisibility, VariableKind};
use crate::parsing::ast::types::{
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, CallExpressionKw,
CommentStyle, ElseIf, Expr, ExpressionStatement, FnArgType, FunctionExpression, Identifier, IfExpression,
ImportItem, ImportSelector, ImportStatement, KclNone, Literal, LiteralIdentifier, MemberExpression, MemberObject,
ImportItem, ImportSelector, ImportStatement, Literal, LiteralIdentifier, MemberExpression, MemberObject,
NonCodeMeta, NonCodeNode, NonCodeValue, ObjectExpression, ObjectProperty, Parameter, PipeExpression,
PipeSubstitution, Program, ReturnStatement, TagDeclarator, UnaryExpression, VariableDeclaration,
VariableDeclarator,
@ -115,7 +115,6 @@ 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");
@ -203,12 +202,6 @@ 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());
@ -403,13 +396,6 @@ 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());

View File

@ -1,3 +1,4 @@
pub(crate) mod cache;
pub(crate) mod digest;
pub mod modify;
pub mod types;
@ -36,7 +37,6 @@ 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,
}
}

View File

@ -598,7 +598,6 @@ pub enum Expr {
MemberExpression(BoxNode<MemberExpression>),
UnaryExpression(BoxNode<UnaryExpression>),
IfExpression(BoxNode<IfExpression>),
LabelledExpression(BoxNode<LabelledExpression>),
None(Node<KclNone>),
}
@ -641,7 +640,6 @@ 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,
}
}
@ -668,7 +666,6 @@ 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(_) => {}
}
}
@ -690,7 +687,6 @@ 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,
}
}
@ -712,7 +708,6 @@ 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,
}
}
@ -739,8 +734,6 @@ 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,
}
@ -770,7 +763,6 @@ 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(_) => {}
}
}
@ -796,19 +788,9 @@ 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 {
@ -823,36 +805,6 @@ 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")]

View File

@ -3,7 +3,7 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::{super::digest::Digest, Node};
use super::Node;
use crate::{execution::KclValue, parsing::ast::types::ConstraintLevel};
const KCL_NONE_ID: &str = "KCL_NONE_ID";
@ -19,18 +19,11 @@ 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 {},
digest: None,
}
Self { __private: Private {} }
}
}

View File

@ -33,8 +33,6 @@ 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) };
@ -339,7 +337,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), labelled_fn_call), // The expression.
preceded(opt(whitespace), fn_call), // The expression.
repeat(0.., noncode_just_after_code), // After the expression.
);
let tail: Vec<(Vec<_>, _, Vec<_>)> = repeat(
@ -355,7 +353,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.into())
Some(call_expr.as_source_range())
} else {
None
}
@ -375,7 +373,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(code);
values.push(Expr::CallExpression(Box::new(code)));
code_count += 1;
for nc in noncode_after {
max_noncode_end = nc.end.max(max_noncode_end);
@ -529,8 +527,7 @@ fn operand(i: &mut TokenSlice) -> PResult<BinaryPart> {
| Expr::PipeSubstitution(_)
| Expr::ArrayExpression(_)
| Expr::ArrayRangeExpression(_)
| Expr::ObjectExpression(_)
| Expr::LabelledExpression(..) => return Err(CompilationError::fatal(source_range, TODO_783)),
| Expr::ObjectExpression(_) => return Err(CompilationError::fatal(source_range, TODO_783)),
Expr::None(_) => {
return Err(CompilationError::fatal(
source_range,
@ -1631,34 +1628,13 @@ fn expression(i: &mut TokenSlice) -> PResult<Expr> {
}
fn expression_but_not_pipe(i: &mut TokenSlice) -> PResult<Expr> {
let expr = alt((
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)?;
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)
.parse_next(i)
}
fn unnecessarily_bracketed(i: &mut TokenSlice) -> PResult<Expr> {
@ -2474,17 +2450,6 @@ 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)?;

View File

@ -5,7 +5,6 @@ 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,
@ -18,6 +17,7 @@ use crate::{
parsing::ast::types::{ItemVisibility, VariableKind},
source_range::{ModuleId, SourceRange},
};
use tokeniser::Input;
mod tokeniser;

View File

@ -10,12 +10,13 @@ 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();
@ -364,8 +365,9 @@ fn keyword_type_or_word(i: &mut Input<'_>) -> PResult<Token> {
mod tests {
use winnow::Located;
use super::*;
use crate::parsing::token::TokenSlice;
use super::*;
fn assert_parse_err<'i, P, O, E>(mut p: P, s: &'i str)
where
O: std::fmt::Debug,

View File

@ -380,9 +380,9 @@ impl From<UnitLength> for kittycad_modeling_cmds::units::UnitLength {
#[display(style = "snake_case")]
pub enum MouseControlType {
#[default]
#[display("zoo")]
#[serde(rename = "zoo", alias = "Zoo", alias = "KittyCAD")]
Zoo,
#[display("kittycad")]
#[serde(rename = "kittycad", alias = "KittyCAD")]
KittyCad,
#[display("onshape")]
#[serde(rename = "onshape", alias = "OnShape")]
OnShape,

View File

@ -1,303 +0,0 @@
//! 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))
}

View File

@ -1096,34 +1096,6 @@ 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()?;

View File

@ -1,6 +1,5 @@
//! Functions implemented for language execution.
pub mod appearance;
pub mod args;
pub mod array;
pub mod assert;
@ -21,7 +20,6 @@ pub mod segment;
pub mod shapes;
pub mod shell;
pub mod sketch;
pub mod sweep;
pub mod types;
pub mod units;
pub mod utils;
@ -52,7 +50,6 @@ 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),
@ -115,7 +112,6 @@ 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),

View File

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

View File

@ -55,11 +55,7 @@ async fn do_execute_and_snapshot(
program: Program,
) -> Result<(crate::execution::ExecState, image::DynamicImage), ExecError> {
let mut exec_state = Default::default();
let snapshot_png_bytes = ctx
.execute_and_prepare_snapshot(&program, &mut exec_state)
.await?
.contents
.0;
let snapshot_png_bytes = ctx.execute_and_prepare(&program, &mut exec_state).await?.contents.0;
// Decode the snapshot, return it.
let img = image::ImageReader::new(std::io::Cursor::new(snapshot_png_bytes))

View File

@ -194,12 +194,6 @@ 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")
}
@ -413,8 +407,7 @@ fn expr_is_trivial(expr: &Expr) -> bool {
| Expr::ObjectExpression(_)
| Expr::MemberExpression(_)
| Expr::UnaryExpression(_)
| Expr::IfExpression(_)
| Expr::LabelledExpression(_) => false,
| Expr::IfExpression(_) => false,
}
}
@ -1526,28 +1519,6 @@ 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')

View File

@ -32,50 +32,15 @@ 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>),
KclNone(&'a types::KclNone),
}
MemberObject(&'a types::MemberObject),
LiteralIdentifier(&'a types::LiteralIdentifier),
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,
}
}
KclNone(&'a types::KclNone),
}
/// Returned during source_range conversion.
@ -112,8 +77,9 @@ 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::LabelledExpression(n) => SourceRange::from(*n),
Node::LiteralIdentifier(l) => SourceRange::new(l.start(), l.end(), l.module_id()),
// This is broken too
Node::ElseIf(n) => SourceRange::new(n.cond.start(), n.cond.end(), n.cond.module_id()),
@ -154,7 +120,6 @@ 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(),
}
}
@ -175,24 +140,6 @@ 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> {
@ -235,7 +182,8 @@ 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!(Node, LabelledExpression);
impl_from_ref!(Node, LiteralIdentifier);
impl_from!(Node, KclNone);

View File

@ -127,13 +127,12 @@ 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![],
}
@ -142,9 +141,8 @@ impl<'tree> Visitable<'tree> for Node<'tree> {
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use super::*;
use std::sync::Mutex;
macro_rules! kcl {
( $kcl:expr ) => {{

View File

@ -3,5 +3,4 @@ mod ast_visitor;
mod ast_walk;
pub use ast_node::Node;
pub use ast_visitor::{Visitable, Visitor};
pub use ast_walk::walk;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

View File

@ -296,7 +296,7 @@ description: Result of parsing sketch_on_face_start.kcl
},
{
"declaration": {
"end": 243,
"end": 236,
"id": {
"end": 183,
"name": "part001",
@ -372,47 +372,35 @@ description: Result of parsing sketch_on_face_start.kcl
"type": "CallExpression"
},
{
"end": 243,
"expr": {
"arguments": [
{
"end": 232,
"raw": "20",
"start": 230,
"type": "Literal",
"type": "Literal",
"value": 20.0
},
{
"end": 235,
"start": 234,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 229,
"name": "extrude",
"start": 222,
"type": "Identifier"
"arguments": [
{
"end": 232,
"raw": "20",
"start": 230,
"type": "Literal",
"type": "Literal",
"value": 20.0
},
"end": 236,
{
"end": 235,
"start": 234,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 229,
"name": "extrude",
"start": 222,
"type": "CallExpression",
"type": "CallExpression"
},
"label": {
"end": 243,
"name": "foo",
"start": 240,
"type": "Identifier"
},
"end": 236,
"start": 222,
"type": "LabelledExpression",
"type": "LabelledExpression"
"type": "CallExpression",
"type": "CallExpression"
}
],
"end": 243,
"end": 236,
"start": 186,
"type": "PipeExpression",
"type": "PipeExpression"
@ -420,7 +408,7 @@ description: Result of parsing sketch_on_face_start.kcl
"start": 176,
"type": "VariableDeclarator"
},
"end": 243,
"end": 236,
"kind": "const",
"start": 176,
"type": "VariableDeclaration",
@ -428,11 +416,11 @@ description: Result of parsing sketch_on_face_start.kcl
},
{
"declaration": {
"end": 413,
"end": 410,
"id": {
"end": 252,
"end": 245,
"name": "part002",
"start": 245,
"start": 238,
"type": "Identifier"
},
"init": {
@ -440,29 +428,29 @@ description: Result of parsing sketch_on_face_start.kcl
{
"arguments": [
{
"end": 272,
"name": "foo",
"start": 269,
"end": 269,
"name": "part001",
"start": 262,
"type": "Identifier",
"type": "Identifier"
},
{
"end": 281,
"end": 278,
"raw": "\"start\"",
"start": 274,
"start": 271,
"type": "Literal",
"type": "Literal",
"value": "start"
}
],
"callee": {
"end": 268,
"end": 261,
"name": "startSketchOn",
"start": 255,
"start": 248,
"type": "Identifier"
},
"end": 282,
"start": 255,
"end": 279,
"start": 248,
"type": "CallExpression",
"type": "CallExpression"
},
@ -470,6 +458,14 @@ description: Result of parsing sketch_on_face_start.kcl
"arguments": [
{
"elements": [
{
"end": 302,
"raw": "0",
"start": 301,
"type": "Literal",
"type": "Literal",
"value": 0.0
},
{
"end": 305,
"raw": "0",
@ -477,36 +473,28 @@ description: Result of parsing sketch_on_face_start.kcl
"type": "Literal",
"type": "Literal",
"value": 0.0
},
{
"end": 308,
"raw": "0",
"start": 307,
"type": "Literal",
"type": "Literal",
"value": 0.0
}
],
"end": 309,
"start": 303,
"end": 306,
"start": 300,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
{
"end": 312,
"start": 311,
"end": 309,
"start": 308,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 302,
"end": 299,
"name": "startProfileAt",
"start": 288,
"start": 285,
"type": "Identifier"
},
"end": 313,
"start": 288,
"end": 310,
"start": 285,
"type": "CallExpression",
"type": "CallExpression"
},
@ -515,42 +503,42 @@ description: Result of parsing sketch_on_face_start.kcl
{
"elements": [
{
"end": 326,
"end": 323,
"raw": "0",
"start": 322,
"type": "Literal",
"type": "Literal",
"value": 0.0
},
{
"end": 327,
"raw": "10",
"start": 325,
"type": "Literal",
"type": "Literal",
"value": 0.0
},
{
"end": 330,
"raw": "10",
"start": 328,
"type": "Literal",
"type": "Literal",
"value": 10.0
}
],
"end": 331,
"start": 324,
"end": 328,
"start": 321,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
{
"end": 334,
"start": 333,
"end": 331,
"start": 330,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 323,
"end": 320,
"name": "line",
"start": 319,
"start": 316,
"type": "Identifier"
},
"end": 335,
"start": 319,
"end": 332,
"start": 316,
"type": "CallExpression",
"type": "CallExpression"
},
@ -558,43 +546,43 @@ description: Result of parsing sketch_on_face_start.kcl
"arguments": [
{
"elements": [
{
"end": 346,
"raw": "10",
"start": 344,
"type": "Literal",
"type": "Literal",
"value": 10.0
},
{
"end": 349,
"raw": "10",
"start": 347,
"type": "Literal",
"type": "Literal",
"value": 10.0
},
{
"end": 352,
"raw": "0",
"start": 351,
"start": 348,
"type": "Literal",
"type": "Literal",
"value": 0.0
}
],
"end": 353,
"start": 346,
"end": 350,
"start": 343,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
{
"end": 356,
"start": 355,
"end": 353,
"start": 352,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 345,
"end": 342,
"name": "line",
"start": 341,
"start": 338,
"type": "Identifier"
},
"end": 357,
"start": 341,
"end": 354,
"start": 338,
"type": "CallExpression",
"type": "CallExpression"
},
@ -603,123 +591,123 @@ description: Result of parsing sketch_on_face_start.kcl
{
"elements": [
{
"end": 370,
"end": 367,
"raw": "0",
"start": 369,
"start": 366,
"type": "Literal",
"type": "Literal",
"value": 0.0
},
{
"argument": {
"end": 375,
"end": 372,
"raw": "10",
"start": 373,
"start": 370,
"type": "Literal",
"type": "Literal",
"value": 10.0
},
"end": 375,
"end": 372,
"operator": "-",
"start": 372,
"start": 369,
"type": "UnaryExpression",
"type": "UnaryExpression"
}
],
"end": 376,
"start": 368,
"end": 373,
"start": 365,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
{
"end": 379,
"start": 378,
"end": 376,
"start": 375,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 367,
"end": 364,
"name": "line",
"start": 363,
"start": 360,
"type": "Identifier"
},
"end": 380,
"start": 363,
"end": 377,
"start": 360,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"end": 393,
"start": 392,
"end": 390,
"start": 389,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 391,
"end": 388,
"name": "close",
"start": 386,
"start": 383,
"type": "Identifier"
},
"end": 394,
"start": 386,
"end": 391,
"start": 383,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"end": 409,
"end": 406,
"raw": "5",
"start": 408,
"start": 405,
"type": "Literal",
"type": "Literal",
"value": 5.0
},
{
"end": 412,
"start": 411,
"end": 409,
"start": 408,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 407,
"end": 404,
"name": "extrude",
"start": 400,
"start": 397,
"type": "Identifier"
},
"end": 413,
"start": 400,
"end": 410,
"start": 397,
"type": "CallExpression",
"type": "CallExpression"
}
],
"end": 413,
"start": 255,
"end": 410,
"start": 248,
"type": "PipeExpression",
"type": "PipeExpression"
},
"start": 245,
"start": 238,
"type": "VariableDeclarator"
},
"end": 413,
"end": 410,
"kind": "const",
"start": 245,
"start": 238,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 414,
"end": 411,
"nonCodeMeta": {
"nonCodeNodes": {
"1": [
{
"end": 245,
"start": 243,
"end": 238,
"start": 236,
"type": "NonCodeNode",
"value": {
"type": "newLine"

View File

@ -9,9 +9,9 @@ fn cube(pos, scale) {
}
part001 = cube([0, 0], 20)
|> close(%)
|> extrude(20, %) as foo
|> extrude(20, %)
part002 = startSketchOn(foo, "start")
part002 = startSketchOn(part001, "start")
|> startProfileAt([0, 0], %)
|> line([0, 10], %)
|> line([10, 0], %)

View File

@ -339,209 +339,6 @@ description: Program memory after executing sketch_on_face_start.kcl
}
]
},
"foo": {
"type": "Solid",
"type": "Solid",
"id": "[uuid]",
"value": [
{
"faceId": "[uuid]",
"id": "[uuid]",
"sourceRange": [
86,
105,
0
],
"tag": null,
"type": "extrudePlane"
},
{
"faceId": "[uuid]",
"id": "[uuid]",
"sourceRange": [
113,
132,
0
],
"tag": null,
"type": "extrudePlane"
},
{
"faceId": "[uuid]",
"id": "[uuid]",
"sourceRange": [
140,
160,
0
],
"tag": null,
"type": "extrudePlane"
},
{
"faceId": "[uuid]",
"id": "[uuid]",
"sourceRange": [
208,
216,
0
],
"tag": null,
"type": "extrudePlane"
}
],
"sketch": {
"type": "Sketch",
"id": "[uuid]",
"paths": [
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
86,
105,
0
]
},
"from": [
0.0,
0.0
],
"tag": null,
"to": [
0.0,
20.0
],
"type": "ToPoint"
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
113,
132,
0
]
},
"from": [
0.0,
20.0
],
"tag": null,
"to": [
20.0,
20.0
],
"type": "ToPoint"
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
140,
160,
0
]
},
"from": [
20.0,
20.0
],
"tag": null,
"to": [
20.0,
0.0
],
"type": "ToPoint"
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
208,
216,
0
]
},
"from": [
20.0,
0.0
],
"tag": null,
"to": [
0.0,
0.0
],
"type": "ToPoint"
}
],
"on": {
"type": "plane",
"id": "[uuid]",
"value": "XY",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"xAxis": {
"x": 1.0,
"y": 0.0,
"z": 0.0
},
"yAxis": {
"x": 0.0,
"y": 1.0,
"z": 0.0
},
"zAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0
},
"__meta": []
},
"start": {
"from": [
0.0,
0.0
],
"to": [
0.0,
0.0
],
"tag": null,
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
56,
78,
0
]
}
},
"__meta": [
{
"sourceRange": [
56,
78,
0
]
}
]
},
"height": 20.0,
"startCapId": "[uuid]",
"endCapId": "[uuid]",
"__meta": [
{
"sourceRange": [
56,
78,
0
]
}
]
},
"part001": {
"type": "Solid",
"type": "Solid",
@ -754,8 +551,8 @@ description: Program memory after executing sketch_on_face_start.kcl
"faceId": "[uuid]",
"id": "[uuid]",
"sourceRange": [
319,
335,
316,
332,
0
],
"tag": null,
@ -765,8 +562,8 @@ description: Program memory after executing sketch_on_face_start.kcl
"faceId": "[uuid]",
"id": "[uuid]",
"sourceRange": [
341,
357,
338,
354,
0
],
"tag": null,
@ -776,8 +573,8 @@ description: Program memory after executing sketch_on_face_start.kcl
"faceId": "[uuid]",
"id": "[uuid]",
"sourceRange": [
363,
380,
360,
377,
0
],
"tag": null,
@ -787,8 +584,8 @@ description: Program memory after executing sketch_on_face_start.kcl
"faceId": "[uuid]",
"id": "[uuid]",
"sourceRange": [
386,
394,
383,
391,
0
],
"tag": null,
@ -803,8 +600,8 @@ description: Program memory after executing sketch_on_face_start.kcl
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
319,
335,
316,
332,
0
]
},
@ -823,8 +620,8 @@ description: Program memory after executing sketch_on_face_start.kcl
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
341,
357,
338,
354,
0
]
},
@ -843,8 +640,8 @@ description: Program memory after executing sketch_on_face_start.kcl
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
363,
380,
360,
377,
0
]
},
@ -863,8 +660,8 @@ description: Program memory after executing sketch_on_face_start.kcl
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
386,
394,
383,
391,
0
]
},
@ -1104,8 +901,8 @@ description: Program memory after executing sketch_on_face_start.kcl
"__meta": [
{
"sourceRange": [
255,
282,
248,
279,
0
]
}
@ -1124,8 +921,8 @@ description: Program memory after executing sketch_on_face_start.kcl
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
288,
313,
285,
310,
0
]
}
@ -1133,8 +930,8 @@ description: Program memory after executing sketch_on_face_start.kcl
"__meta": [
{
"sourceRange": [
288,
313,
285,
310,
0
]
}
@ -1146,8 +943,8 @@ description: Program memory after executing sketch_on_face_start.kcl
"__meta": [
{
"sourceRange": [
288,
313,
285,
310,
0
]
}

View File

@ -1,6 +1,6 @@
//! Wasm bindings for `kcl`.
use std::sync::Arc;
use std::{str::FromStr, sync::Arc};
use futures::stream::TryStreamExt;
use gloo_utils::format::JsValueSerdeExt;
@ -56,10 +56,10 @@ pub async fn clear_scene_and_bust_cache(
// wasm_bindgen wrapper for execute
#[wasm_bindgen]
pub async fn execute(
pub async fn execute_wasm(
program_ast_json: &str,
program_memory_override_str: &str,
settings: &str,
units: &str,
engine_manager: kcl_lib::wasm_engine::EngineCommandManager,
fs_manager: kcl_lib::wasm_engine::FileSystemManager,
) -> Result<JsValue, String> {
@ -73,11 +73,11 @@ pub async fn execute(
// You cannot override the memory in non-mock mode.
let is_mock = program_memory_override.is_some();
let settings: kcl_lib::Configuration = serde_json::from_str(settings).map_err(|e| e.to_string())?;
let units = kcl_lib::UnitLength::from_str(units).map_err(|e| e.to_string())?;
let ctx = if is_mock {
kcl_lib::ExecutorContext::new_mock(fs_manager, settings.into()).await?
kcl_lib::ExecutorContext::new_mock(fs_manager, units).await?
} else {
kcl_lib::ExecutorContext::new(engine_manager, fs_manager, settings.into()).await?
kcl_lib::ExecutorContext::new(engine_manager, fs_manager, units).await?
};
let mut exec_state = ExecState::default();
@ -168,6 +168,23 @@ pub async fn make_default_planes(
JsValue::from_serde(&default_planes).map_err(|e| e.to_string())
}
// wasm_bindgen wrapper for modifying the grid
#[wasm_bindgen]
pub async fn modify_grid(
engine_manager: kcl_lib::wasm_engine::EngineCommandManager,
hidden: bool,
) -> Result<(), String> {
console_error_panic_hook::set_once();
// deserialize the ast from a stringified json
let engine = kcl_lib::wasm_engine::EngineConnection::new(engine_manager)
.await
.map_err(|e| format!("{:?}", e))?;
engine.modify_grid(hidden).await.map_err(String::from)?;
Ok(())
}
// wasm_bindgen wrapper for execute
#[wasm_bindgen]
pub async fn modify_ast_for_sketch_wasm(
@ -279,7 +296,7 @@ impl ServerConfig {
pub async fn kcl_lsp_run(
config: ServerConfig,
engine_manager: Option<kcl_lib::wasm_engine::EngineCommandManager>,
settings: Option<String>,
units: &str,
token: String,
baseurl: String,
) -> Result<(), JsValue> {
@ -292,12 +309,8 @@ pub async fn kcl_lsp_run(
} = config;
let executor_ctx = if let Some(engine_manager) = engine_manager {
let settings: kcl_lib::Configuration = if let Some(settings) = settings {
serde_json::from_str(&settings).map_err(|e| e.to_string())?
} else {
Default::default()
};
Some(kcl_lib::ExecutorContext::new(engine_manager, fs.clone(), settings.into()).await?)
let units = kcl_lib::UnitLength::from_str(units).map_err(|e| e.to_string())?;
Some(kcl_lib::ExecutorContext::new(engine_manager, fs.clone(), units).await?)
} else {
None
};

View File

@ -1,216 +0,0 @@
//! Cache testing framework.
use anyhow::Result;
use kcl_lib::ExecError;
struct Variation<'a> {
code: &'a str,
settings: &'a kcl_lib::ExecutorSettings,
}
async fn cache_test(test_name: &str, variations: Vec<Variation<'_>>) -> Result<Vec<(String, image::DynamicImage)>> {
let first = variations
.first()
.ok_or_else(|| anyhow::anyhow!("No variations provided for test '{}'", test_name))?;
let mut ctx = kcl_lib::ExecutorContext::new_with_client(first.settings.clone(), None, None).await?;
let mut exec_state = kcl_lib::ExecState::default();
let mut old_ast_state = None;
let mut img_results = Vec::new();
for (index, variation) in variations.iter().enumerate() {
let program = kcl_lib::Program::parse_no_errs(variation.code)?;
// set the new settings.
ctx.settings = variation.settings.clone();
ctx.run(
kcl_lib::CacheInformation {
old: old_ast_state,
new_ast: program.ast.clone(),
},
&mut exec_state,
)
.await?;
let snapshot_png_bytes = ctx.prepare_snapshot().await?.contents.0;
// Decode the snapshot, return it.
let img = image::ImageReader::new(std::io::Cursor::new(snapshot_png_bytes))
.with_guessed_format()
.map_err(|e| ExecError::BadPng(e.to_string()))
.and_then(|x| x.decode().map_err(|e| ExecError::BadPng(e.to_string())))?;
// Save the snapshot.
let path = crate::assert_out(&format!("cache_{}_{}", test_name, index), &img);
img_results.push((path, img));
// Prepare the last state.
old_ast_state = Some(kcl_lib::OldAstState {
ast: program.ast,
exec_state: exec_state.clone(),
settings: variation.settings.clone(),
});
}
Ok(img_results)
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_cache_change_units_changes_output() {
let code = r#"part001 = startSketchOn('XY')
|> startProfileAt([5.5229, 5.25217], %)
|> line([10.50433, -1.19122], %)
|> line([8.01362, -5.48731], %)
|> line([-1.02877, -6.76825], %)
|> line([-11.53311, 2.81559], %)
|> close(%)
|> extrude(4, %)
"#;
let result = cache_test(
"change_units_changes_output",
vec![
Variation {
code,
settings: &kcl_lib::ExecutorSettings {
units: kcl_lib::UnitLength::In,
..Default::default()
},
},
Variation {
code,
settings: &kcl_lib::ExecutorSettings {
units: kcl_lib::UnitLength::Mm,
..Default::default()
},
},
],
)
.await
.unwrap();
let first = result.first().unwrap();
let second = result.last().unwrap();
assert!(first.1 != second.1);
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_cache_change_grid_visualizes_grid_off_to_on() {
let code = r#"part001 = startSketchOn('XY')
|> startProfileAt([5.5229, 5.25217], %)
|> line([10.50433, -1.19122], %)
|> line([8.01362, -5.48731], %)
|> line([-1.02877, -6.76825], %)
|> line([-11.53311, 2.81559], %)
|> close(%)
|> extrude(4, %)
"#;
let result = cache_test(
"change_grid_visualizes_grid_off_to_on",
vec![
Variation {
code,
settings: &kcl_lib::ExecutorSettings {
show_grid: false,
..Default::default()
},
},
Variation {
code,
settings: &kcl_lib::ExecutorSettings {
show_grid: true,
..Default::default()
},
},
],
)
.await
.unwrap();
let first = result.first().unwrap();
let second = result.last().unwrap();
assert!(first.1 != second.1);
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_cache_change_grid_visualizes_grid_on_to_off() {
let code = r#"part001 = startSketchOn('XY')
|> startProfileAt([5.5229, 5.25217], %)
|> line([10.50433, -1.19122], %)
|> line([8.01362, -5.48731], %)
|> line([-1.02877, -6.76825], %)
|> line([-11.53311, 2.81559], %)
|> close(%)
|> extrude(4, %)
"#;
let result = cache_test(
"change_grid_visualizes_grid_on_to_off",
vec![
Variation {
code,
settings: &kcl_lib::ExecutorSettings {
show_grid: true,
..Default::default()
},
},
Variation {
code,
settings: &kcl_lib::ExecutorSettings {
show_grid: false,
..Default::default()
},
},
],
)
.await
.unwrap();
let first = result.first().unwrap();
let second = result.last().unwrap();
assert!(first.1 != second.1);
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_cache_change_highlight_edges_changes_visual() {
let code = r#"part001 = startSketchOn('XY')
|> startProfileAt([5.5229, 5.25217], %)
|> line([10.50433, -1.19122], %)
|> line([8.01362, -5.48731], %)
|> line([-1.02877, -6.76825], %)
|> line([-11.53311, 2.81559], %)
|> close(%)
|> extrude(4, %)
"#;
let result = cache_test(
"change_highlight_edges_changes_visual",
vec![
Variation {
code,
settings: &kcl_lib::ExecutorSettings {
highlight_edges: true,
..Default::default()
},
},
Variation {
code,
settings: &kcl_lib::ExecutorSettings {
highlight_edges: false,
..Default::default()
},
},
],
)
.await
.unwrap();
let first = result.first().unwrap();
let second = result.last().unwrap();
assert!(first.1 != second.1);
}

View File

@ -1,5 +1,3 @@
mod cache;
use kcl_lib::{
test_server::{execute_and_snapshot, execute_and_snapshot_no_auth},
UnitLength,
@ -7,7 +5,7 @@ use kcl_lib::{
/// The minimum permissible difference between asserted twenty-twenty images.
/// i.e. how different the current model snapshot can be from the previous saved one.
pub(crate) const MIN_DIFF: f64 = 0.99;
const MIN_DIFF: f64 = 0.99;
macro_rules! kcl_input {
($file:literal) => {
@ -15,11 +13,8 @@ macro_rules! kcl_input {
};
}
pub(crate) fn assert_out(test_name: &str, result: &image::DynamicImage) -> String {
let path = format!("tests/executor/outputs/{test_name}.png");
twenty_twenty::assert_image(&path, result, MIN_DIFF);
path
fn assert_out(test_name: &str, result: &image::DynamicImage) {
twenty_twenty::assert_image(format!("tests/executor/outputs/{test_name}.png"), result, MIN_DIFF);
}
#[tokio::test(flavor = "multi_thread")]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Some files were not shown because too many files have changed in this diff Show More