Merge branch 'main' into achalmers/kw-fn-sketches

This commit is contained in:
Adam Chalmers
2025-02-01 08:00:25 -06:00
140 changed files with 2808 additions and 318 deletions

View File

@ -18,6 +18,7 @@ export class ToolbarFixture {
filletButton!: Locator filletButton!: Locator
chamferButton!: Locator chamferButton!: Locator
shellButton!: Locator shellButton!: Locator
revolveButton!: Locator
offsetPlaneButton!: Locator offsetPlaneButton!: Locator
startSketchBtn!: Locator startSketchBtn!: Locator
lineBtn!: Locator lineBtn!: Locator
@ -47,6 +48,7 @@ export class ToolbarFixture {
this.filletButton = page.getByTestId('fillet3d') this.filletButton = page.getByTestId('fillet3d')
this.chamferButton = page.getByTestId('chamfer3d') this.chamferButton = page.getByTestId('chamfer3d')
this.shellButton = page.getByTestId('shell') this.shellButton = page.getByTestId('shell')
this.revolveButton = page.getByTestId('revolve')
this.offsetPlaneButton = page.getByTestId('plane-offset') this.offsetPlaneButton = page.getByTestId('plane-offset')
this.startSketchBtn = page.getByTestId('sketch') this.startSketchBtn = page.getByTestId('sketch')
this.lineBtn = page.getByTestId('line') this.lineBtn = page.getByTestId('line')

View File

@ -1078,7 +1078,7 @@ sketch002 = startSketchOn('XZ')
await page.waitForTimeout(500) await page.waitForTimeout(500)
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await expect( await expect(
page.getByText('Unable to sweep with the provided selection') page.getByText('Unable to sweep with the current selection. Reason:')
).toBeVisible() ).toBeVisible()
}) })
}) })
@ -1183,7 +1183,7 @@ extrude001 = extrude(-12, sketch001)
currentArgKey: 'radius', currentArgKey: 'radius',
currentArgValue: '5', currentArgValue: '5',
headerArguments: { headerArguments: {
Selection: '1 face', Selection: '1 segment',
Radius: '', Radius: '',
}, },
stage: 'arguments', stage: 'arguments',
@ -1192,7 +1192,7 @@ extrude001 = extrude(-12, sketch001)
await cmdBar.expectState({ await cmdBar.expectState({
commandName: 'Fillet', commandName: 'Fillet',
headerArguments: { headerArguments: {
Selection: '1 face', Selection: '1 segment',
Radius: '5', Radius: '5',
}, },
stage: 'review', stage: 'review',
@ -1398,7 +1398,7 @@ extrude001 = extrude(sketch001, length = -12)
currentArgKey: 'length', currentArgKey: 'length',
currentArgValue: '5', currentArgValue: '5',
headerArguments: { headerArguments: {
Selection: '1 face', Selection: '1 segment',
Length: '', Length: '',
}, },
stage: 'arguments', stage: 'arguments',
@ -1407,7 +1407,7 @@ extrude001 = extrude(sketch001, length = -12)
await cmdBar.expectState({ await cmdBar.expectState({
commandName: 'Chamfer', commandName: 'Chamfer',
headerArguments: { headerArguments: {
Selection: '1 face', Selection: '1 segment',
Length: '5', Length: '5',
}, },
stage: 'review', stage: 'review',
@ -1846,8 +1846,176 @@ sweep001 = sweep({ path = sketch002 }, sketch001)
await page.waitForTimeout(500) await page.waitForTimeout(500)
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await expect( await expect(
page.getByText('Unable to shell with the provided selection') page.getByText('Unable to shell with the current selection. Reason:')
).toBeVisible() ).toBeVisible()
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
}) })
}) })
test.describe('Revolve point and click workflows', () => {
test('Base case workflow, auto spam continue in command bar', async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `
sketch001 = startSketchOn('XZ')
|> startProfileAt([-100.0, 100.0], %)
|> angledLine([0, 200.0], %, $rectangleSegmentA001)
|> angledLine([segAng(rectangleSegmentA001) - 90, 200], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(200, sketch001)
sketch002 = startSketchOn(extrude001, rectangleSegmentA001)
|> startProfileAt([-66.77, 84.81], %)
|> angledLine([180, 27.08], %, $rectangleSegmentA002)
|> angledLine([
segAng(rectangleSegmentA002) - 90,
27.8
], %, $rectangleSegmentB002)
|> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %, $rectangleSegmentC002)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// select line of code
const codeToSelecton = `segAng(rectangleSegmentA002) - 90,`
// revolve
await page.getByText(codeToSelecton).click()
await toolbar.revolveButton.click()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
const newCodeToFind = `revolve001 = revolve({ angle = 360, axis = 'X' }, sketch002)`
expect(editor.expectEditor.toContain(newCodeToFind)).toBeTruthy()
})
test('revolve surface around edge from an extruded solid2d', async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `
sketch001 = startSketchOn('XZ')
|> startProfileAt([-102.57, 101.72], %)
|> angledLine([0, 202.6], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
202.6
], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(50, sketch001)
sketch002 = startSketchOn(extrude001, rectangleSegmentA001)
|> circle({
center = [-11.34, 10.0],
radius = 8.69
}, %)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// select line of code
const codeToSelecton = `center = [-11.34, 10.0]`
// revolve
await page.getByText(codeToSelecton).click()
await toolbar.revolveButton.click()
await page.getByText('Edge', { exact: true }).click()
const lineCodeToSelection = `|> angledLine([0, 202.6], %, $rectangleSegmentA001)`
await page.getByText(lineCodeToSelection).click()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
const newCodeToFind = `revolve001 = revolve({angle = 360, axis = getOppositeEdge(rectangleSegmentA001)}, sketch002) `
expect(editor.expectEditor.toContain(newCodeToFind)).toBeTruthy()
})
test('revolve sketch circle around line segment from startProfileAt sketch', async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `
sketch002 = startSketchOn('XY')
|> startProfileAt([-2.02, 1.79], %)
|> xLine(2.6, %)
sketch001 = startSketchOn('-XY')
|> startProfileAt([-0.48, 1.25], %)
|> angledLine([0, 2.38], %, $rectangleSegmentA001)
|> angledLine([segAng(rectangleSegmentA001) - 90, 2.4], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(5, sketch001)
sketch003 = startSketchOn(extrude001, 'START')
|> circle({
center = [-0.69, 0.56],
radius = 0.28
}, %)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// select line of code
const codeToSelecton = `center = [-0.69, 0.56]`
// revolve
await page.getByText(codeToSelecton).click()
await toolbar.revolveButton.click()
await page.getByText('Edge', { exact: true }).click()
const lineCodeToSelection = `|> xLine(2.6, %)`
await page.getByText(lineCodeToSelection).click()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
const newCodeToFind = `revolve001 = revolve({ angle = 360, axis = seg01 }, sketch003)`
expect(editor.expectEditor.toContain(newCodeToFind)).toBeTruthy()
})
})

View File

@ -572,7 +572,7 @@ test(
fs.utimesSync(`${dir}/lego/main.kcl`, _1995, _1995) fs.utimesSync(`${dir}/lego/main.kcl`, _1995, _1995)
}) })
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 600 })
page.on('console', console.log) page.on('console', console.log)

View File

@ -886,7 +886,7 @@ test.describe('Sketch tests', () => {
// sketch selection should already have been made. "Selection: 1 face" only show up when the selection has been made already // sketch selection should already have been made. "Selection: 1 face" only show up when the selection has been made already
// otherwise the cmdbar would be waiting for a selection. // otherwise the cmdbar would be waiting for a selection.
await expect( await expect(
page.getByRole('button', { name: 'selection : 1 face', exact: false }) page.getByRole('button', { name: 'selection : 1 segment', exact: false })
).toBeVisible({ ).toBeVisible({
timeout: 10_000, timeout: 10_000,
}) })

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

View File

@ -69,7 +69,6 @@ test.describe('Testing in-app sample loading', () => {
await confirmButton.click() await confirmButton.click()
await editor.expectEditor.toContain('// ' + newSample.title) await editor.expectEditor.toContain('// ' + newSample.title)
await expect(unitsToast('in')).toBeVisible()
}) })
}) })
@ -158,7 +157,6 @@ test.describe('Testing in-app sample loading', () => {
await editor.expectEditor.toContain('// ' + sampleOne.title) await editor.expectEditor.toContain('// ' + sampleOne.title)
await expect(newlyCreatedFile(sampleOne.file)).toBeVisible() await expect(newlyCreatedFile(sampleOne.file)).toBeVisible()
await expect(projectMenuButton).toContainText(sampleOne.file) await expect(projectMenuButton).toContainText(sampleOne.file)
await expect(unitsToast('in')).toBeVisible()
}) })
await test.step(`Now overwrite the current file`, async () => { await test.step(`Now overwrite the current file`, async () => {
@ -188,7 +186,6 @@ test.describe('Testing in-app sample loading', () => {
await expect(newlyCreatedFile(sampleOne.file)).toBeVisible() await expect(newlyCreatedFile(sampleOne.file)).toBeVisible()
await expect(newlyCreatedFile(sampleTwo.file)).not.toBeVisible() await expect(newlyCreatedFile(sampleTwo.file)).not.toBeVisible()
await expect(projectMenuButton).toContainText(sampleOne.file) await expect(projectMenuButton).toContainText(sampleOne.file)
await expect(unitsToast('mm')).toBeVisible()
}) })
} }
) )

View File

@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { useHotKeyListener } from './hooks/useHotKeyListener' import { useHotKeyListener } from './hooks/useHotKeyListener'
import { Stream } from './components/Stream' import { Stream } from './components/Stream'
import { AppHeader } from './components/AppHeader' import { AppHeader } from './components/AppHeader'
@ -24,7 +24,12 @@ import { UnitsMenu } from 'components/UnitsMenu'
import { CameraProjectionToggle } from 'components/CameraProjectionToggle' import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher' import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
import { maybeWriteToDisk } from 'lib/telemetry' import { maybeWriteToDisk } from 'lib/telemetry'
import { takeScreenshotOfVideoStreamCanvas } from 'lib/screenshot'
import { writeProjectThumbnailFile } from 'lib/desktop'
import { useRouteLoaderData } from 'react-router-dom'
import { useEngineCommands } from 'components/EngineCommands'
import { commandBarActor } from 'machines/commandBarMachine' import { commandBarActor } from 'machines/commandBarMachine'
import { useToken } from 'machines/appMachine'
maybeWriteToDisk() maybeWriteToDisk()
.then(() => {}) .then(() => {})
.catch(() => {}) .catch(() => {})
@ -54,14 +59,20 @@ export function App() {
const projectName = project?.name || null const projectName = project?.name || null
const projectPath = project?.path || null const projectPath = project?.path || null
const [commands] = useEngineCommands()
const [capturedCanvas, setCapturedCanvas] = useState(false)
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const lastCommandType = commands[commands.length - 1]?.type
useEffect(() => { useEffect(() => {
onProjectOpen({ name: projectName, path: projectPath }, file || null) onProjectOpen({ name: projectName, path: projectPath }, file || null)
}, [projectName, projectPath]) }, [projectName, projectPath])
useHotKeyListener() useHotKeyListener()
const { auth, settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const token = auth?.context?.token const token = useToken()
const coreDumpManager = useMemo( const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, codeManager, token), () => new CoreDumpManager(engineCommandManager, codeManager, token),
@ -91,6 +102,28 @@ export function App() {
useEngineConnectionSubscriptions() useEngineConnectionSubscriptions()
// Generate thumbnail.png when loading the app
useEffect(() => {
if (!capturedCanvas && lastCommandType === 'execution-done') {
setTimeout(() => {
const projectDirectoryWithoutEndingSlash = loaderData?.project?.path
if (!projectDirectoryWithoutEndingSlash) {
return
}
const dataUrl: string = takeScreenshotOfVideoStreamCanvas()
// zoom to fit command does not wait, wait 500ms to see if zoom to fit finishes
writeProjectThumbnailFile(dataUrl, projectDirectoryWithoutEndingSlash)
.then(() => {})
.catch((e) => {
console.error(
`Failed to generate thumbnail for ${projectDirectoryWithoutEndingSlash}`
)
console.error(e)
})
}, 500)
}
}, [lastCommandType])
return ( return (
<div className="relative h-full flex flex-col" ref={ref}> <div className="relative h-full flex flex-col" ref={ref}>
<AppHeader <AppHeader

View File

@ -1,10 +1,10 @@
import { useAuthState } from 'machines/appMachine'
import Loading from './components/Loading' import Loading from './components/Loading'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
// Wrapper around protected routes, used in src/Router.tsx // Wrapper around protected routes, used in src/Router.tsx
export const Auth = ({ children }: React.PropsWithChildren) => { export const Auth = ({ children }: React.PropsWithChildren) => {
const { auth } = useSettingsAuthContext() const authState = useAuthState()
const isLoggingIn = auth?.state.matches('checkIfLoggedIn') const isLoggingIn = authState.matches('checkIfLoggedIn')
return isLoggingIn ? ( return isLoggingIn ? (
<Loading> <Loading>

View File

@ -37,7 +37,6 @@ import { KclContextProvider } from 'lang/KclProvider'
import { ASK_TO_OPEN_QUERY_PARAM, BROWSER_PROJECT_NAME } from 'lib/constants' import { ASK_TO_OPEN_QUERY_PARAM, BROWSER_PROJECT_NAME } from 'lib/constants'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import { codeManager, engineCommandManager } from 'lib/singletons' import { codeManager, engineCommandManager } from 'lib/singletons'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import useHotkeyWrapper from 'lib/hotkeyWrapper' import useHotkeyWrapper from 'lib/hotkeyWrapper'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { coreDump } from 'lang/wasm' import { coreDump } from 'lang/wasm'
@ -47,6 +46,7 @@ import { reportRejection } from 'lib/trap'
import { RouteProvider } from 'components/RouteProvider' import { RouteProvider } from 'components/RouteProvider'
import { ProjectsContextProvider } from 'components/ProjectsContextProvider' import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
import { OpenInDesktopAppHandler } from 'components/OpenInDesktopAppHandler' import { OpenInDesktopAppHandler } from 'components/OpenInDesktopAppHandler'
import { useToken } from 'machines/appMachine'
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
@ -203,8 +203,7 @@ export const Router = () => {
} }
function CoreDump() { function CoreDump() {
const { auth } = useSettingsAuthContext() const token = useToken()
const token = auth?.context?.token
const coreDumpManager = useMemo( const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, codeManager, token), () => new CoreDumpManager(engineCommandManager, codeManager, token),
[] []

View File

@ -2,11 +2,11 @@ import { Toolbar } from '../Toolbar'
import UserSidebarMenu from 'components/UserSidebarMenu' import UserSidebarMenu from 'components/UserSidebarMenu'
import { type IndexLoaderData } from 'lib/types' import { type IndexLoaderData } from 'lib/types'
import ProjectSidebarMenu from './ProjectSidebarMenu' import ProjectSidebarMenu from './ProjectSidebarMenu'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import styles from './AppHeader.module.css' import styles from './AppHeader.module.css'
import { RefreshButton } from 'components/RefreshButton' import { RefreshButton } from 'components/RefreshButton'
import { CommandBarOpenButton } from './CommandBarOpenButton' import { CommandBarOpenButton } from './CommandBarOpenButton'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { useUser } from 'machines/appMachine'
interface AppHeaderProps extends React.PropsWithChildren { interface AppHeaderProps extends React.PropsWithChildren {
showToolbar?: boolean showToolbar?: boolean
@ -24,8 +24,7 @@ export const AppHeader = ({
style, style,
enableMenu = false, enableMenu = false,
}: AppHeaderProps) => { }: AppHeaderProps) => {
const { auth } = useSettingsAuthContext() const user = useUser()
const user = auth?.context?.user
return ( return (
<header <header

View File

@ -30,6 +30,7 @@ import {
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { markOnce } from 'lib/performance' import { markOnce } from 'lib/performance'
import { commandBarActor } from 'machines/commandBarMachine' import { commandBarActor } from 'machines/commandBarMachine'
import { useToken } from 'machines/appMachine'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -47,7 +48,8 @@ export const FileMachineProvider = ({
children: React.ReactNode children: React.ReactNode
}) => { }) => {
const navigate = useNavigate() const navigate = useNavigate()
const { settings, auth } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const token = useToken()
const projectData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData const projectData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const { project, file } = projectData const { project, file } = projectData
const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>( const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>(
@ -297,7 +299,7 @@ export const FileMachineProvider = ({
const kclCommandMemo = useMemo( const kclCommandMemo = useMemo(
() => () =>
kclCommands({ kclCommands({
authToken: auth?.context?.token ?? '', authToken: token ?? '',
projectData, projectData,
settings: { settings: {
defaultUnit: settings?.context?.modeling.defaultUnit.current ?? 'mm', defaultUnit: settings?.context?.modeling.defaultUnit.current ?? 'mm',

View File

@ -27,6 +27,7 @@ import { PROJECT_ENTRYPOINT } from 'lib/constants'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { codeManager } from 'lib/singletons' import { codeManager } from 'lib/singletons'
import { useToken } from 'machines/appMachine'
function getWorkspaceFolders(): LSP.WorkspaceFolder[] { function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
return [] return []
@ -69,8 +70,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const [isKclLspReady, setIsKclLspReady] = useState(false) const [isKclLspReady, setIsKclLspReady] = useState(false)
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false) const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
const { auth } = useSettingsAuthContext() const token = useToken()
const token = auth?.context.token
const navigate = useNavigate() const navigate = useNavigate()
// So this is a bit weird, we need to initialize the lsp server and client. // So this is a bit weird, we need to initialize the lsp server and client.

View File

@ -1,10 +1,8 @@
import { useEngineCommands } from './EngineCommands' import { useEngineCommands } from './EngineCommands'
import { Spinner } from './Spinner' import { Spinner } from './Spinner'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
export const ModelStateIndicator = () => { export const ModelStateIndicator = () => {
const [commands] = useEngineCommands() const [commands] = useEngineCommands()
const lastCommandType = commands[commands.length - 1]?.type const lastCommandType = commands[commands.length - 1]?.type
let className = 'w-6 h-6 ' let className = 'w-6 h-6 '

View File

@ -89,6 +89,7 @@ import { Node } from 'wasm-lib/kcl/bindings/Node'
import { promptToEditFlow } from 'lib/promptToEdit' import { promptToEditFlow } from 'lib/promptToEdit'
import { kclEditorActor } from 'machines/kclEditorMachine' import { kclEditorActor } from 'machines/kclEditorMachine'
import { commandBarActor } from 'machines/commandBarMachine' import { commandBarActor } from 'machines/commandBarMachine'
import { useToken } from 'machines/appMachine'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -110,7 +111,6 @@ export const ModelingMachineProvider = ({
children: React.ReactNode children: React.ReactNode
}) => { }) => {
const { const {
auth,
settings: { settings: {
context: { context: {
app: { theme, enableSSAO, allowOrbitInSketchMode }, app: { theme, enableSSAO, allowOrbitInSketchMode },
@ -127,7 +127,7 @@ export const ModelingMachineProvider = ({
const navigate = useNavigate() const navigate = useNavigate()
const { context, send: fileMachineSend } = useFileContext() const { context, send: fileMachineSend } = useFileContext()
const { file } = useLoaderData() as IndexLoaderData const { file } = useLoaderData() as IndexLoaderData
const token = auth?.context?.token const token = useToken()
const streamRef = useRef<HTMLDivElement>(null) const streamRef = useRef<HTMLDivElement>(null)
const persistedContext = useMemo(() => getPersistedContext(), []) const persistedContext = useMemo(() => getPersistedContext(), [])

View File

@ -2,7 +2,7 @@ import { FormEvent, useEffect, useRef, useState } from 'react'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { ActionButton } from '../ActionButton' import { ActionButton } from '../ActionButton'
import { FILE_EXT } from 'lib/constants' import { FILE_EXT, PROJECT_IMAGE_NAME } from 'lib/constants'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import Tooltip from '../Tooltip' import Tooltip from '../Tooltip'
import { DeleteConfirmationDialog } from './DeleteProjectDialog' import { DeleteConfirmationDialog } from './DeleteProjectDialog'
@ -29,7 +29,7 @@ function ProjectCard({
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false) const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
const [numberOfFiles, setNumberOfFiles] = useState(1) const [numberOfFiles, setNumberOfFiles] = useState(1)
const [numberOfFolders, setNumberOfFolders] = useState(0) const [numberOfFolders, setNumberOfFolders] = useState(0)
// const [imageUrl, setImageUrl] = useState('') const [imageUrl, setImageUrl] = useState('')
let inputRef = useRef<HTMLInputElement>(null) let inputRef = useRef<HTMLInputElement>(null)
@ -53,18 +53,21 @@ function ProjectCard({
setNumberOfFolders(project.directory_count) setNumberOfFolders(project.directory_count)
} }
// async function setupImageUrl() { async function setupImageUrl() {
// const projectImagePath = await join(project.file.path, PROJECT_IMAGE_NAME) const projectImagePath = window.electron.path.join(
// if (await exists(projectImagePath)) { project.path,
// const imageData = await readFile(projectImagePath) PROJECT_IMAGE_NAME
// const blob = new Blob([imageData], { type: 'image/jpg' }) )
// const imageUrl = URL.createObjectURL(blob) if (await window.electron.exists(projectImagePath)) {
// setImageUrl(imageUrl) const imageData = await window.electron.readFile(projectImagePath)
// } const blob = new Blob([imageData], { type: 'image/png' })
// } const imageUrl = URL.createObjectURL(blob)
setImageUrl(imageUrl)
}
}
void getNumberOfFiles() void getNumberOfFiles()
// void setupImageUrl() void setupImageUrl()
}, [project.kcl_file_count, project.directory_count]) }, [project.kcl_file_count, project.directory_count])
useEffect(() => { useEffect(() => {
@ -84,7 +87,7 @@ function ProjectCard({
to={`${PATHS.FILE}/${encodeURIComponent(project.default_file)}`} to={`${PATHS.FILE}/${encodeURIComponent(project.default_file)}`}
className="flex flex-col flex-1 !no-underline !text-chalkboard-110 dark:!text-chalkboard-10 group-hover:!hue-rotate-0 min-h-[5em] divide-y divide-primary/40 dark:divide-chalkboard-80 group-hover:!divide-primary" className="flex flex-col flex-1 !no-underline !text-chalkboard-110 dark:!text-chalkboard-10 group-hover:!hue-rotate-0 min-h-[5em] divide-y divide-primary/40 dark:divide-chalkboard-80 group-hover:!divide-primary"
> >
{/* <div className="h-36 relative overflow-hidden bg-gradient-to-b from-transparent to-primary/10 rounded-t-sm"> <div className="h-36 relative overflow-hidden bg-gradient-to-b from-transparent to-primary/10 rounded-t-sm">
{imageUrl && ( {imageUrl && (
<img <img
src={imageUrl} src={imageUrl}
@ -92,7 +95,7 @@ function ProjectCard({
className="h-full w-full transition-transform group-hover:scale-105 object-cover" className="h-full w-full transition-transform group-hover:scale-105 object-cover"
/> />
)} )}
</div> */} </div>
<div className="pb-2 flex flex-col flex-grow flex-auto gap-2 rounded-b-sm"> <div className="pb-2 flex flex-col flex-grow flex-auto gap-2 rounded-b-sm">
{isEditing ? ( {isEditing ? (
<ProjectCardRenameForm <ProjectCardRenameForm

View File

@ -20,6 +20,7 @@ import { useSelector } from '@xstate/react'
import { copyFileShareLink } from 'lib/links' import { copyFileShareLink } from 'lib/links'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { DEV } from 'env' import { DEV } from 'env'
import { useToken } from 'machines/appMachine'
const ProjectSidebarMenu = ({ const ProjectSidebarMenu = ({
project, project,
@ -103,7 +104,8 @@ function ProjectMenuPopover({
const location = useLocation() const location = useLocation()
const navigate = useNavigate() const navigate = useNavigate()
const filePath = useAbsoluteFilePath() const filePath = useAbsoluteFilePath()
const { settings, auth } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const token = useToken()
const machineManager = useContext(MachineManagerContext) const machineManager = useContext(MachineManagerContext)
const commands = useSelector(commandBarActor, commandsSelector) const commands = useSelector(commandBarActor, commandsSelector)
@ -194,7 +196,7 @@ function ProjectMenuPopover({
disabled: !DEV, disabled: !DEV,
onClick: async () => { onClick: async () => {
await copyFileShareLink({ await copyFileShareLink({
token: auth?.context.token || '', token: token ?? '',
code: codeManager.code, code: codeManager.code,
name: project?.name || '', name: project?.name || '',
units: settings.context.modeling.defaultUnit.current, units: settings.context.modeling.defaultUnit.current,

View File

@ -8,10 +8,10 @@ import Tooltip from './Tooltip'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
import { toSync } from 'lib/utils' import { toSync } from 'lib/utils'
import { useToken } from 'machines/appMachine'
export const RefreshButton = ({ children }: React.PropsWithChildren) => { export const RefreshButton = ({ children }: React.PropsWithChildren) => {
const { auth } = useSettingsAuthContext() const token = useToken()
const token = auth?.context?.token
const coreDumpManager = useMemo( const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, codeManager, token), () => new CoreDumpManager(engineCommandManager, codeManager, token),
[] []

View File

@ -2,10 +2,12 @@ import { useEffect, useState, createContext, ReactNode } from 'react'
import { useNavigation, useLocation } from 'react-router-dom' import { useNavigation, useLocation } from 'react-router-dom'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { markOnce } from 'lib/performance' import { markOnce } from 'lib/performance'
import { useAuthNavigation } from 'hooks/useAuthNavigation'
export const RouteProviderContext = createContext({}) export const RouteProviderContext = createContext({})
export function RouteProvider({ children }: { children: ReactNode }) { export function RouteProvider({ children }: { children: ReactNode }) {
useAuthNavigation()
const [first, setFirstState] = useState(true) const [first, setFirstState] = useState(true)
const navigation = useNavigation() const navigation = useNavigation()
const location = useLocation() const location = useLocation()

View File

@ -2,10 +2,7 @@ import { trap } from 'lib/trap'
import { useMachine, useSelector } from '@xstate/react' import { useMachine, useSelector } from '@xstate/react'
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom' import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
import { PATHS, BROWSER_PATH } from 'lib/paths' import { PATHS, BROWSER_PATH } from 'lib/paths'
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
import withBaseUrl from '../lib/withBaseURL'
import React, { createContext, useEffect, useState } from 'react' import React, { createContext, useEffect, useState } from 'react'
import useStateMachineCommands from '../hooks/useStateMachineCommands'
import { settingsMachine } from 'machines/settingsMachine' import { settingsMachine } from 'machines/settingsMachine'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { import {
@ -16,7 +13,6 @@ import {
} from 'lib/theme' } from 'lib/theme'
import decamelize from 'decamelize' import decamelize from 'decamelize'
import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate' import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate'
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
import { import {
kclManager, kclManager,
sceneInfra, sceneInfra,
@ -50,7 +46,6 @@ type MachineContext<T extends AnyStateMachine> = {
} }
type SettingsAuthContextType = { type SettingsAuthContextType = {
auth: MachineContext<typeof authMachine>
settings: MachineContext<typeof settingsMachine> settings: MachineContext<typeof settingsMachine>
} }
@ -370,40 +365,9 @@ export const SettingsAuthProviderBase = ({
) )
}, [settingsState.context.textEditor.blinkingCursor.current]) }, [settingsState.context.textEditor.blinkingCursor.current])
// Auth machine setup
const [authState, authSend, authActor] = useMachine(
authMachine.provide({
actions: {
goToSignInPage: () => {
navigate(PATHS.SIGN_IN)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
logout()
},
goToIndexPage: () => {
if (location.pathname.includes(PATHS.SIGN_IN)) {
navigate(PATHS.INDEX)
}
},
},
})
)
useStateMachineCommands({
machineId: 'auth',
state: authState,
send: authSend,
commandBarConfig: authCommandBarConfig,
actor: authActor,
})
return ( return (
<SettingsAuthContext.Provider <SettingsAuthContext.Provider
value={{ value={{
auth: {
state: authState,
context: authState.context,
send: authSend,
},
settings: { settings: {
state: settingsState, state: settingsState,
context: settingsState.context, context: settingsState.context,
@ -417,12 +381,3 @@ export const SettingsAuthProviderBase = ({
} }
export default SettingsAuthProvider export default SettingsAuthProvider
export async function logout() {
localStorage.removeItem(TOKEN_PERSIST_KEY)
if (isDesktop()) return Promise.resolve(null)
return fetch(withBaseUrl('/logout'), {
method: 'POST',
credentials: 'include',
})
}

View File

@ -4,12 +4,12 @@ import { useLocation, useNavigate } from 'react-router-dom'
import { Fragment, useMemo, useState } from 'react' import { Fragment, useMemo, useState } from 'react'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import Tooltip from './Tooltip' import Tooltip from './Tooltip'
import usePlatform from 'hooks/usePlatform' import usePlatform from 'hooks/usePlatform'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { authActor } from 'machines/appMachine'
type User = Models['User_type'] type User = Models['User_type']
@ -20,7 +20,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
const displayedName = getDisplayName(user) const displayedName = getDisplayName(user)
const [imageLoadFailed, setImageLoadFailed] = useState(false) const [imageLoadFailed, setImageLoadFailed] = useState(false)
const navigate = useNavigate() const navigate = useNavigate()
const send = useSettingsAuthContext()?.auth?.send const send = authActor.send
// We filter this memoized list so that no orphan "break" elements are rendered. // We filter this memoized list so that no orphan "break" elements are rendered.
const userMenuItems = useMemo<(ActionButtonProps | 'break')[]>( const userMenuItems = useMemo<(ActionButtonProps | 'break')[]>(

View File

@ -0,0 +1,29 @@
import { PATHS } from 'lib/paths'
import { useAuthState } from 'machines/appMachine'
import { useEffect } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
/**
* A simple hook that listens to the auth state of the app and navigates
* accordingly.
*/
export function useAuthNavigation() {
const navigate = useNavigate()
const location = useLocation()
const authState = useAuthState()
// Subscribe to the auth state of the app and navigate accordingly.
useEffect(() => {
if (
authState.matches('loggedIn') &&
location.pathname.includes(PATHS.SIGN_IN)
) {
navigate(PATHS.INDEX)
} else if (
authState.matches('loggedOut') &&
!location.pathname.includes(PATHS.SIGN_IN)
) {
navigate(PATHS.SIGN_IN)
}
}, [authState])
}

View File

@ -5,7 +5,11 @@ import {
PathToNode, PathToNode,
Identifier, Identifier,
topLevelRange, topLevelRange,
PipeExpression,
CallExpression,
VariableDeclarator,
} from './wasm' } from './wasm'
import { ProgramMemory } from 'lang/wasm'
import { import {
findAllPreviousVariables, findAllPreviousVariables,
isNodeSafeToReplace, isNodeSafeToReplace,
@ -25,9 +29,11 @@ import {
createCallExpression, createCallExpression,
createLiteral, createLiteral,
createPipeSubstitution, createPipeSubstitution,
createCallExpressionStdLib,
} from './modifyAst' } from './modifyAst'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { codeRefFromRange } from './std/artifactGraph' import { codeRefFromRange } from './std/artifactGraph'
import { addCallExpressionsToPipe, addCloseToPipe } from 'lang/std/sketch'
beforeAll(async () => { beforeAll(async () => {
await initPromise await initPromise
@ -680,3 +686,115 @@ myNestedVar = [
expect(pathToNode).toEqual(pathToNode2) expect(pathToNode).toEqual(pathToNode2)
}) })
}) })
describe('Testing specific sketch getNodeFromPath workflow', () => {
it('should parse the code', () => {
const openSketch = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0.02, 0.22], %)
|> xLine(0.39, %)
|> line([0.02, -0.17], %)
|> yLine(-0.15, %)
|> line([-0.21, -0.02], %)
|> xLine(-0.15, %)
|> line([-0.02, 0.21], %)
|> line([-0.08, 0.05], %)`
const ast = assertParse(openSketch)
expect(ast.start).toEqual(0)
expect(ast.end).toEqual(227)
})
it('should find the location to add new lineTo', () => {
const openSketch = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0.02, 0.22], %)
|> xLine(0.39, %)
|> line([0.02, -0.17], %)
|> yLine(-0.15, %)
|> line([-0.21, -0.02], %)
|> xLine(-0.15, %)
|> line([-0.02, 0.21], %)
|> line([-0.08, 0.05], %)`
const ast = assertParse(openSketch)
const sketchSnippet = `startProfileAt([0.02, 0.22], %)`
const sketchRange = topLevelRange(
openSketch.indexOf(sketchSnippet),
openSketch.indexOf(sketchSnippet) + sketchSnippet.length
)
const sketchPathToNode = getNodePathFromSourceRange(ast, sketchRange)
const modifiedAst = addCallExpressionsToPipe({
node: ast,
programMemory: ProgramMemory.empty(),
pathToNode: sketchPathToNode,
expressions: [
createCallExpressionStdLib(
'lineTo', // We are forcing lineTo!
[
createArrayExpression([
createCallExpressionStdLib('profileStartX', [
createPipeSubstitution(),
]),
createCallExpressionStdLib('profileStartY', [
createPipeSubstitution(),
]),
]),
createPipeSubstitution(),
]
),
],
})
if (err(modifiedAst)) throw modifiedAst
const recasted = recast(modifiedAst)
const expectedCode = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0.02, 0.22], %)
|> xLine(0.39, %)
|> line([0.02, -0.17], %)
|> yLine(-0.15, %)
|> line([-0.21, -0.02], %)
|> xLine(-0.15, %)
|> line([-0.02, 0.21], %)
|> line([-0.08, 0.05], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
`
expect(recasted).toEqual(expectedCode)
})
it('it should find the location to add close', () => {
const openSketch = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0.02, 0.22], %)
|> xLine(0.39, %)
|> line([0.02, -0.17], %)
|> yLine(-0.15, %)
|> line([-0.21, -0.02], %)
|> xLine(-0.15, %)
|> line([-0.02, 0.21], %)
|> line([-0.08, 0.05], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
`
const ast = assertParse(openSketch)
const sketchSnippet = `startProfileAt([0.02, 0.22], %)`
const sketchRange = topLevelRange(
openSketch.indexOf(sketchSnippet),
openSketch.indexOf(sketchSnippet) + sketchSnippet.length
)
const sketchPathToNode = getNodePathFromSourceRange(ast, sketchRange)
const modifiedAst = addCloseToPipe({
node: ast,
programMemory: ProgramMemory.empty(),
pathToNode: sketchPathToNode,
})
if (err(modifiedAst)) throw modifiedAst
const recasted = recast(modifiedAst)
const expectedCode = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0.02, 0.22], %)
|> xLine(0.39, %)
|> line([0.02, -0.17], %)
|> yLine(-0.15, %)
|> line([-0.21, -0.02], %)
|> xLine(-0.15, %)
|> line([-0.02, 0.21], %)
|> line([-0.08, 0.05], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
`
expect(recasted).toEqual(expectedCode)
})
})

View File

@ -22,6 +22,7 @@ import {
topLevelRange, topLevelRange,
VariableDeclaration, VariableDeclaration,
VariableDeclarator, VariableDeclarator,
recast,
} from './wasm' } from './wasm'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { createIdentifier, splitPathAtLastIndex } from './modifyAst' import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
@ -79,7 +80,28 @@ export function getNodeFromPath<T>(
deepPath: successfulPaths, deepPath: successfulPaths,
} }
} }
return new Error('not an object') const stackTraceError = new Error()
const sourceCode = recast(node)
const levels = stackTraceError.stack?.split('\n')
const aFewFunctionNames: string[] = []
let tree = ''
levels?.forEach((val, index) => {
const fnName = val.trim().split(' ')[1]
const ending = index === levels.length - 1 ? ' ' : ' > '
tree += fnName + ending
if (index < 3) {
aFewFunctionNames.push(fnName)
}
})
const error = new Error(
`Failed to stopAt ${stopAt}, ${aFewFunctionNames
.filter((a) => a)
.join(' > ')}`
)
console.error(tree)
console.error(sourceCode)
console.error(error.stack)
return error
} }
parent = currentNode parent = currentNode
parentEdge = pathItem[0] parentEdge = pathItem[0]

View File

@ -1999,7 +1999,7 @@ export class EngineCommandManager extends EventTarget {
.catch((e) => { .catch((e) => {
// TODO: Previously was never caught, we are not rejecting these pendingCommands but this needs to be handled at some point. // TODO: Previously was never caught, we are not rejecting these pendingCommands but this needs to be handled at some point.
/*noop*/ /*noop*/
return null return e
}) })
} }
/** /**

View File

@ -18,6 +18,7 @@ import {
default_project_settings, default_project_settings,
base64_decode, base64_decode,
clear_scene_and_bust_cache, clear_scene_and_bust_cache,
change_kcl_settings,
reloadModule, reloadModule,
} from 'lib/wasm_lib_wrapper' } from 'lib/wasm_lib_wrapper'
@ -56,6 +57,7 @@ import { ArtifactGraph as RustArtifactGraph } from 'wasm-lib/kcl/bindings/Artifa
import { Artifact } from './std/artifactGraph' import { Artifact } from './std/artifactGraph'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { NumericSuffix } from 'wasm-lib/kcl/bindings/NumericSuffix' import { NumericSuffix } from 'wasm-lib/kcl/bindings/NumericSuffix'
import { MetaSettings } from 'wasm-lib/kcl/bindings/MetaSettings'
export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact' export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact' export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
@ -848,3 +850,17 @@ export function base64Decode(base64: string): ArrayBuffer | Error {
return new Error('Caught error decoding base64 string: ' + e) return new Error('Caught error decoding base64 string: ' + e)
} }
} }
/// Change the meta settings for the kcl file.
/// Returns the new kcl string with the updated settings.
export function changeKclSettings(
kcl: string,
settings: MetaSettings
): string | Error {
try {
return change_kcl_settings(kcl, JSON.stringify(settings))
} catch (e) {
console.error('Caught error changing kcl settings: ' + e)
return new Error('Caught error changing kcl settings: ' + e)
}
}

View File

@ -1,17 +1,14 @@
import { StateMachineCommandSetConfig } from 'lib/commandTypes' import { Command } from 'lib/commandTypes'
import { authMachine } from 'machines/authMachine' import { authActor } from 'machines/appMachine'
import { ACTOR_IDS } from 'machines/machineConstants'
type AuthCommandSchema = {} export const authCommands: Command[] = [
{
export const authCommandBarConfig: StateMachineCommandSetConfig< groupId: ACTOR_IDS.AUTH,
typeof authMachine, name: 'log-out',
AuthCommandSchema displayName: 'Log out',
> = {
'Log in': {
hide: 'both',
},
'Log out': {
args: [],
icon: 'arrowLeft', icon: 'arrowLeft',
needsReview: false,
onSubmit: () => authActor.send({ type: 'Log out' }),
}, },
} ]

View File

@ -308,7 +308,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
description: description:
'Create a 3D body by moving a sketch region along an arbitrary path.', 'Create a 3D body by moving a sketch region along an arbitrary path.',
icon: 'sweep', icon: 'sweep',
status: 'development',
needsReview: false, needsReview: false,
args: { args: {
target: { target: {
@ -317,8 +316,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
required: true, required: true,
skip: true, skip: true,
multiple: false, multiple: false,
warningMessage:
'The sweep workflow is new and under tested. Please break it and report issues.',
}, },
trajectory: { trajectory: {
inputType: 'selection', inputType: 'selection',
@ -368,7 +365,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
Revolve: { Revolve: {
description: 'Create a 3D body by rotating a sketch region about an axis.', description: 'Create a 3D body by rotating a sketch region about an axis.',
icon: 'revolve', icon: 'revolve',
status: 'development',
needsReview: true, needsReview: true,
args: { args: {
selection: { selection: {
@ -377,8 +373,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
multiple: false, // TODO: multiple selection multiple: false, // TODO: multiple selection
required: true, required: true,
skip: true, skip: true,
warningMessage:
'The revolve workflow is new and under tested. Please break it and report issues.',
}, },
axisOrEdge: { axisOrEdge: {
inputType: 'options', inputType: 'options',

View File

@ -0,0 +1,19 @@
import { parseEngineErrorMessage } from './validators'
describe('parseEngineErrorMessage', () => {
it('takes an engine error string and parses its json message', () => {
const engineError =
'engine error: [{"error_code":"internal_engine","message":"Trajectory curve must be G1 continuous (with continuous tangents)"}]'
const message = parseEngineErrorMessage(engineError)
expect(message).toEqual(
'Trajectory curve must be G1 continuous (with continuous tangents)'
)
})
it('retuns undefined on strings with different formats', () => {
const s1 = 'engine error: []'
const s2 = 'blabla'
expect(parseEngineErrorMessage(s1)).toBeUndefined()
expect(parseEngineErrorMessage(s2)).toBeUndefined()
})
})

View File

@ -3,6 +3,7 @@ import { engineCommandManager } from 'lib/singletons'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { CommandBarContext } from 'machines/commandBarMachine' import { CommandBarContext } from 'machines/commandBarMachine'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { ApiError_type } from '@kittycad/lib/dist/types/src/models'
export const disableDryRunWithRetry = async (numberOfRetries = 3) => { export const disableDryRunWithRetry = async (numberOfRetries = 3) => {
for (let tries = 0; tries < numberOfRetries; tries++) { for (let tries = 0; tries < numberOfRetries; tries++) {
@ -46,6 +47,20 @@ function isSelections(selections: unknown): selections is Selections {
) )
} }
export function parseEngineErrorMessage(engineError: string) {
const parts = engineError.split('engine error: ')
if (parts.length < 2) {
return undefined
}
const errors = JSON.parse(parts[1]) as ApiError_type[]
if (!errors[0]) {
return undefined
}
return errors[0].message
}
export const revolveAxisValidator = async ({ export const revolveAxisValidator = async ({
data, data,
context, context,
@ -83,7 +98,7 @@ export const revolveAxisValidator = async ({
value: 360, value: 360,
} }
const revolveAboutEdgeCommand = async () => { const command = async () => {
return await engineCommandManager.sendSceneCommand({ return await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),
@ -92,17 +107,18 @@ export const revolveAxisValidator = async ({
angle: angleInDegrees, angle: angleInDegrees,
edge_id: edgeSelection, edge_id: edgeSelection,
target: sketchSelection, target: sketchSelection,
tolerance: 0.0001, // Gotcha: Playwright will fail with larger tolerances, need to use a smaller one.
tolerance: 1e-7,
}, },
}) })
} }
const attemptRevolve = await dryRunWrapper(revolveAboutEdgeCommand) const result = await dryRunWrapper(command)
if (attemptRevolve?.success) { if (result?.success) {
return true return true
} else {
// return error message for the toast
return 'Unable to revolve with selected edge'
} }
const reason = parseEngineErrorMessage(result) || 'unknown'
return `Unable to revolve with the current selection. Reason: ${reason}`
} }
export const loftValidator = async ({ export const loftValidator = async ({
@ -128,7 +144,7 @@ export const loftValidator = async ({
return 'Unable to loft, selection contains less than two solid2ds' return 'Unable to loft, selection contains less than two solid2ds'
} }
const loftCommand = async () => { const command = async () => {
// TODO: check what to do with these // TODO: check what to do with these
const DEFAULT_V_DEGREE = 2 const DEFAULT_V_DEGREE = 2
const DEFAULT_TOLERANCE = 2 const DEFAULT_TOLERANCE = 2
@ -145,13 +161,13 @@ export const loftValidator = async ({
}, },
}) })
} }
const attempt = await dryRunWrapper(loftCommand) const result = await dryRunWrapper(command)
if (attempt?.success) { if (result?.success) {
return true return true
} else {
// return error message for the toast
return 'Unable to loft with selected sketches'
} }
const reason = parseEngineErrorMessage(result) || 'unknown'
return `Unable to loft with the current selection. Reason: ${reason}`
} }
export const shellValidator = async ({ export const shellValidator = async ({
@ -180,7 +196,7 @@ export const shellValidator = async ({
return "Unable to shell, couldn't find the solid" return "Unable to shell, couldn't find the solid"
} }
const shellCommand = async () => { const command = async () => {
// TODO: figure out something better than an arbitrarily small value // TODO: figure out something better than an arbitrarily small value
const DEFAULT_THICKNESS: Models['LengthUnit_type'] = 1e-9 const DEFAULT_THICKNESS: Models['LengthUnit_type'] = 1e-9
const DEFAULT_HOLLOW = false const DEFAULT_HOLLOW = false
@ -200,12 +216,13 @@ export const shellValidator = async ({
}) })
} }
const attemptShell = await dryRunWrapper(shellCommand) const result = await dryRunWrapper(command)
if (attemptShell?.success) { if (result?.success) {
return true return true
} }
return 'Unable to shell with the provided selection' const reason = parseEngineErrorMessage(result) || 'unknown'
return `Unable to shell with the current selection. Reason: ${reason}`
} }
export const sweepValidator = async ({ export const sweepValidator = async ({
@ -241,7 +258,7 @@ export const sweepValidator = async ({
} }
const target = targetArtifact.pathId const target = targetArtifact.pathId
const sweepCommand = async () => { const command = async () => {
// TODO: second look on defaults here // TODO: second look on defaults here
const DEFAULT_TOLERANCE: Models['LengthUnit_type'] = 1e-7 const DEFAULT_TOLERANCE: Models['LengthUnit_type'] = 1e-7
const DEFAULT_SECTIONAL = false const DEFAULT_SECTIONAL = false
@ -261,10 +278,11 @@ export const sweepValidator = async ({
}) })
} }
const attemptSweep = await dryRunWrapper(sweepCommand) const result = await dryRunWrapper(command)
if (attemptSweep?.success) { if (result?.success) {
return true return true
} }
return 'Unable to sweep with the provided selection' const reason = parseEngineErrorMessage(result) || 'unknown'
return `Unable to sweep with the current selection. Reason: ${reason}`
} }

View File

@ -26,7 +26,7 @@ export const FILE_EXT = '.kcl'
/** Default file to open when a project is opened */ /** Default file to open when a project is opened */
export const PROJECT_ENTRYPOINT = `main${FILE_EXT}` as const export const PROJECT_ENTRYPOINT = `main${FILE_EXT}` as const
/** Thumbnail file name */ /** Thumbnail file name */
export const PROJECT_IMAGE_NAME = `main.jpg` as const export const PROJECT_IMAGE_NAME = `thumbnail.png` as const
/** The localStorage key for last-opened projects */ /** The localStorage key for last-opened projects */
export const FILE_PERSIST_KEY = `${PROJECT_FOLDER}-last-opened` as const export const FILE_PERSIST_KEY = `${PROJECT_FOLDER}-last-opened` as const
/** The default name given to new kcl files in a project */ /** The default name given to new kcl files in a project */

View File

@ -10,6 +10,7 @@ import {
import { import {
PROJECT_ENTRYPOINT, PROJECT_ENTRYPOINT,
PROJECT_FOLDER, PROJECT_FOLDER,
PROJECT_IMAGE_NAME,
PROJECT_SETTINGS_FILE_NAME, PROJECT_SETTINGS_FILE_NAME,
SETTINGS_FILE_NAME, SETTINGS_FILE_NAME,
TELEMETRY_FILE_NAME, TELEMETRY_FILE_NAME,
@ -625,3 +626,19 @@ export const getUser = async (
} }
return Promise.reject(new Error('unreachable')) return Promise.reject(new Error('unreachable'))
} }
export const writeProjectThumbnailFile = async (
dataUrl: string,
projectDirectoryPath: string
) => {
const filePath = window.electron.path.join(
projectDirectoryPath,
PROJECT_IMAGE_NAME
)
const data = atob(dataUrl.substring('data:image/png;base64,'.length))
const asArray = new Uint8Array(data.length)
for (let i = 0, len = data.length; i < len; ++i) {
asArray[i] = data.charCodeAt(i)
}
return window.electron.writeFile(filePath, asArray)
}

View File

@ -1,13 +1,10 @@
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning' import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
import { Command, CommandArgumentOption } from './commandTypes' import { Command, CommandArgumentOption } from './commandTypes'
import { codeManager, kclManager } from './singletons' import { kclManager } from './singletons'
import { isDesktop } from './isDesktop' import { isDesktop } from './isDesktop'
import { FILE_EXT, PROJECT_SETTINGS_FILE_NAME } from './constants' import { FILE_EXT } from './constants'
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models' import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
import { parseProjectSettings } from 'lang/wasm' import { reportRejection } from './trap'
import { err, reportRejection } from './trap'
import { projectConfigurationToSettingsPayload } from './settings/settingsUtils'
import { copyFileShareLink } from './links'
import { IndexLoaderData } from './types' import { IndexLoaderData } from './types'
interface OnSubmitProps { interface OnSubmitProps {
@ -68,56 +65,23 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
const sampleCodeUrl = `https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/${encodeURIComponent( const sampleCodeUrl = `https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/${encodeURIComponent(
projectPathPart projectPathPart
)}/${encodeURIComponent(primaryKclFile)}` )}/${encodeURIComponent(primaryKclFile)}`
const sampleSettingsFileUrl = `https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/${encodeURIComponent(
projectPathPart
)}/${PROJECT_SETTINGS_FILE_NAME}`
Promise.allSettled([fetch(sampleCodeUrl), fetch(sampleSettingsFileUrl)]) fetch(sampleCodeUrl)
.then((results) => { .then(async (codeResponse): Promise<OnSubmitProps> => {
const a = if (!codeResponse.ok) {
'value' in results[0] ? results[0].value : results[0].reason console.error(
const b = 'Failed to fetch sample code:',
'value' in results[1] ? results[1].value : results[1].reason codeResponse.statusText
return [a, b] )
}) return Promise.reject(new Error('Failed to fetch sample code'))
.then(
async ([
codeResponse,
settingsResponse,
]): Promise<OnSubmitProps> => {
if (!codeResponse.ok) {
console.error(
'Failed to fetch sample code:',
codeResponse.statusText
)
return Promise.reject(new Error('Failed to fetch sample code'))
}
const code = await codeResponse.text()
// It's possible that a sample doesn't have a project.toml
// associated with it.
let projectSettingsPayload: ReturnType<
typeof projectConfigurationToSettingsPayload
> = {}
if (settingsResponse.ok) {
const parsedProjectSettings = parseProjectSettings(
await settingsResponse.text()
)
if (!err(parsedProjectSettings)) {
projectSettingsPayload =
projectConfigurationToSettingsPayload(parsedProjectSettings)
}
}
return {
sampleName: data.sample.split('/')[0] + FILE_EXT,
code,
method: data.method,
sampleUnits:
projectSettingsPayload.modeling?.defaultUnit || 'mm',
}
} }
) const code = await codeResponse.text()
return {
sampleName: data.sample.split('/')[0] + FILE_EXT,
code,
method: data.method,
}
})
.then((props) => { .then((props) => {
if (props?.code) { if (props?.code) {
commandProps.specialPropsForSampleCommand commandProps.specialPropsForSampleCommand

View File

@ -1,4 +1,4 @@
function takeScreenshotOfVideoStreamCanvas() { export function takeScreenshotOfVideoStreamCanvas() {
const canvas = document.querySelector('[data-engine]') const canvas = document.querySelector('[data-engine]')
const video = document.getElementById('video-stream') const video = document.getElementById('video-stream')
if ( if (

View File

@ -577,10 +577,9 @@ export function getSelectionTypeDisplayText(
.map( .map(
// Hack for showing "face" instead of "extrude-wall" in command bar text // Hack for showing "face" instead of "extrude-wall" in command bar text
([type, count]) => ([type, count]) =>
`${count} ${type `${count} ${type.replace('wall', 'face').replace('solid2d', 'face')}${
.replace('wall', 'face') count > 1 ? 's' : ''
.replace('solid2d', 'face') }`
.replace('segment', 'face')}${count > 1 ? 's' : ''}`
) )
.toArray() .toArray()
.join(', ') .join(', ')

View File

@ -103,7 +103,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
data: { name: 'Revolve', groupId: 'modeling' }, data: { name: 'Revolve', groupId: 'modeling' },
}), }),
icon: 'revolve', icon: 'revolve',
status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'kcl-only', status: 'available',
title: 'Revolve', title: 'Revolve',
hotkey: 'R', hotkey: 'R',
description: description:
@ -124,7 +124,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
data: { name: 'Sweep', groupId: 'modeling' }, data: { name: 'Sweep', groupId: 'modeling' },
}), }),
icon: 'sweep', icon: 'sweep',
status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'kcl-only', status: 'available',
title: 'Sweep', title: 'Sweep',
hotkey: 'W', hotkey: 'W',
description: description:

View File

@ -26,6 +26,7 @@ import {
default_project_settings as DefaultProjectSettings, default_project_settings as DefaultProjectSettings,
base64_decode as Base64Decode, base64_decode as Base64Decode,
clear_scene_and_bust_cache as ClearSceneAndBustCache, clear_scene_and_bust_cache as ClearSceneAndBustCache,
change_kcl_settings as ChangeKclSettings,
} from '../wasm-lib/pkg/wasm_lib' } from '../wasm-lib/pkg/wasm_lib'
type ModuleType = typeof import('../wasm-lib/pkg/wasm_lib') type ModuleType = typeof import('../wasm-lib/pkg/wasm_lib')
@ -110,3 +111,6 @@ export const clear_scene_and_bust_cache: typeof ClearSceneAndBustCache = (
) => { ) => {
return getModule().clear_scene_and_bust_cache(...args) return getModule().clear_scene_and_bust_cache(...args)
} }
export const change_kcl_settings: typeof ChangeKclSettings = (...args) => {
return getModule().change_kcl_settings(...args)
}

View File

@ -0,0 +1,30 @@
import { ActorRefFrom, createActor, setup } from 'xstate'
import { authMachine } from './authMachine'
import { useSelector } from '@xstate/react'
import { ACTOR_IDS } from './machineConstants'
const appMachine = setup({
actors: {
[ACTOR_IDS.AUTH]: authMachine,
},
}).createMachine({
/** @xstate-layout N4IgpgJg5mDOIC5gF8A0IB2B7CdGgAoBbAQwGMALASwzAEp8QAHLWKgFyqw0YA9EAjACZ0AT0FDkU5EA */
id: 'modeling-app',
invoke: [
{
src: ACTOR_IDS.AUTH,
systemId: ACTOR_IDS.AUTH,
},
],
})
export const appActor = createActor(appMachine).start()
export const authActor = appActor.system.get(ACTOR_IDS.AUTH) as ActorRefFrom<
typeof authMachine
>
export const useAuthState = () => useSelector(authActor, (state) => state)
export const useToken = () =>
useSelector(authActor, (state) => state.context.token)
export const useUser = () =>
useSelector(authActor, (state) => state.context.user)

View File

@ -15,6 +15,8 @@ import {
} from 'lib/desktop' } from 'lib/desktop'
import { COOKIE_NAME } from 'lib/constants' import { COOKIE_NAME } from 'lib/constants'
import { markOnce } from 'lib/performance' import { markOnce } from 'lib/performance'
import { ACTOR_IDS } from './machineConstants'
import withBaseUrl from '../lib/withBaseURL'
const SKIP_AUTH = VITE_KC_SKIP_AUTH === 'true' && DEV const SKIP_AUTH = VITE_KC_SKIP_AUTH === 'true' && DEV
@ -50,7 +52,7 @@ export type Events =
} }
export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY' export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
const persistedToken = export const persistedToken =
VITE_KC_DEV_TOKEN || VITE_KC_DEV_TOKEN ||
getCookie(COOKIE_NAME) || getCookie(COOKIE_NAME) ||
localStorage?.getItem(TOKEN_PERSIST_KEY) || localStorage?.getItem(TOKEN_PERSIST_KEY) ||
@ -69,18 +71,17 @@ export const authMachine = setup({
} }
} }
}, },
actions: {
goToIndexPage: () => {},
goToSignInPage: () => {},
},
actors: { actors: {
getUser: fromPromise(({ input }: { input: { token?: string } }) => getUser: fromPromise(({ input }: { input: { token?: string } }) =>
getUser(input) getUser(input)
), ),
logout: fromPromise(async () =>
isDesktop() ? writeTokenFile('') : logout()
),
}, },
}).createMachine({ }).createMachine({
/** @xstate-layout N4IgpgJg5mDOIC5QEECuAXAFgOgMabFwGsBJAMwBkB7KGCEgOwGIIqGxsBLBgNyqI75CRALQAbGnRHcA2gAYAuolAAHKrE7pObZSAAeiAIwAWQ9gBspuQCYAnAGYAHPYCsx+4ccAaEAE9E1q7YcoZyxrYR1m7mcrYAvnE+aFh4BMTk1LSQjExgAE55VHnYKmIAhuhkRQC2qcLikpDSDPJKSCBqGlo67QYI9gDs5tge5o6h5vau7oY+-v3mA9jWco4u5iu21ua2YcYJSRg4Eln0zJkABFQYrbqdmtoMun2GA7YjxuPmLqvGNh5zRCfJaOcyLUzuAYuFyGcwHEDJY6NCAAeQwTEuskUd3UDx6oD6Im2wUcAzkMJ2cjBxlMgIWLmwZLWljecjJTjh8IYVAgcF0iJxXUez0QIgGxhJZIpu2ptL8AWwtje1nCW2iq1shns8MRdXSlGRjEFeKevUQjkcy3sqwGHimbg83nlCF22GMytVUWMMUc8USCKO2BOdCN7Xu3VNBKMKsVFp2hm2vu+1id83slkVrgTxhcW0pNJ1geDkDR6GNEZFCAT1kZZLk9cMLltb0WdPMjewjjC1mzOZCtk5CSAA */ /** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAOhzEwGsBJAMwBkB7KGCa-AYgkcJIIDdGlMGWwVKAWgA2zVhIIBtAAwBdRKAAOjWLgAuuHupAAPRAGYArAEYSADgu2AnGYBMLpVYBsZz7YA0IACeiG6OJM62tmZKLgDsno5KtvEAvikBaFh4hKTkVHRMLJDsHGAATmWMZSQaUui6tFWoouLSspDy+MpqSCBaOvqGvaYIljb2Tq7uXj7+QYgALFYW4clWy1ZmVgsWsZtpGRg4BMQkMkVsnIUABIwArrrdRv16BvhGI74LJBYW7o5WKJmKILObBUZeEgJP4LTxKMwIhZmBYLA4gTLHHJnWQEKAAeQeXB4IgEQhEGOyp3OUFxBN0CFJmHqb26T16L0G72GiCsSg8PyszkBCViTiUjgC4Jcnhc4SUsQcvgsoL2VjRFJOpGptMJ5Uq1Vq9UaZWaGqx2vw+IeDPwgiZnNZqme2leQ1An1s31+-0BCJBYJCLm+lk8CRl9hRyos6qOlK17QgdI4N0UTvZLs5Hx58NsJARuys0tDSl+AYQthsgNi0TMqt2LjVaPwjAgcCMZuIzoGbyzCAknkliH7Maympa+QYCfYXddXPdixcg4QvKUdk2u2iLkcsXhCRHmKpU7nfQzPe5CAsMpIXi8MvFKM8VliS5c1jzj53W3isNFqPS6NjMcLStXQZ0zc8ohsJI-kcFxXEcR9HAWF9gTzDxbCUXxAQWEsdn3ONsQuOkwLPedl22MIzFg3YP1gl9PG+bYvGsSxlUcRJozSFIgA */
id: 'Auth', id: ACTOR_IDS.AUTH,
initial: 'checkIfLoggedIn', initial: 'checkIfLoggedIn',
context: { context: {
token: persistedToken, token: persistedToken,
@ -112,19 +113,30 @@ export const authMachine = setup({
}, },
}, },
loggedIn: { loggedIn: {
entry: ['goToIndexPage'],
on: { on: {
'Log out': { 'Log out': {
target: 'loggedOut', target: 'loggingOut',
actions: () => { },
// eslint-disable-next-line @typescript-eslint/no-floating-promises },
if (isDesktop()) writeTokenFile('') },
}, loggingOut: {
invoke: {
src: 'logout',
onDone: 'loggedOut',
onError: {
target: 'loggedIn',
actions: [
({ event }) => {
console.error(
'Error while logging out',
'error' in event ? `: ${event.error}` : ''
)
},
],
}, },
}, },
}, },
loggedOut: { loggedOut: {
entry: ['goToSignInPage'],
on: { on: {
'Log in': { 'Log in': {
target: 'checkIfLoggedIn', target: 'checkIfLoggedIn',
@ -235,3 +247,12 @@ async function getAndSyncStoredToken(input: {
localStorage.setItem(TOKEN_PERSIST_KEY, fileToken) localStorage.setItem(TOKEN_PERSIST_KEY, fileToken)
return fileToken return fileToken
} }
async function logout() {
localStorage.removeItem(TOKEN_PERSIST_KEY)
if (isDesktop()) return Promise.resolve(null)
return fetch(withBaseUrl('/logout'), {
method: 'POST',
credentials: 'include',
})
}

View File

@ -10,6 +10,7 @@ import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
import { MachineManager } from 'components/MachineManagerProvider' import { MachineManager } from 'components/MachineManagerProvider'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { useSelector } from '@xstate/react' import { useSelector } from '@xstate/react'
import { authCommands } from 'lib/commandBarConfigs/authCommandConfig'
export type CommandBarContext = { export type CommandBarContext = {
commands: Command[] commands: Command[]
@ -80,6 +81,7 @@ export type CommandBarMachineEvent =
export const commandBarMachine = setup({ export const commandBarMachine = setup({
types: { types: {
context: {} as CommandBarContext, context: {} as CommandBarContext,
input: {} as { commands: Command[] },
events: {} as CommandBarMachineEvent, events: {} as CommandBarMachineEvent,
}, },
actions: { actions: {
@ -409,8 +411,8 @@ export const commandBarMachine = setup({
}, },
}).createMachine({ }).createMachine({
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAIwB2AHTiAHAE45AZjkAmdcoaSArCoA0IAJ6JxDOdJ2SF4gCySHaqZIa2Avm8NpMuAsXJUYKSMLEggHLA8fAJhIgiiOgBssokKTpJqiXK2OsqJaoYm8eJqytKJ9hqq+eJW2h5eGNh4RKRkAGKcLb74tJRgAMbc+ANNviGCEVH8gnEKubK5ympVrrlyhabm0rZ5CtYM4hVq83ININ7NfqTSVDSQZADybGA4E2FTvDOxiGoMDNJMjo9H9lLYGDpKpsEModOJpA5XOIwakwcidOdLj1-LdqLQIGQAIIQAijHx4WDvdhcL4xUBxURqSTw3LzfYKZSSOQZWzQ5S1crMuTAiElRKuTFjFo4u74sgAJTA6FQADcwCMpRBKcxJjTorMxEzkhouftuXCGGpIdCpADmZJxfzJLY5Cp5pLydcSLj7gSqeE9d96Yh+TppKoXfkGKdlHloUyFIDUvMXBzbFl3J4LprWt6AMpgfpDLpQDWesgFovDMlXf2ffU-BBZZKuczWWylRRwvlM6Q2LIMZQ2QdHZQeq65245vqDbgPOuBunCEP88MqPQchwVNLKaHptTSYUuRI6f4npyJcfYm5Ylozobz8ShamRWkGhBmeTSQeJX+JXQ6H88jQnCMiugoELCpypQKJeWa3l6U6er0hazvOajPgGr4NsGH6WhYsHOkycglLCEJ7iclgaIosKSLGGgKFe0o3AA4lg3AABZgCQJb4KQUAAK7oK83CwBQHG4DAIwCSQJAiXxJCCcJODcAu2FBsuH55GGJ7irYHYqHpGzGKYWiWB2nK2Kcv6wWO8E5jibGcdxvH8UJIliQAInAqFDGWtY6h8i7vlkZREbYZg6JuwEmQgfxhnkHbiHYJ7yHYTGIU5XE8TgpZucponSISADuWBRLl+BdGwAncBWAkAEboDwClKSJanTEucS1Bk36DicDCpOYLoKHu-x9tGuhgoozKpBlk5ZS5FX5R50gAGpYJQnAQOxJZkBA-BgNIXQqqgADWh0qhtW3sWAeYlv0hKKe5KntW+YRFGi5RyOKDrZEaUW8pppTguGuwDRFEO-nNjnsdlrlPQVsBrVd228LlZDcSQqDemwlDsQAZtj6DSJdm2o7d91gI9rUvYFL4dYIH0RV9P0Zv9CiA3EwMAmCWgVHYKVwY0yE4oqKqcGAxV1Y1zU1uMdNYQzjbMpYcgQlFxFq66u6xXRALbkyg7-Bz0bQzcYsS1LxIEMttOYfWGldYcCWwQmNiRrU0Jq2GRxJCbHZqC6ahm96FuSwqSqquqtuqQrDudVs4iJhUyZtn9qjQokYKHjk6hSLsZhciH0hh1LACiEDNTHr04ZpJT6bIEXxcKp50YDRT-mGrrJbUgFDp3xfIFxAynbx1PPaJe0HUdOAnedJMozd4+IzXjuIJCLIQqUWjJS4MVFBByS7BzyJq38WTB-ZIs3sPo8VcvHlTzgh3HWdF2L3OD8qZST66upCdxUTLBJOUUHB6GFPpSQXt1CWEGpaOiv4EyD1vmPBGj9MbY2kLjAmRMF5kyXmg7+q8AFJwbjkXQEVsj5FyO3RAiQjjfi5CReYPck7iA8FmHAqAIBwEEAhXMf8la4VEMsWwgJuTTXNGYK0tC4rwkDvsAaSQ-qlAhIPPEkBBFvWEVycoFQhywUHGaHWH04Q7FUNBdc+x2FXwnDiSss5eJyzwFo2ucQIplBWHrcEVhuoFFirCeEuxsj8mWEOFYmZhZ2JvNOXyc4ICuLXggaxCJwG70AiUHQfIzHBKHGYDMJFhTFwWjlPKhDRKJJIRFGQcIFBsiOIHJw8ZIQ7CUGrY+9g9AnmKbDRaZSaaFRKmVNGpYqo1Uqe+FKYZan1PyElbJYjrB6C5CUJQ9DL5ROvN6Ep8MBlI3WvgkZEzGzyAbnkZK-wjan38UzeEzZtBswdADYupdjm4XoTIOwuwHB5HyGkYyRRdBBLosCIE6ZvpnFsVs24KD77lPgEFf+kzxRH32EyFYlpOzQjAZYV2ehTkfI4W4IAA */ /** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAIwB2AHTiAHAE45AZjkAmdcoaSArCoA0IAJ6JxDOdJ2SF4gCySHaqZIa2Avm8NpMuAsXJUYKSMLEggHLA8fAJhIgiiOgBssokKTpJqiXK2OsqJaoYm8eJqytKJ9hqq+eJW2h5eGNh4RKRkAGKcLb74tJRgAMbc+ANNviGCEVH8gnEKubK5ympVrrlyhabm0rZ5CtYM4hVq83ININ7NfqTSVDSQZADybGA4E2FTvDOxiGoMDNJMjo9H9lLYGDpKpsEModOJpA5XOIwakwcidOdLj1-LdqLQIGQAIIQAijHx4WDvdhcL4xUBxURqSTw3LzfYKZSSOQZWzQ5S1crMuTAiElRKuTFjFo4u74sgAJTA6FQADcwCMpRBKcxJjTorMxEzkhouftuXCGGpIdCpADmZJxfzJLY5Cp5pLydcSLj7gSqeE9d96Yh+TppKoXfkGKdlHloUyFIDUvMXBzbFl3J4LprWt6AMpgfpDLpQDWesgFovDMlXf2ffU-BBZZKuczWWylRRwvlM6Q2LIMZQ2QdHZQeq65245vqDbgPOuBunCEP88MqPQchwVNLKaHptTSYUuRI6f4npyJcfYm5Ylozobz8ShamRWkGhBmeTSQeJX+JXQ6H88jQnCMiugoELCpypQKJeWa3l6U6er0hazvOajPgGr4NsGH6WhYsHOkycglLCEJ7iclgaIosKSLGGgKFe0o3AA4lg3AABZgCQJb4KQUAAK7oK83CwBQHG4DAIwCSQJAiXxJCCcJODcAu2FBsuH55GGJ7irYHYqHpGzGKYWiWB2nK2Kcv6wWO8E5jibGcdxvH8UJIliQAInAqFDGWtY6h8i7vlkZREbYZg6JuwEmQgfxhnkHbiHYJ7yHYTGIU5XE8TgpZucponSISADuWBRLl+BdGwAncBWAkAEboDwClKSJanTEucS1Bk36DicDCpOYLoKHu-x9tGuhgoozKpBlk5ZS5FX5R50gAGpYJQnAQOxJZkBA-BgNIXQqqgADWh0qhtW3sWAeYlv0hKKe5KntW+YRFGi5RyOKDrZEaUW8pppTguGuwDRFEO-nNjnsdlrlPQVsBrVd228LlZDcSQqDemwlDsQAZtj6DSJdm2o7d91gI9rUvYFL4dYIH0RV9P0Zv9CiA3EwMAmCWgVHYKVwY0yE4oqKqcGAxV1Y1zU1uMdNYQzjbMpYcgQlFxFq66u6xXRALbkyg7-Bz0bQzcYsS1LxIEMttOYfWGldYcCWwQmNiRrU0Jq2GRxJCbHZqC6ahm96FuSwqSqquqtuqQrDudVs4iJhUyZtn9qjQokYKHjk6hSLsZhciH0hh1LACiEDNTHr04ZpJT6bIEXxcKp50YDRT-mGrrJbUgFDp3xfIFxAynbx1PPaJe0HUdOAnedJMozd4+IzXjuIJCLIQqUWjJS4MVFBByS7BzyJq38WTB-ZIs3sPo8VcvHlTzgh3HWdF2L3OD8qZST66upCdxUTLBJOUUHB6GFPpSQXt1CWEGpaOiv4EyD1vmPBGj9MbY2kLjAmRMF5kyXmg7+q8AFJwbjkXQEVsj5FyO3RAiQjjfi5CReYPck7iA8FmHAqAIBwEEAhXMf8la4VEMsWwgJuTTXNGYK0tC4rwkDvsAaSQ-qlAhIPPEkBBFvWEVycoFQhywUHGaHWH04Q7FUNBdc+x2FXwnDiSss5eJyzwFo2ucQIplBWHrcEVhuoFFirCeEuxsj8mWEOFYmZhZ2JvNOXyc4ICuLXggaxCJwG70AiUHQfIzHBKHGYDMJFhTFwWjlPKhDRKJJIRFGQcIFBsiOIHJw8ZIQ7CUGrY+9g9AnmKbDRaZSaaFRKmVNGpYqo1Uqe+FKYZan1PyElbJYjrB6C5CUJQ9DL5ROvN6Ep8MBlI3WvgkZEzGzyAbnkZK-wjan38UzeEzZtBswdADYupdjm4XoTIOwuwHB5HyGkYyRRdBBLosCIE6ZvpnFsVs24KD77lPgEFf+kzxRH32EyFYlpOzQjAZYV2ehTkfI4W4IAA */
context: { context: ({ input }) => ({
commands: [], commands: input.commands || [],
selectedCommand: undefined, selectedCommand: undefined,
currentArgument: undefined, currentArgument: undefined,
selectionRanges: { selectionRanges: {
@ -425,7 +427,7 @@ export const commandBarMachine = setup({
setCurrentMachine: () => {}, setCurrentMachine: () => {},
noMachinesReason: () => undefined, noMachinesReason: () => undefined,
}, },
}, }),
id: 'Command Bar', id: 'Command Bar',
initial: 'Closed', initial: 'Closed',
states: { states: {
@ -631,7 +633,11 @@ function sortCommands(a: Command, b: Command) {
return a.name.localeCompare(b.name) return a.name.localeCompare(b.name)
} }
export const commandBarActor = createActor(commandBarMachine).start() export const commandBarActor = createActor(commandBarMachine, {
input: {
commands: [...authCommands],
},
}).start()
/** Basic state snapshot selector */ /** Basic state snapshot selector */
const cmdBarStateSelector = (state: SnapshotFrom<typeof commandBarActor>) => const cmdBarStateSelector = (state: SnapshotFrom<typeof commandBarActor>) =>

View File

@ -0,0 +1,3 @@
export const ACTOR_IDS = {
AUTH: 'auth',
}

View File

@ -1452,13 +1452,14 @@ export const modelingMachine = setup({
unknown, unknown,
ModelingCommandSchema['Extrude'] | undefined ModelingCommandSchema['Extrude'] | undefined
>(async ({ input }) => { >(async ({ input }) => {
if (!input) return new Error('No input provided') if (!input) return Promise.reject('No input provided')
const { selection, distance } = input const { selection, distance } = input
let ast = structuredClone(kclManager.ast) let ast = structuredClone(kclManager.ast)
let extrudeName: string | undefined = undefined
const sourceRange = selection.graphSelections[0]?.codeRef.range const pathToNode = getNodePathFromSourceRange(
const pathToNode = getNodePathFromSourceRange(ast, sourceRange) ast,
selection.graphSelections[0]?.codeRef.range
)
// Add an extrude statement to the AST // Add an extrude statement to the AST
const extrudeSketchRes = extrudeSketch( const extrudeSketchRes = extrudeSketch(
ast, ast,
@ -1468,7 +1469,7 @@ export const modelingMachine = setup({
? distance.variableIdentifierAst ? distance.variableIdentifierAst
: distance.valueAst : distance.valueAst
) )
if (err(extrudeSketchRes)) return extrudeSketchRes if (err(extrudeSketchRes)) return Promise.reject(extrudeSketchRes)
const { modifiedAst, pathToExtrudeArg } = extrudeSketchRes const { modifiedAst, pathToExtrudeArg } = extrudeSketchRes
// Insert the distance variable if the user has provided a variable name // Insert the distance variable if the user has provided a variable name
@ -2559,7 +2560,7 @@ export const modelingMachine = setup({
'Delete segment': { 'Delete segment': {
reenter: false, reenter: false,
actions: ['Delete segment', 'Set sketchDetails'], actions: ['Delete segment', 'Set sketchDetails', 'reset selections'],
}, },
'code edit during sketch': '.clean slate', 'code edit during sketch': '.clean slate',
}, },

View File

@ -1,15 +1,14 @@
import { OnboardingButtons, useDismiss, useNextClick } from '.' import { OnboardingButtons, useDismiss, useNextClick } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useUser } from 'machines/appMachine'
export default function UserMenu() { export default function UserMenu() {
const { auth } = useSettingsAuthContext() const user = useUser()
const dismiss = useDismiss() const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.PROJECT_MENU) const next = useNextClick(onboardingPaths.PROJECT_MENU)
const [avatarErrored, setAvatarErrored] = useState(false) const [avatarErrored, setAvatarErrored] = useState(false)
const user = auth?.context?.user
const errorOrNoImage = !user?.image || avatarErrored const errorOrNoImage = !user?.image || avatarErrored
const buttonDescription = errorOrNoImage ? 'the menu button' : 'your avatar' const buttonDescription = errorOrNoImage ? 'the menu button' : 'your avatar'

View File

@ -14,6 +14,7 @@ import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { toSync } from 'lib/utils' import { toSync } from 'lib/utils'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { authActor } from 'machines/appMachine'
const subtleBorder = const subtleBorder =
'border border-solid border-chalkboard-30 dark:border-chalkboard-80' 'border border-solid border-chalkboard-30 dark:border-chalkboard-80'
@ -22,7 +23,6 @@ const cardArea = `${subtleBorder} rounded-lg px-6 py-3 text-chalkboard-70 dark:t
const SignIn = () => { const SignIn = () => {
const [userCode, setUserCode] = useState('') const [userCode, setUserCode] = useState('')
const { const {
auth: { send },
settings: { settings: {
state: { state: {
context: { context: {
@ -70,7 +70,7 @@ const SignIn = () => {
toast.error('Error while trying to log in') toast.error('Error while trying to log in')
return return
} }
send({ type: 'Log in', token }) authActor.send({ type: 'Log in', token })
} }
return ( return (

View File

@ -1710,7 +1710,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.2.32" version = "0.2.33"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx 0.5.1", "approx 0.5.1",

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-lib" name = "kcl-lib"
description = "KittyCAD Language implementation and tools" description = "KittyCAD Language implementation and tools"
version = "0.2.32" version = "0.2.33"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -13,9 +13,7 @@ use tower_lsp::lsp_types::{
MarkupKind, ParameterInformation, ParameterLabel, SignatureHelp, SignatureInformation, MarkupKind, ParameterInformation, ParameterLabel, SignatureHelp, SignatureInformation,
}; };
use crate::execution::Sketch; use crate::{execution::Sketch, std::Primitive};
use crate::std::Primitive;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
#[ts(export)] #[ts(export)]

View File

@ -7,9 +7,9 @@ use crate::{
KclError, SourceRange, KclError, SourceRange,
}; };
pub(super) const SETTINGS: &str = "settings"; pub(crate) const SETTINGS: &str = "settings";
pub(super) const SETTINGS_UNIT_LENGTH: &str = "defaultLengthUnit"; pub(crate) const SETTINGS_UNIT_LENGTH: &str = "defaultLengthUnit";
pub(super) const SETTINGS_UNIT_ANGLE: &str = "defaultAngleUnit"; pub(crate) const SETTINGS_UNIT_ANGLE: &str = "defaultAngleUnit";
#[derive(Copy, Clone, Debug, Eq, PartialEq)] #[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(super) enum AnnotationScope { pub(super) enum AnnotationScope {

View File

@ -2,6 +2,7 @@ use std::collections::HashMap;
use async_recursion::async_recursion; use async_recursion::async_recursion;
use super::cad_op::{OpArg, Operation};
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{ execution::{
@ -19,8 +20,6 @@ use crate::{
}, },
}; };
use super::cad_op::{OpArg, Operation};
impl BinaryPart { impl BinaryPart {
#[async_recursion] #[async_recursion]
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> { pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {

View File

@ -15,6 +15,7 @@ use kittycad_modeling_cmds as kcmc;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use super::ExecutorContext;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{ExecState, ImportedGeometry}, execution::{ExecState, ImportedGeometry},
@ -22,8 +23,6 @@ use crate::{
source_range::SourceRange, source_range::SourceRange,
}; };
use super::ExecutorContext;
// Zoo co-ordinate system. // Zoo co-ordinate system.
// //
// * Forward: -Y // * Forward: -Y

View File

@ -581,10 +581,11 @@ impl KclValue {
} }
// TODO called UnitLen so as not to clash with UnitLength in settings) // TODO called UnitLen so as not to clash with UnitLength in settings)
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)] #[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)]
#[ts(export)] #[ts(export)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum UnitLen { pub enum UnitLen {
#[default]
Mm, Mm,
Cm, Cm,
M, M,
@ -593,6 +594,19 @@ pub enum UnitLen {
Yards, Yards,
} }
impl std::fmt::Display for UnitLen {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UnitLen::Mm => write!(f, "mm"),
UnitLen::Cm => write!(f, "cm"),
UnitLen::M => write!(f, "m"),
UnitLen::Inches => write!(f, "in"),
UnitLen::Feet => write!(f, "ft"),
UnitLen::Yards => write!(f, "yd"),
}
}
}
impl TryFrom<NumericSuffix> for UnitLen { impl TryFrom<NumericSuffix> for UnitLen {
type Error = (); type Error = ();
@ -644,6 +658,15 @@ pub enum UnitAngle {
Radians, Radians,
} }
impl std::fmt::Display for UnitAngle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UnitAngle::Degrees => write!(f, "deg"),
UnitAngle::Radians => write!(f, "rad"),
}
}
}
impl TryFrom<NumericSuffix> for UnitAngle { impl TryFrom<NumericSuffix> for UnitAngle {
type Error = (); type Error = ();

View File

@ -13,8 +13,7 @@ use kcmc::{
websocket::{ModelingSessionData, OkWebSocketResponseData}, websocket::{ModelingSessionData, OkWebSocketResponseData},
ImageFormat, ModelingCmd, ImageFormat, ModelingCmd,
}; };
use kittycad_modeling_cmds::length_unit::LengthUnit; use kittycad_modeling_cmds::{self as kcmc, length_unit::LengthUnit, websocket::WebSocketResponse};
use kittycad_modeling_cmds::{self as kcmc, websocket::WebSocketResponse};
use parse_display::{Display, FromStr}; use parse_display::{Display, FromStr};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -27,14 +26,18 @@ pub(crate) use import::{import_foreign, send_to_engine as send_import_to_engine,
pub use kcl_value::{KclObjectFields, KclValue, UnitAngle, UnitLen}; pub use kcl_value::{KclObjectFields, KclValue, UnitAngle, UnitLen};
use uuid::Uuid; use uuid::Uuid;
mod annotations; pub(crate) mod annotations;
mod artifact; mod artifact;
pub(crate) mod cache; pub(crate) mod cache;
mod cad_op; mod cad_op;
mod exec_ast; mod exec_ast;
mod function_param; mod function_param;
mod import; mod import;
mod kcl_value; pub(crate) mod kcl_value;
// Re-exports.
pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, ArtifactId};
pub use cad_op::Operation;
use crate::{ use crate::{
engine::{EngineManager, ExecutionKind}, engine::{EngineManager, ExecutionKind},
@ -52,10 +55,6 @@ use crate::{
ExecError, KclErrorWithOutputs, Program, ExecError, KclErrorWithOutputs, Program,
}; };
// Re-exports.
pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, ArtifactId};
pub use cad_op::Operation;
/// State for executing a program. /// State for executing a program.
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -247,7 +246,7 @@ impl ModuleState {
} }
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct MetaSettings { pub struct MetaSettings {
@ -256,7 +255,11 @@ pub struct MetaSettings {
} }
impl MetaSettings { impl MetaSettings {
fn update_from_annotation(&mut self, annotation: &NonCodeValue, source_range: SourceRange) -> Result<(), KclError> { pub fn update_from_annotation(
&mut self,
annotation: &NonCodeValue,
source_range: SourceRange,
) -> Result<(), KclError> {
let properties = annotations::expect_properties(annotations::SETTINGS, annotation, source_range)?; let properties = annotations::expect_properties(annotations::SETTINGS, annotation, source_range)?;
for p in properties { for p in properties {

View File

@ -84,7 +84,7 @@ pub use engine::{EngineManager, ExecutionKind};
pub use errors::{CompilationError, ConnectionError, ExecError, KclError, KclErrorWithOutputs}; pub use errors::{CompilationError, ConnectionError, ExecError, KclError, KclErrorWithOutputs};
pub use execution::{ pub use execution::{
cache::{CacheInformation, OldAstState}, cache::{CacheInformation, OldAstState},
ExecState, ExecutorContext, ExecutorSettings, Point2d, ExecState, ExecutorContext, ExecutorSettings, MetaSettings, Point2d,
}; };
pub use lsp::{ pub use lsp::{
copilot::Backend as CopilotLspBackend, copilot::Backend as CopilotLspBackend,
@ -121,8 +121,7 @@ pub mod std_utils {
} }
pub mod pretty { pub mod pretty {
pub use crate::parsing::token::NumericSuffix; pub use crate::{parsing::token::NumericSuffix, unparser::format_number};
pub use crate::unparser::format_number;
} }
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -162,6 +161,18 @@ impl Program {
self.ast.compute_digest() self.ast.compute_digest()
} }
/// Get the meta settings for the kcl file from the annotations.
pub fn get_meta_settings(&self) -> Result<Option<crate::MetaSettings>, KclError> {
self.ast.get_meta_settings()
}
/// Change the meta settings for the kcl file.
pub fn change_meta_settings(&mut self, settings: crate::MetaSettings) -> Result<Self, KclError> {
Ok(Self {
ast: self.ast.change_meta_settings(settings)?,
})
}
pub fn lint_all(&self) -> Result<Vec<lint::Discovered>, anyhow::Error> { pub fn lint_all(&self) -> Result<Vec<lint::Discovered>, anyhow::Error> {
self.ast.lint_all() self.ast.lint_all()
} }

View File

@ -17,7 +17,6 @@ use tower_lsp::lsp_types::{
CompletionItem, CompletionItemKind, DocumentSymbol, FoldingRange, FoldingRangeKind, Range as LspRange, SymbolKind, CompletionItem, CompletionItemKind, DocumentSymbol, FoldingRange, FoldingRangeKind, Range as LspRange, SymbolKind,
}; };
use super::digest::Digest;
pub use crate::parsing::ast::types::{ pub use crate::parsing::ast::types::{
condition::{ElseIf, IfExpression}, condition::{ElseIf, IfExpression},
literal_value::LiteralValue, literal_value::LiteralValue,
@ -26,7 +25,8 @@ pub use crate::parsing::ast::types::{
use crate::{ use crate::{
docs::StdLibFn, docs::StdLibFn,
errors::KclError, errors::KclError,
execution::{KclValue, Metadata, TagIdentifier}, execution::{annotations, KclValue, Metadata, TagIdentifier},
parsing::ast::digest::Digest,
parsing::PIPE_OPERATOR, parsing::PIPE_OPERATOR,
source_range::{ModuleId, SourceRange}, source_range::{ModuleId, SourceRange},
}; };
@ -254,6 +254,52 @@ impl Node<Program> {
} }
Ok(findings) Ok(findings)
} }
/// Get the annotations for the meta settings from the kcl file.
pub fn get_meta_settings(&self) -> Result<Option<crate::execution::MetaSettings>, KclError> {
let annotations = self
.non_code_meta
.start_nodes
.iter()
.filter_map(|n| n.annotation().map(|result| (result, n.as_source_range())));
for (annotation, source_range) in annotations {
if annotation.annotation_name() == Some(annotations::SETTINGS) {
let mut meta_settings = crate::execution::MetaSettings::default();
meta_settings.update_from_annotation(annotation, source_range)?;
return Ok(Some(meta_settings));
}
}
Ok(None)
}
pub fn change_meta_settings(&mut self, settings: crate::execution::MetaSettings) -> Result<Self, KclError> {
let mut new_program = self.clone();
let mut found = false;
for node in &mut new_program.non_code_meta.start_nodes {
if let Some(annotation) = node.annotation() {
if annotation.annotation_name() == Some(annotations::SETTINGS) {
let annotation = NonCodeValue::new_from_meta_settings(&settings);
*node = Node::no_src(NonCodeNode {
value: annotation,
digest: None,
});
found = true;
break;
}
}
}
if !found {
let annotation = NonCodeValue::new_from_meta_settings(&settings);
new_program.non_code_meta.start_nodes.push(Node::no_src(NonCodeNode {
value: annotation,
digest: None,
}));
}
Ok(new_program)
}
} }
impl Program { impl Program {
@ -1106,6 +1152,24 @@ impl NonCodeValue {
_ => None, _ => None,
} }
} }
pub fn new_from_meta_settings(settings: &crate::execution::MetaSettings) -> NonCodeValue {
let mut properties: Vec<Node<ObjectProperty>> = vec![ObjectProperty::new(
Identifier::new(annotations::SETTINGS_UNIT_LENGTH),
Expr::Identifier(Box::new(Identifier::new(&settings.default_length_units.to_string()))),
)];
if settings.default_angle_units != Default::default() {
properties.push(ObjectProperty::new(
Identifier::new(annotations::SETTINGS_UNIT_ANGLE),
Expr::Identifier(Box::new(Identifier::new(&settings.default_angle_units.to_string()))),
));
}
NonCodeValue::Annotation {
name: Identifier::new(annotations::SETTINGS),
properties: Some(properties),
}
}
} }
#[derive(Debug, Default, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Default, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
@ -2365,6 +2429,14 @@ impl Node<ObjectProperty> {
} }
impl ObjectProperty { impl ObjectProperty {
pub fn new(key: Node<Identifier>, value: Expr) -> Node<Self> {
Node::no_src(Self {
key,
value,
digest: None,
})
}
/// Returns a hover value that includes the given character position. /// Returns a hover value that includes the given character position.
pub fn get_hover_value_for_position(&self, pos: usize, code: &str) -> Option<Hover> { pub fn get_hover_value_for_position(&self, pos: usize, code: &str) -> Option<Hover> {
let value_source_range: SourceRange = self.value.clone().into(); let value_source_range: SourceRange = self.value.clone().into();
@ -3784,4 +3856,98 @@ const cylinder = startSketchOn('-XZ')
assert_eq!(l.raw, "false"); assert_eq!(l.raw, "false");
} }
#[tokio::test(flavor = "multi_thread")]
async fn test_parse_get_meta_settings_inch() {
let some_program_string = r#"@settings(defaultLengthUnit = inch)
startSketchOn('XY')"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let result = program.get_meta_settings().unwrap();
assert!(result.is_some());
let meta_settings = result.unwrap();
assert_eq!(
meta_settings.default_length_units,
crate::execution::kcl_value::UnitLen::Inches
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_parse_get_meta_settings_inch_to_mm() {
let some_program_string = r#"@settings(defaultLengthUnit = inch)
startSketchOn('XY')"#;
let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
let result = program.get_meta_settings().unwrap();
assert!(result.is_some());
let meta_settings = result.unwrap();
assert_eq!(
meta_settings.default_length_units,
crate::execution::kcl_value::UnitLen::Inches
);
// Edit the ast.
let new_program = program
.change_meta_settings(crate::execution::MetaSettings {
default_length_units: crate::execution::kcl_value::UnitLen::Mm,
..Default::default()
})
.unwrap();
let result = new_program.get_meta_settings().unwrap();
assert!(result.is_some());
let meta_settings = result.unwrap();
assert_eq!(
meta_settings.default_length_units,
crate::execution::kcl_value::UnitLen::Mm
);
let formatted = new_program.recast(&Default::default(), 0);
assert_eq!(
formatted,
r#"@settings(defaultLengthUnit = mm)
startSketchOn('XY')
"#
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_parse_get_meta_settings_nothing_to_mm() {
let some_program_string = r#"startSketchOn('XY')"#;
let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
let result = program.get_meta_settings().unwrap();
assert!(result.is_none());
// Edit the ast.
let new_program = program
.change_meta_settings(crate::execution::MetaSettings {
default_length_units: crate::execution::kcl_value::UnitLen::Mm,
..Default::default()
})
.unwrap();
let result = new_program.get_meta_settings().unwrap();
assert!(result.is_some());
let meta_settings = result.unwrap();
assert_eq!(
meta_settings.default_length_units,
crate::execution::kcl_value::UnitLen::Mm
);
let formatted = new_program.recast(&Default::default(), 0);
assert_eq!(
formatted,
r#"@settings(defaultLengthUnit = mm)
startSketchOn('XY')
"#
);
}
} }

View File

@ -4,6 +4,8 @@ use insta::rounded_redaction;
use crate::{ use crate::{
errors::KclError, errors::KclError,
exec::ArtifactCommand,
execution::{ArtifactGraph, Operation},
parsing::ast::types::{Node, Program}, parsing::ast::types::{Node, Program},
source_range::ModuleId, source_range::ModuleId,
}; };
@ -109,36 +111,12 @@ async fn execute(test_name: &str, render_to_png: bool) {
".environments[].**[].z[]" => rounded_redaction(4), ".environments[].**[].z[]" => rounded_redaction(4),
}); });
}); });
assert_snapshot(test_name, "Operations executed", || { assert_common_snapshots(
insta::assert_json_snapshot!("ops", exec_state.mod_local.operations); test_name,
}); exec_state.mod_local.operations,
assert_snapshot(test_name, "Artifact commands", || { exec_state.global.artifact_commands,
insta::assert_json_snapshot!("artifact_commands", exec_state.global.artifact_commands, { exec_state.global.artifact_graph,
"[].command.segment.*.x" => rounded_redaction(4), );
"[].command.segment.*.y" => rounded_redaction(4),
"[].command.segment.*.z" => rounded_redaction(4),
});
});
assert_snapshot(test_name, "Artifact graph flowchart", || {
let flowchart = exec_state
.global
.artifact_graph
.to_mermaid_flowchart()
.unwrap_or_else(|e| format!("Failed to convert artifact graph to flowchart: {e}"));
// Change the snapshot suffix so that it is rendered as a
// Markdown file in GitHub.
insta::assert_binary_snapshot!("artifact_graph_flowchart.md", flowchart.as_bytes().to_owned());
});
assert_snapshot(test_name, "Artifact graph mind map", || {
let mind_map = exec_state
.global
.artifact_graph
.to_mermaid_mind_map()
.unwrap_or_else(|e| format!("Failed to convert artifact graph to mind map: {e}"));
// Change the snapshot suffix so that it is rendered as a
// Markdown file in GitHub.
insta::assert_binary_snapshot!("artifact_graph_mind_map.md", mind_map.as_bytes().to_owned());
});
} }
Err(e) => { Err(e) => {
let ok_path_str = format!("tests/{test_name}/program_memory.snap"); let ok_path_str = format!("tests/{test_name}/program_memory.snap");
@ -165,17 +143,12 @@ async fn execute(test_name: &str, render_to_png: bool) {
insta::assert_snapshot!("execution_error", report); insta::assert_snapshot!("execution_error", report);
}); });
assert_snapshot(test_name, "Operations executed", || { assert_common_snapshots(
insta::assert_json_snapshot!("ops", error.operations); test_name,
}); error.operations,
error.artifact_commands,
assert_snapshot(test_name, "Artifact commands", || { error.artifact_graph,
insta::assert_json_snapshot!("artifact_commands", error.artifact_commands, { );
"[].command.segment.*.x" => rounded_redaction(4),
"[].command.segment.*.y" => rounded_redaction(4),
"[].command.segment.*.z" => rounded_redaction(4),
});
});
} }
e => { e => {
// These kinds of errors aren't expected to occur. We don't // These kinds of errors aren't expected to occur. We don't
@ -188,6 +161,42 @@ async fn execute(test_name: &str, render_to_png: bool) {
} }
} }
/// Assert snapshots that should happen both when KCL execution succeeds and
/// when it results in an error.
fn assert_common_snapshots(
test_name: &str,
operations: Vec<Operation>,
artifact_commands: Vec<ArtifactCommand>,
artifact_graph: ArtifactGraph,
) {
assert_snapshot(test_name, "Operations executed", || {
insta::assert_json_snapshot!("ops", operations);
});
assert_snapshot(test_name, "Artifact commands", || {
insta::assert_json_snapshot!("artifact_commands", artifact_commands, {
"[].command.segment.*.x" => rounded_redaction(4),
"[].command.segment.*.y" => rounded_redaction(4),
"[].command.segment.*.z" => rounded_redaction(4),
});
});
assert_snapshot(test_name, "Artifact graph flowchart", || {
let flowchart = artifact_graph
.to_mermaid_flowchart()
.unwrap_or_else(|e| format!("Failed to convert artifact graph to flowchart: {e}"));
// Change the snapshot suffix so that it is rendered as a Markdown file
// in GitHub.
insta::assert_binary_snapshot!("artifact_graph_flowchart.md", flowchart.as_bytes().to_owned());
});
assert_snapshot(test_name, "Artifact graph mind map", || {
let mind_map = artifact_graph
.to_mermaid_mind_map()
.unwrap_or_else(|e| format!("Failed to convert artifact graph to mind map: {e}"));
// Change the snapshot suffix so that it is rendered as a Markdown file
// in GitHub.
insta::assert_binary_snapshot!("artifact_graph_mind_map.md", mind_map.as_bytes().to_owned());
});
}
mod cube { mod cube {
const TEST_NAME: &str = "cube"; const TEST_NAME: &str = "cube";
@ -209,6 +218,27 @@ mod cube {
super::execute(TEST_NAME, true).await super::execute(TEST_NAME, true).await
} }
} }
mod cube_with_error {
const TEST_NAME: &str = "cube_with_error";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}
mod artifact_graph_example_code1 { mod artifact_graph_example_code1 {
const TEST_NAME: &str = "artifact_graph_example_code1"; const TEST_NAME: &str = "artifact_graph_example_code1";

View File

@ -3,14 +3,13 @@
use anyhow::Result; use anyhow::Result;
use derive_docs::stdlib; use derive_docs::stdlib;
use super::args::FromArgs;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{ExecState, KclValue}, execution::{ExecState, KclValue},
std::Args, std::Args,
}; };
use super::args::FromArgs;
/// Compute the remainder after dividing `num` by `div`. /// Compute the remainder after dividing `num` by `div`.
/// If `num` is negative, the result will be too. /// If `num` is negative, the result will be too.
pub async fn rem(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn rem(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {

View File

@ -11,12 +11,11 @@ use parse_display::{Display, FromStr};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::execution::{Artifact, ArtifactId};
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{ execution::{
BasePath, ExecState, Face, GeoMeta, KclValue, Path, Plane, Point2d, Point3d, Sketch, SketchSet, SketchSurface, Artifact, ArtifactId, BasePath, ExecState, Face, GeoMeta, KclValue, Path, Plane, Point2d, Point3d, Sketch,
Solid, TagEngineInfo, TagIdentifier, SketchSet, SketchSurface, Solid, TagEngineInfo, TagIdentifier,
}, },
parsing::ast::types::TagNode, parsing::ast::types::TagNode,
std::{ std::{
@ -2302,7 +2301,10 @@ mod tests {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use crate::{execution::TagIdentifier, std::sketch::PlaneData, std::utils::calculate_circle_center}; use crate::{
execution::TagIdentifier,
std::{sketch::PlaneData, utils::calculate_circle_center},
};
#[test] #[test]
fn test_deserialize_plane_data() { fn test_deserialize_plane_data() {

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph flowchart argument_error.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,3 @@
```mermaid
flowchart LR
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph mind map argument_error.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,4 @@
```mermaid
mindmap
root
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph flowchart array_elem_pop_empty_fail.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,3 @@
```mermaid
flowchart LR
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph mind map array_elem_pop_empty_fail.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,4 @@
```mermaid
mindmap
root
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph flowchart array_elem_pop_fail.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,3 @@
```mermaid
flowchart LR
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph mind map array_elem_pop_fail.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,4 @@
```mermaid
mindmap
root
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph flowchart array_elem_push_fail.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,3 @@
```mermaid
flowchart LR
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph mind map array_elem_push_fail.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,4 @@
```mermaid
mindmap
root
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph flowchart array_index_oob.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,3 @@
```mermaid
flowchart LR
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph mind map array_index_oob.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,4 @@
```mermaid
mindmap
root
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph flowchart comparisons_multiple.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,3 @@
```mermaid
flowchart LR
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph mind map comparisons_multiple.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,4 @@
```mermaid
mindmap
root
```

View File

@ -0,0 +1,538 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact commands cube_with_error.kcl
---
[
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": 1.0,
"y": 0.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 1.0,
"z": 0.0
},
"size": 100.0,
"clobber": false,
"hide": true
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "plane_set_color",
"plane_id": "[uuid]",
"color": {
"r": 0.7,
"g": 0.28,
"b": 0.28,
"a": 0.4
}
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": 0.0,
"y": 1.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 0.0,
"z": 1.0
},
"size": 100.0,
"clobber": false,
"hide": true
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "plane_set_color",
"plane_id": "[uuid]",
"color": {
"r": 0.28,
"g": 0.7,
"b": 0.28,
"a": 0.4
}
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": 1.0,
"y": 0.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 0.0,
"z": 1.0
},
"size": 100.0,
"clobber": false,
"hide": true
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "plane_set_color",
"plane_id": "[uuid]",
"color": {
"r": 0.28,
"g": 0.28,
"b": 0.7,
"a": 0.4
}
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": -1.0,
"y": 0.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 1.0,
"z": 0.0
},
"size": 100.0,
"clobber": false,
"hide": true
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": 0.0,
"y": -1.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 0.0,
"z": 1.0
},
"size": 100.0,
"clobber": false,
"hide": true
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": -1.0,
"y": 0.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 0.0,
"z": 1.0
},
"size": 100.0,
"clobber": false,
"hide": true
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "edge_lines_visible",
"hidden": false
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "set_scene_units",
"unit": "mm"
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "object_visible",
"object_id": "[uuid]",
"hidden": true
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "object_visible",
"object_id": "[uuid]",
"hidden": true
}
},
{
"cmdId": "[uuid]",
"range": [
177,
194,
0
],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": 1.0,
"y": 0.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 1.0,
"z": 0.0
},
"size": 60.0,
"clobber": false,
"hide": true
}
},
{
"cmdId": "[uuid]",
"range": [
177,
194,
0
],
"command": {
"type": "enable_sketch_mode",
"entity_id": "[uuid]",
"ortho": false,
"animated": false,
"adjust_camera": false,
"planar_normal": {
"x": 0.0,
"y": 0.0,
"z": 1.0
}
}
},
{
"cmdId": "[uuid]",
"range": [
177,
194,
0
],
"command": {
"type": "start_path"
}
},
{
"cmdId": "[uuid]",
"range": [
177,
194,
0
],
"command": {
"type": "move_path_pen",
"path": "[uuid]",
"to": {
"x": -20.0,
"y": -20.0,
"z": 0.0
}
}
},
{
"cmdId": "[uuid]",
"range": [
202,
215,
0
],
"command": {
"type": "extend_path",
"path": "[uuid]",
"segment": {
"type": "line",
"end": {
"x": -20.0,
"y": 20.0,
"z": 0.0
},
"relative": false
}
}
},
{
"cmdId": "[uuid]",
"range": [
223,
236,
0
],
"command": {
"type": "extend_path",
"path": "[uuid]",
"segment": {
"type": "line",
"end": {
"x": 20.0,
"y": 20.0,
"z": 0.0
},
"relative": false
}
}
},
{
"cmdId": "[uuid]",
"range": [
244,
257,
0
],
"command": {
"type": "extend_path",
"path": "[uuid]",
"segment": {
"type": "line",
"end": {
"x": 20.0,
"y": -20.0,
"z": 0.0
},
"relative": false
}
}
},
{
"cmdId": "[uuid]",
"range": [
265,
278,
0
],
"command": {
"type": "extend_path",
"path": "[uuid]",
"segment": {
"type": "line",
"end": {
"x": -20.0,
"y": -20.0,
"z": 0.0
},
"relative": false
}
}
},
{
"cmdId": "[uuid]",
"range": [
286,
294,
0
],
"command": {
"type": "close_path",
"path_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [
286,
294,
0
],
"command": {
"type": "sketch_mode_disable"
}
},
{
"cmdId": "[uuid]",
"range": [
302,
320,
0
],
"command": {
"type": "enable_sketch_mode",
"entity_id": "[uuid]",
"ortho": false,
"animated": false,
"adjust_camera": false,
"planar_normal": {
"x": 0.0,
"y": 0.0,
"z": 1.0
}
}
},
{
"cmdId": "[uuid]",
"range": [
302,
320,
0
],
"command": {
"type": "extrude",
"target": "[uuid]",
"distance": 40.0,
"faces": null
}
},
{
"cmdId": "[uuid]",
"range": [
302,
320,
0
],
"command": {
"type": "sketch_mode_disable"
}
},
{
"cmdId": "[uuid]",
"range": [
302,
320,
0
],
"command": {
"type": "object_bring_to_front",
"object_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [
302,
320,
0
],
"command": {
"type": "solid3d_get_extrusion_face_info",
"object_id": "[uuid]",
"edge_id": "[uuid]"
}
}
]

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph flowchart cube_with_error.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,38 @@
```mermaid
flowchart LR
subgraph path2 [Path]
2["Path<br>[177, 194, 0]"]
3["Segment<br>[202, 215, 0]"]
4["Segment<br>[223, 236, 0]"]
5["Segment<br>[244, 257, 0]"]
6["Segment<br>[265, 278, 0]"]
7["Segment<br>[286, 294, 0]"]
8[Solid2d]
end
1["Plane<br>[177, 194, 0]"]
9["Sweep Extrusion<br>[302, 320, 0]"]
10[Wall]
11[Wall]
12[Wall]
13[Wall]
14["Cap Start"]
15["Cap End"]
1 --- 2
2 --- 3
2 --- 4
2 --- 5
2 --- 6
2 --- 7
2 ---- 9
2 --- 8
3 --- 13
4 --- 12
5 --- 11
6 --- 10
9 --- 10
9 --- 11
9 --- 12
9 --- 13
9 --- 14
9 --- 15
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph mind map cube_with_error.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,23 @@
```mermaid
mindmap
root
Plane
Path
Segment
Wall
Segment
Wall
Segment
Wall
Segment
Wall
Segment
Sweep Extrusion
Wall
Wall
Wall
Wall
Cap Start
Cap End
Solid2d
```

View File

@ -0,0 +1,809 @@
---
source: kcl/src/simulation_tests.rs
description: Result of parsing cube_with_error.kcl
---
{
"Ok": {
"body": [
{
"declaration": {
"end": 322,
"id": {
"end": 7,
"name": "cube",
"start": 3,
"type": "Identifier"
},
"init": {
"body": {
"body": [
{
"declaration": {
"end": 42,
"id": {
"end": 29,
"name": "l",
"start": 28,
"type": "Identifier"
},
"init": {
"end": 42,
"left": {
"end": 38,
"name": "length",
"start": 32,
"type": "Identifier",
"type": "Identifier"
},
"operator": "/",
"right": {
"end": 42,
"raw": "2",
"start": 41,
"type": "Literal",
"type": "Literal",
"value": {
"value": 2.0,
"suffix": "None"
}
},
"start": 32,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
"start": 28,
"type": "VariableDeclarator"
},
"end": 42,
"kind": "const",
"start": 28,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 58,
"id": {
"end": 46,
"name": "x",
"start": 45,
"type": "Identifier"
},
"init": {
"computed": false,
"end": 58,
"object": {
"end": 55,
"name": "center",
"start": 49,
"type": "Identifier",
"type": "Identifier"
},
"property": {
"end": 57,
"raw": "0",
"start": 56,
"type": "Literal",
"type": "Literal",
"value": {
"value": 0.0,
"suffix": "None"
}
},
"start": 49,
"type": "MemberExpression",
"type": "MemberExpression"
},
"start": 45,
"type": "VariableDeclarator"
},
"end": 58,
"kind": "const",
"start": 45,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 74,
"id": {
"end": 62,
"name": "y",
"start": 61,
"type": "Identifier"
},
"init": {
"computed": false,
"end": 74,
"object": {
"end": 71,
"name": "center",
"start": 65,
"type": "Identifier",
"type": "Identifier"
},
"property": {
"end": 73,
"raw": "1",
"start": 72,
"type": "Literal",
"type": "Literal",
"value": {
"value": 1.0,
"suffix": "None"
}
},
"start": 65,
"type": "MemberExpression",
"type": "MemberExpression"
},
"start": 61,
"type": "VariableDeclarator"
},
"end": 74,
"kind": "const",
"start": 61,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 98,
"id": {
"end": 79,
"name": "p0",
"start": 77,
"type": "Identifier"
},
"init": {
"elements": [
{
"end": 89,
"left": {
"argument": {
"end": 85,
"name": "l",
"start": 84,
"type": "Identifier",
"type": "Identifier"
},
"end": 85,
"operator": "-",
"start": 83,
"type": "UnaryExpression",
"type": "UnaryExpression"
},
"operator": "+",
"right": {
"end": 89,
"name": "x",
"start": 88,
"type": "Identifier",
"type": "Identifier"
},
"start": 83,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
{
"end": 97,
"left": {
"argument": {
"end": 93,
"name": "l",
"start": 92,
"type": "Identifier",
"type": "Identifier"
},
"end": 93,
"operator": "-",
"start": 91,
"type": "UnaryExpression",
"type": "UnaryExpression"
},
"operator": "+",
"right": {
"end": 97,
"name": "y",
"start": 96,
"type": "Identifier",
"type": "Identifier"
},
"start": 91,
"type": "BinaryExpression",
"type": "BinaryExpression"
}
],
"end": 98,
"start": 82,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
"start": 77,
"type": "VariableDeclarator"
},
"end": 98,
"kind": "const",
"start": 77,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 121,
"id": {
"end": 103,
"name": "p1",
"start": 101,
"type": "Identifier"
},
"init": {
"elements": [
{
"end": 113,
"left": {
"argument": {
"end": 109,
"name": "l",
"start": 108,
"type": "Identifier",
"type": "Identifier"
},
"end": 109,
"operator": "-",
"start": 107,
"type": "UnaryExpression",
"type": "UnaryExpression"
},
"operator": "+",
"right": {
"end": 113,
"name": "x",
"start": 112,
"type": "Identifier",
"type": "Identifier"
},
"start": 107,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
{
"end": 120,
"left": {
"end": 116,
"name": "l",
"start": 115,
"type": "Identifier",
"type": "Identifier"
},
"operator": "+",
"right": {
"end": 120,
"name": "y",
"start": 119,
"type": "Identifier",
"type": "Identifier"
},
"start": 115,
"type": "BinaryExpression",
"type": "BinaryExpression"
}
],
"end": 121,
"start": 106,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
"start": 101,
"type": "VariableDeclarator"
},
"end": 121,
"kind": "const",
"start": 101,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 143,
"id": {
"end": 126,
"name": "p2",
"start": 124,
"type": "Identifier"
},
"init": {
"elements": [
{
"end": 135,
"left": {
"end": 131,
"name": "l",
"start": 130,
"type": "Identifier",
"type": "Identifier"
},
"operator": "+",
"right": {
"end": 135,
"name": "x",
"start": 134,
"type": "Identifier",
"type": "Identifier"
},
"start": 130,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
{
"end": 142,
"left": {
"end": 138,
"name": "l",
"start": 137,
"type": "Identifier",
"type": "Identifier"
},
"operator": "+",
"right": {
"end": 142,
"name": "y",
"start": 141,
"type": "Identifier",
"type": "Identifier"
},
"start": 137,
"type": "BinaryExpression",
"type": "BinaryExpression"
}
],
"end": 143,
"start": 129,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
"start": 124,
"type": "VariableDeclarator"
},
"end": 143,
"kind": "const",
"start": 124,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 166,
"id": {
"end": 148,
"name": "p3",
"start": 146,
"type": "Identifier"
},
"init": {
"elements": [
{
"end": 157,
"left": {
"end": 153,
"name": "l",
"start": 152,
"type": "Identifier",
"type": "Identifier"
},
"operator": "+",
"right": {
"end": 157,
"name": "x",
"start": 156,
"type": "Identifier",
"type": "Identifier"
},
"start": 152,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
{
"end": 165,
"left": {
"argument": {
"end": 161,
"name": "l",
"start": 160,
"type": "Identifier",
"type": "Identifier"
},
"end": 161,
"operator": "-",
"start": 159,
"type": "UnaryExpression",
"type": "UnaryExpression"
},
"operator": "+",
"right": {
"end": 165,
"name": "y",
"start": 164,
"type": "Identifier",
"type": "Identifier"
},
"start": 159,
"type": "BinaryExpression",
"type": "BinaryExpression"
}
],
"end": 166,
"start": 151,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
"start": 146,
"type": "VariableDeclarator"
},
"end": 166,
"kind": "const",
"start": 146,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"argument": {
"body": [
{
"arguments": [
{
"end": 193,
"name": "p0",
"start": 191,
"type": "Identifier",
"type": "Identifier"
}
],
"callee": {
"end": 190,
"name": "startSketchAt",
"start": 177,
"type": "Identifier"
},
"end": 194,
"start": 177,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"end": 211,
"name": "p1",
"start": 209,
"type": "Identifier",
"type": "Identifier"
},
{
"end": 214,
"start": 213,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 208,
"name": "lineTo",
"start": 202,
"type": "Identifier"
},
"end": 215,
"start": 202,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"end": 232,
"name": "p2",
"start": 230,
"type": "Identifier",
"type": "Identifier"
},
{
"end": 235,
"start": 234,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 229,
"name": "lineTo",
"start": 223,
"type": "Identifier"
},
"end": 236,
"start": 223,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"end": 253,
"name": "p3",
"start": 251,
"type": "Identifier",
"type": "Identifier"
},
{
"end": 256,
"start": 255,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 250,
"name": "lineTo",
"start": 244,
"type": "Identifier"
},
"end": 257,
"start": 244,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"end": 274,
"name": "p0",
"start": 272,
"type": "Identifier",
"type": "Identifier"
},
{
"end": 277,
"start": 276,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 271,
"name": "lineTo",
"start": 265,
"type": "Identifier"
},
"end": 278,
"start": 265,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"end": 293,
"start": 292,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 291,
"name": "close",
"start": 286,
"type": "Identifier"
},
"end": 294,
"start": 286,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"end": 316,
"name": "length",
"start": 310,
"type": "Identifier",
"type": "Identifier"
},
{
"end": 319,
"start": 318,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 309,
"name": "extrude",
"start": 302,
"type": "Identifier"
},
"end": 320,
"start": 302,
"type": "CallExpression",
"type": "CallExpression"
}
],
"end": 320,
"start": 177,
"type": "PipeExpression",
"type": "PipeExpression"
},
"end": 320,
"start": 170,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 322,
"nonCodeMeta": {
"nonCodeNodes": {
"6": [
{
"end": 170,
"start": 166,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
]
},
"startNodes": []
},
"start": 24
},
"end": 322,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 14,
"name": "length",
"start": 8,
"type": "Identifier"
}
},
{
"type": "Parameter",
"identifier": {
"end": 22,
"name": "center",
"start": 16,
"type": "Identifier"
}
}
],
"start": 7,
"type": "FunctionExpression",
"type": "FunctionExpression"
},
"start": 3,
"type": "VariableDeclarator"
},
"end": 322,
"kind": "fn",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 349,
"id": {
"end": 330,
"name": "myCube",
"start": 324,
"type": "Identifier"
},
"init": {
"arguments": [
{
"end": 340,
"raw": "40",
"start": 338,
"type": "Literal",
"type": "Literal",
"value": {
"value": 40.0,
"suffix": "None"
}
},
{
"elements": [
{
"end": 344,
"raw": "0",
"start": 343,
"type": "Literal",
"type": "Literal",
"value": {
"value": 0.0,
"suffix": "None"
}
},
{
"end": 347,
"raw": "0",
"start": 346,
"type": "Literal",
"type": "Literal",
"value": {
"value": 0.0,
"suffix": "None"
}
}
],
"end": 348,
"start": 342,
"type": "ArrayExpression",
"type": "ArrayExpression"
}
],
"callee": {
"end": 337,
"name": "cube",
"start": 333,
"type": "Identifier"
},
"end": 349,
"start": 333,
"type": "CallExpression",
"type": "CallExpression"
},
"start": 324,
"type": "VariableDeclarator"
},
"end": 349,
"kind": "const",
"start": 324,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"end": 398,
"expression": {
"end": 398,
"name": "foo",
"start": 395,
"type": "Identifier",
"type": "Identifier"
},
"start": 395,
"type": "ExpressionStatement",
"type": "ExpressionStatement"
}
],
"end": 399,
"nonCodeMeta": {
"nonCodeNodes": {
"0": [
{
"end": 324,
"start": 322,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
],
"1": [
{
"end": 394,
"start": 349,
"type": "NonCodeNode",
"value": {
"type": "newLineBlockComment",
"value": "Error, after creating meaningful output.",
"style": "line"
}
}
]
},
"startNodes": []
},
"start": 0
}
}

View File

@ -0,0 +1,12 @@
---
source: kcl/src/simulation_tests.rs
description: Error from executing cube_with_error.kcl
---
KCL UndefinedValue error
× undefined value: memory item key `foo` is not defined
╭─[22:1]
21 │ // Error, after creating meaningful output.
22 │ foo
· ───
╰────

View File

@ -0,0 +1,22 @@
fn cube(length, center) {
l = length / 2
x = center[0]
y = center[1]
p0 = [-l + x, -l + y]
p1 = [-l + x, l + y]
p2 = [l + x, l + y]
p3 = [l + x, -l + y]
return startSketchAt(p0)
|> lineTo(p1, %)
|> lineTo(p2, %)
|> lineTo(p3, %)
|> lineTo(p0, %)
|> close(%)
|> extrude(length, %)
}
myCube = cube(40, [0, 0])
// Error, after creating meaningful output.
foo

View File

@ -0,0 +1,51 @@
---
source: kcl/src/simulation_tests.rs
description: Operations executed cube_with_error.kcl
---
[
{
"type": "UserDefinedFunctionCall",
"name": "cube",
"functionSourceRange": [
7,
322,
0
],
"unlabeledArg": null,
"labeledArgs": {},
"sourceRange": [
333,
349,
0
]
},
{
"labeledArgs": {
"length": {
"sourceRange": [
310,
316,
0
]
},
"sketch_set": {
"sourceRange": [
318,
319,
0
]
}
},
"name": "extrude",
"sourceRange": [
302,
320,
0
],
"type": "StdLibCall",
"unlabeledArg": null
},
{
"type": "UserDefinedFunctionReturn"
}
]

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph flowchart import_cycle1.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,3 @@
```mermaid
flowchart LR
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph mind map import_cycle1.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,4 @@
```mermaid
mindmap
root
```

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph flowchart invalid_index_fractional.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,3 @@
```mermaid
flowchart LR
```

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