Compare commits

...

6 Commits

Author SHA1 Message Date
5b207d7d1a chore: adding unit test to test getNodeFromPath failures (#5134)
* chore: adding unit test to test the parsing of the code

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* chore: trying to console error more content for getNodeByPath since it is cryptic

* fix: removing testing unit test

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* fix: fmt

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* bump

* fix: ...

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* bump

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* bump

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* bump

* fix: found da bug

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-31 15:13:43 -06:00
2fac213c58 Add engine message to dry run validation error toasts (#5175)
* Add engine message to dry run validation error toasts
Fixes #5174

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Add unit tests

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Reset snapshots

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Revert snapshot changes

* Fix lint

* Fix test

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-31 16:13:35 -05:00
2f72a8ef14 Helper functions for meta settings (get/update) (#5200)
* get the units back out

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* edit

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add to wasmts

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fmt

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-01-31 21:11:15 +00:00
27ce9f8aa4 Remove project.toml fetch during Open Sample (#5203)
* Fix units in tests

* Don't default to mm

* Last fix

* FIx lint

* Remove project.toml fetching from samples
2025-01-31 15:38:04 -05:00
b0426e3f94 Refactor: separate authMachine from React (#5110)
* Create a global appMachine

* Strip authMachine of side-effects

* Replace react-bound authMachine use with XState actor use

* Fix import goof

* Register auth commands directly!

* @lf94 feedback: conver `AuthNavigationHandler` to `useAuthNavigation`

* Uh, fix signing out thank you @lf94

* Fix tsc

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)"

This reverts commit 8dc50b6a26.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-31 14:47:08 -05:00
d707c66e53 Migrate Extrude AST mod from XState action to actor (#5146)
* Migrate Extrude AST mod from XState action to actor

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* @lf94 feedback

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-31 18:34:38 +00:00
47 changed files with 721 additions and 276 deletions

View File

@ -1078,7 +1078,7 @@ sketch002 = startSketchOn('XZ')
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await expect(
page.getByText('Unable to sweep with the provided selection')
page.getByText('Unable to sweep with the current selection. Reason:')
).toBeVisible()
})
})
@ -1846,7 +1846,7 @@ sweep001 = sweep({ path = sketch002 }, sketch001)
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await expect(
page.getByText('Unable to shell with the provided selection')
page.getByText('Unable to shell with the current selection. Reason:')
).toBeVisible()
await page.waitForTimeout(1000)
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -69,7 +69,6 @@ test.describe('Testing in-app sample loading', () => {
await confirmButton.click()
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 expect(newlyCreatedFile(sampleOne.file)).toBeVisible()
await expect(projectMenuButton).toContainText(sampleOne.file)
await expect(unitsToast('in')).toBeVisible()
})
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(sampleTwo.file)).not.toBeVisible()
await expect(projectMenuButton).toContainText(sampleOne.file)
await expect(unitsToast('mm')).toBeVisible()
})
}
)

View File

@ -25,6 +25,7 @@ import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
import { maybeWriteToDisk } from 'lib/telemetry'
import { commandBarActor } from 'machines/commandBarMachine'
import { useToken } from 'machines/appMachine'
maybeWriteToDisk()
.then(() => {})
.catch(() => {})
@ -60,8 +61,8 @@ export function App() {
useHotKeyListener()
const { auth, settings } = useSettingsAuthContext()
const token = auth?.context?.token
const { settings } = useSettingsAuthContext()
const token = useToken()
const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, codeManager, token),

View File

@ -1,10 +1,10 @@
import { useAuthState } from 'machines/appMachine'
import Loading from './components/Loading'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
// Wrapper around protected routes, used in src/Router.tsx
export const Auth = ({ children }: React.PropsWithChildren) => {
const { auth } = useSettingsAuthContext()
const isLoggingIn = auth?.state.matches('checkIfLoggedIn')
const authState = useAuthState()
const isLoggingIn = authState.matches('checkIfLoggedIn')
return isLoggingIn ? (
<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 { CoreDumpManager } from 'lib/coredump'
import { codeManager, engineCommandManager } from 'lib/singletons'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import useHotkeyWrapper from 'lib/hotkeyWrapper'
import toast from 'react-hot-toast'
import { coreDump } from 'lang/wasm'
@ -47,6 +46,7 @@ import { reportRejection } from 'lib/trap'
import { RouteProvider } from 'components/RouteProvider'
import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
import { OpenInDesktopAppHandler } from 'components/OpenInDesktopAppHandler'
import { useToken } from 'machines/appMachine'
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
@ -203,8 +203,7 @@ export const Router = () => {
}
function CoreDump() {
const { auth } = useSettingsAuthContext()
const token = auth?.context?.token
const token = useToken()
const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, codeManager, token),
[]

View File

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

View File

@ -30,6 +30,7 @@ import {
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { markOnce } from 'lib/performance'
import { commandBarActor } from 'machines/commandBarMachine'
import { useToken } from 'machines/appMachine'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -47,7 +48,8 @@ export const FileMachineProvider = ({
children: React.ReactNode
}) => {
const navigate = useNavigate()
const { settings, auth } = useSettingsAuthContext()
const { settings } = useSettingsAuthContext()
const token = useToken()
const projectData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const { project, file } = projectData
const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>(
@ -297,7 +299,7 @@ export const FileMachineProvider = ({
const kclCommandMemo = useMemo(
() =>
kclCommands({
authToken: auth?.context?.token ?? '',
authToken: token ?? '',
projectData,
settings: {
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 { isDesktop } from 'lib/isDesktop'
import { codeManager } from 'lib/singletons'
import { useToken } from 'machines/appMachine'
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
return []
@ -69,8 +70,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const [isKclLspReady, setIsKclLspReady] = useState(false)
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
const { auth } = useSettingsAuthContext()
const token = auth?.context.token
const token = useToken()
const navigate = useNavigate()
// So this is a bit weird, we need to initialize the lsp server and client.

View File

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

View File

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

View File

@ -8,10 +8,10 @@ import Tooltip from './Tooltip'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { reportRejection } from 'lib/trap'
import { toSync } from 'lib/utils'
import { useToken } from 'machines/appMachine'
export const RefreshButton = ({ children }: React.PropsWithChildren) => {
const { auth } = useSettingsAuthContext()
const token = auth?.context?.token
const token = useToken()
const coreDumpManager = useMemo(
() => 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 { PATHS } from 'lib/paths'
import { markOnce } from 'lib/performance'
import { useAuthNavigation } from 'hooks/useAuthNavigation'
export const RouteProviderContext = createContext({})
export function RouteProvider({ children }: { children: ReactNode }) {
useAuthNavigation()
const [first, setFirstState] = useState(true)
const navigation = useNavigation()
const location = useLocation()

View File

@ -2,10 +2,7 @@ import { trap } from 'lib/trap'
import { useMachine, useSelector } from '@xstate/react'
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
import { PATHS, BROWSER_PATH } from 'lib/paths'
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
import withBaseUrl from '../lib/withBaseURL'
import React, { createContext, useEffect, useState } from 'react'
import useStateMachineCommands from '../hooks/useStateMachineCommands'
import { settingsMachine } from 'machines/settingsMachine'
import { toast } from 'react-hot-toast'
import {
@ -16,7 +13,6 @@ import {
} from 'lib/theme'
import decamelize from 'decamelize'
import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate'
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
import {
kclManager,
sceneInfra,
@ -50,7 +46,6 @@ type MachineContext<T extends AnyStateMachine> = {
}
type SettingsAuthContextType = {
auth: MachineContext<typeof authMachine>
settings: MachineContext<typeof settingsMachine>
}
@ -370,40 +365,9 @@ export const SettingsAuthProviderBase = ({
)
}, [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 (
<SettingsAuthContext.Provider
value={{
auth: {
state: authState,
context: authState.context,
send: authSend,
},
settings: {
state: settingsState,
context: settingsState.context,
@ -417,12 +381,3 @@ export const SettingsAuthProviderBase = ({
}
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 { PATHS } from 'lib/paths'
import { Models } from '@kittycad/lib'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import Tooltip from './Tooltip'
import usePlatform from 'hooks/usePlatform'
import { isDesktop } from 'lib/isDesktop'
import { CustomIcon } from './CustomIcon'
import { authActor } from 'machines/appMachine'
type User = Models['User_type']
@ -20,7 +20,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
const displayedName = getDisplayName(user)
const [imageLoadFailed, setImageLoadFailed] = useState(false)
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.
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,
Identifier,
topLevelRange,
PipeExpression,
CallExpression,
VariableDeclarator,
} from './wasm'
import { ProgramMemory } from 'lang/wasm'
import {
findAllPreviousVariables,
isNodeSafeToReplace,
@ -25,9 +29,11 @@ import {
createCallExpression,
createLiteral,
createPipeSubstitution,
createCallExpressionStdLib,
} from './modifyAst'
import { err } from 'lib/trap'
import { codeRefFromRange } from './std/artifactGraph'
import { addCallExpressionsToPipe, addCloseToPipe } from 'lang/std/sketch'
beforeAll(async () => {
await initPromise
@ -680,3 +686,115 @@ myNestedVar = [
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

@ -21,6 +21,7 @@ import {
topLevelRange,
VariableDeclaration,
VariableDeclarator,
recast,
} from './wasm'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
@ -68,7 +69,28 @@ export function getNodeFromPath<T>(
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
}
currentNode = currentNode?.[pathItem[0]]
successfulPaths.push(pathItem)

View File

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

View File

@ -18,6 +18,7 @@ import {
default_project_settings,
base64_decode,
clear_scene_and_bust_cache,
change_kcl_settings,
reloadModule,
} 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 { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
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 { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
@ -844,3 +846,17 @@ export function base64Decode(base64: string): ArrayBuffer | Error {
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 { authMachine } from 'machines/authMachine'
import { Command } from 'lib/commandTypes'
import { authActor } from 'machines/appMachine'
import { ACTOR_IDS } from 'machines/machineConstants'
type AuthCommandSchema = {}
export const authCommandBarConfig: StateMachineCommandSetConfig<
typeof authMachine,
AuthCommandSchema
> = {
'Log in': {
hide: 'both',
},
'Log out': {
args: [],
export const authCommands: Command[] = [
{
groupId: ACTOR_IDS.AUTH,
name: 'log-out',
displayName: 'Log out',
icon: 'arrowLeft',
needsReview: false,
onSubmit: () => authActor.send({ type: 'Log out' }),
},
}
]

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 { CommandBarContext } from 'machines/commandBarMachine'
import { Selections } from 'lib/selections'
import { ApiError_type } from '@kittycad/lib/dist/types/src/models'
export const disableDryRunWithRetry = async (numberOfRetries = 3) => {
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 ({
data,
context,
@ -83,7 +98,7 @@ export const revolveAxisValidator = async ({
value: 360,
}
const revolveAboutEdgeCommand = async () => {
const command = async () => {
return await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
@ -96,13 +111,13 @@ export const revolveAxisValidator = async ({
},
})
}
const attemptRevolve = await dryRunWrapper(revolveAboutEdgeCommand)
if (attemptRevolve?.success) {
const result = await dryRunWrapper(command)
if (result?.success) {
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 ({
@ -128,7 +143,7 @@ export const loftValidator = async ({
return 'Unable to loft, selection contains less than two solid2ds'
}
const loftCommand = async () => {
const command = async () => {
// TODO: check what to do with these
const DEFAULT_V_DEGREE = 2
const DEFAULT_TOLERANCE = 2
@ -145,13 +160,13 @@ export const loftValidator = async ({
},
})
}
const attempt = await dryRunWrapper(loftCommand)
if (attempt?.success) {
const result = await dryRunWrapper(command)
if (result?.success) {
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 ({
@ -180,7 +195,7 @@ export const shellValidator = async ({
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
const DEFAULT_THICKNESS: Models['LengthUnit_type'] = 1e-9
const DEFAULT_HOLLOW = false
@ -200,12 +215,13 @@ export const shellValidator = async ({
})
}
const attemptShell = await dryRunWrapper(shellCommand)
if (attemptShell?.success) {
const result = await dryRunWrapper(command)
if (result?.success) {
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 ({
@ -241,7 +257,7 @@ export const sweepValidator = async ({
}
const target = targetArtifact.pathId
const sweepCommand = async () => {
const command = async () => {
// TODO: second look on defaults here
const DEFAULT_TOLERANCE: Models['LengthUnit_type'] = 1e-7
const DEFAULT_SECTIONAL = false
@ -261,10 +277,11 @@ export const sweepValidator = async ({
})
}
const attemptSweep = await dryRunWrapper(sweepCommand)
if (attemptSweep?.success) {
const result = await dryRunWrapper(command)
if (result?.success) {
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

@ -1,13 +1,10 @@
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
import { Command, CommandArgumentOption } from './commandTypes'
import { codeManager, kclManager } from './singletons'
import { kclManager } from './singletons'
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 { parseProjectSettings } from 'lang/wasm'
import { err, reportRejection } from './trap'
import { projectConfigurationToSettingsPayload } from './settings/settingsUtils'
import { copyFileShareLink } from './links'
import { reportRejection } from './trap'
import { IndexLoaderData } from './types'
interface OnSubmitProps {
@ -68,56 +65,23 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
const sampleCodeUrl = `https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/${encodeURIComponent(
projectPathPart
)}/${encodeURIComponent(primaryKclFile)}`
const sampleSettingsFileUrl = `https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/${encodeURIComponent(
projectPathPart
)}/${PROJECT_SETTINGS_FILE_NAME}`
Promise.allSettled([fetch(sampleCodeUrl), fetch(sampleSettingsFileUrl)])
.then((results) => {
const a =
'value' in results[0] ? results[0].value : results[0].reason
const b =
'value' in results[1] ? results[1].value : results[1].reason
return [a, b]
})
.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',
}
fetch(sampleCodeUrl)
.then(async (codeResponse): 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()
return {
sampleName: data.sample.split('/')[0] + FILE_EXT,
code,
method: data.method,
}
})
.then((props) => {
if (props?.code) {
commandProps.specialPropsForSampleCommand

View File

@ -26,6 +26,7 @@ import {
default_project_settings as DefaultProjectSettings,
base64_decode as Base64Decode,
clear_scene_and_bust_cache as ClearSceneAndBustCache,
change_kcl_settings as ChangeKclSettings,
} from '../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)
}
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'
import { COOKIE_NAME } from 'lib/constants'
import { markOnce } from 'lib/performance'
import { ACTOR_IDS } from './machineConstants'
import withBaseUrl from '../lib/withBaseURL'
const SKIP_AUTH = VITE_KC_SKIP_AUTH === 'true' && DEV
@ -50,7 +52,7 @@ export type Events =
}
export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
const persistedToken =
export const persistedToken =
VITE_KC_DEV_TOKEN ||
getCookie(COOKIE_NAME) ||
localStorage?.getItem(TOKEN_PERSIST_KEY) ||
@ -69,18 +71,17 @@ export const authMachine = setup({
}
}
},
actions: {
goToIndexPage: () => {},
goToSignInPage: () => {},
},
actors: {
getUser: fromPromise(({ input }: { input: { token?: string } }) =>
getUser(input)
),
logout: fromPromise(async () =>
isDesktop() ? writeTokenFile('') : logout()
),
},
}).createMachine({
/** @xstate-layout N4IgpgJg5mDOIC5QEECuAXAFgOgMabFwGsBJAMwBkB7KGCEgOwGIIqGxsBLBgNyqI75CRALQAbGnRHcA2gAYAuolAAHKrE7pObZSAAeiAIwAWQ9gBspuQCYAnAGYAHPYCsx+4ccAaEAE9E1q7YcoZyxrYR1m7mcrYAvnE+aFh4BMTk1LSQjExgAE55VHnYKmIAhuhkRQC2qcLikpDSDPJKSCBqGlo67QYI9gDs5tge5o6h5vau7oY+-v3mA9jWco4u5iu21ua2YcYJSRg4Eln0zJkABFQYrbqdmtoMun2GA7YjxuPmLqvGNh5zRCfJaOcyLUzuAYuFyGcwHEDJY6NCAAeQwTEuskUd3UDx6oD6Im2wUcAzkMJ2cjBxlMgIWLmwZLWljecjJTjh8IYVAgcF0iJxXUez0QIgGxhJZIpu2ptL8AWwtje1nCW2iq1shns8MRdXSlGRjEFeKevUQjkcy3sqwGHimbg83nlCF22GMytVUWMMUc8USCKO2BOdCN7Xu3VNBKMKsVFp2hm2vu+1id83slkVrgTxhcW0pNJ1geDkDR6GNEZFCAT1kZZLk9cMLltb0WdPMjewjjC1mzOZCtk5CSAA */
id: 'Auth',
/** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAOhzEwGsBJAMwBkB7KGCa-AYgkcJIIDdGlMGWwVKAWgA2zVhIIBtAAwBdRKAAOjWLgAuuHupAAPRAGYArAEYSADgu2AnGYBMLpVYBsZz7YA0IACeiG6OJM62tmZKLgDsno5KtvEAvikBaFh4hKTkVHRMLJDsHGAATmWMZSQaUui6tFWoouLSspDy+MpqSCBaOvqGvaYIljb2Tq7uXj7+QYgALFYW4clWy1ZmVgsWsZtpGRg4BMQkMkVsnIUABIwArrrdRv16BvhGI74LJBYW7o5WKJmKILObBUZeEgJP4LTxKMwIhZmBYLA4gTLHHJnWQEKAAeQeXB4IgEQhEGOyp3OUFxBN0CFJmHqb26T16L0G72GiCsSg8PyszkBCViTiUjgC4Jcnhc4SUsQcvgsoL2VjRFJOpGptMJ5Uq1Vq9UaZWaGqx2vw+IeDPwgiZnNZqme2leQ1An1s31+-0BCJBYJCLm+lk8CRl9hRyos6qOlK17QgdI4N0UTvZLs5Hx58NsJARuys0tDSl+AYQthsgNi0TMqt2LjVaPwjAgcCMZuIzoGbyzCAknkliH7Maympa+QYCfYXddXPdixcg4QvKUdk2u2iLkcsXhCRHmKpU7nfQzPe5CAsMpIXi8MvFKM8VliS5c1jzj53W3isNFqPS6NjMcLStXQZ0zc8ohsJI-kcFxXEcR9HAWF9gTzDxbCUXxAQWEsdn3ONsQuOkwLPedl22MIzFg3YP1gl9PG+bYvGsSxlUcRJozSFIgA */
id: ACTOR_IDS.AUTH,
initial: 'checkIfLoggedIn',
context: {
token: persistedToken,
@ -112,19 +113,30 @@ export const authMachine = setup({
},
},
loggedIn: {
entry: ['goToIndexPage'],
on: {
'Log out': {
target: 'loggedOut',
actions: () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
if (isDesktop()) writeTokenFile('')
},
target: 'loggingOut',
},
},
},
loggingOut: {
invoke: {
src: 'logout',
onDone: 'loggedOut',
onError: {
target: 'loggedIn',
actions: [
({ event }) => {
console.error(
'Error while logging out',
'error' in event ? `: ${event.error}` : ''
)
},
],
},
},
},
loggedOut: {
entry: ['goToSignInPage'],
on: {
'Log in': {
target: 'checkIfLoggedIn',
@ -235,3 +247,12 @@ async function getAndSyncStoredToken(input: {
localStorage.setItem(TOKEN_PERSIST_KEY, 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 toast from 'react-hot-toast'
import { useSelector } from '@xstate/react'
import { authCommands } from 'lib/commandBarConfigs/authCommandConfig'
export type CommandBarContext = {
commands: Command[]
@ -80,6 +81,7 @@ export type CommandBarMachineEvent =
export const commandBarMachine = setup({
types: {
context: {} as CommandBarContext,
input: {} as { commands: Command[] },
events: {} as CommandBarMachineEvent,
},
actions: {
@ -409,8 +411,8 @@ export const commandBarMachine = setup({
},
}).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 */
context: {
commands: [],
context: ({ input }) => ({
commands: input.commands || [],
selectedCommand: undefined,
currentArgument: undefined,
selectionRanges: {
@ -425,7 +427,7 @@ export const commandBarMachine = setup({
setCurrentMachine: () => {},
noMachinesReason: () => undefined,
},
},
}),
id: 'Command Bar',
initial: 'Closed',
states: {
@ -631,7 +633,11 @@ function sortCommands(a: Command, b: Command) {
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 */
const cmdBarStateSelector = (state: SnapshotFrom<typeof commandBarActor>) =>

View File

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

View File

@ -583,6 +583,13 @@ export const modelingMachine = setup({
},
// end guards
actions: {
toastError: ({ event }) => {
if ('output' in event && event.output instanceof Error) {
toast.error(event.output.message)
} else if ('data' in event && event.data instanceof Error) {
toast.error(event.data.message)
}
},
'assign tool in context': assign({
currentTool: ({ event }) =>
'data' in event && event.data && 'tool' in event.data
@ -637,56 +644,6 @@ export const modelingMachine = setup({
sketchDetails: event.output,
}
}),
'AST extrude': ({ context: { store }, event }) => {
if (event.type !== 'Extrude') return
;(async () => {
if (!event.data) return
const { selection, distance } = event.data
let ast = kclManager.ast
if (
'variableName' in distance &&
distance.variableName &&
distance.insertIndex !== undefined
) {
const newBody = [...ast.body]
newBody.splice(
distance.insertIndex,
0,
distance.variableDeclarationAst
)
ast.body = newBody
}
const pathToNode = getNodePathFromSourceRange(
ast,
selection.graphSelections[0]?.codeRef.range
)
const extrudeSketchRes = extrudeSketch(
ast,
pathToNode,
false,
'variableName' in distance
? distance.variableIdentifierAst
: distance.valueAst
)
if (trap(extrudeSketchRes)) return
const { modifiedAst, pathToExtrudeArg } = extrudeSketchRes
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: [pathToExtrudeArg],
zoomToFit: true,
zoomOnRangeAndType: {
range: selection.graphSelections[0]?.codeRef.range,
type: 'path',
},
})
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
if (updatedAst?.selections) {
editorManager.selectRange(updatedAst?.selections)
}
})().catch(reportRejection)
},
'AST revolve': ({ context: { store }, event }) => {
if (event.type !== 'Revolve') return
;(async () => {
@ -1491,6 +1448,63 @@ export const modelingMachine = setup({
return {} as SetSelections
}
),
extrudeAstMod: fromPromise<
void,
ModelingCommandSchema['Extrude'] | undefined
>(async ({ input }) => {
if (!input) return Promise.reject('No input provided')
const { selection, distance } = input
let ast = structuredClone(kclManager.ast)
let extrudeName: string | undefined = undefined
const pathToNode = getNodePathFromSourceRange(
ast,
selection.graphSelections[0]?.codeRef.range
)
// Add an extrude statement to the AST
const extrudeSketchRes = extrudeSketch(
ast,
pathToNode,
false,
'variableName' in distance
? distance.variableIdentifierAst
: distance.valueAst
)
if (err(extrudeSketchRes)) return Promise.reject(extrudeSketchRes)
const { modifiedAst, pathToExtrudeArg } = extrudeSketchRes
// Insert the distance variable if the user has provided a variable name
if (
'variableName' in distance &&
distance.variableName &&
typeof pathToExtrudeArg[1][0] === 'number'
) {
const insertIndex = Math.min(
pathToExtrudeArg[1][0],
distance.insertIndex
)
const newBody = [...modifiedAst.body]
newBody.splice(insertIndex, 0, distance.variableDeclarationAst)
modifiedAst.body = newBody
// Since we inserted a new variable, we need to update the path to the extrude argument
pathToExtrudeArg[1][0]++
}
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: [pathToExtrudeArg],
zoomToFit: true,
zoomOnRangeAndType: {
range: selection.graphSelections[0]?.codeRef.range,
type: 'path',
},
})
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
if (updatedAst?.selections) {
editorManager.selectRange(updatedAst?.selections)
}
}),
offsetPlaneAstMod: fromPromise(
async ({
input,
@ -1821,9 +1835,8 @@ export const modelingMachine = setup({
],
Extrude: {
target: 'idle',
actions: ['AST extrude'],
reenter: false,
target: 'Applying extrude',
reenter: true,
},
Revolve: {
@ -2548,7 +2561,7 @@ export const modelingMachine = setup({
'Delete segment': {
reenter: false,
actions: ['Delete segment', 'Set sketchDetails'],
actions: ['Delete segment', 'Set sketchDetails', 'reset selections'],
},
'code edit during sketch': '.clean slate',
},
@ -2620,6 +2633,22 @@ export const modelingMachine = setup({
},
},
'Applying extrude': {
invoke: {
src: 'extrudeAstMod',
id: 'extrudeAstMod',
input: ({ event }) => {
if (event.type !== 'Extrude') return undefined
return event.data
},
onDone: ['idle'],
onError: {
target: 'idle',
actions: 'toastError',
},
},
},
'Applying offset plane': {
invoke: {
src: 'offsetPlaneAstMod',

View File

@ -1,15 +1,14 @@
import { OnboardingButtons, useDismiss, useNextClick } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { useEffect, useState } from 'react'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useUser } from 'machines/appMachine'
export default function UserMenu() {
const { auth } = useSettingsAuthContext()
const user = useUser()
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.PROJECT_MENU)
const [avatarErrored, setAvatarErrored] = useState(false)
const user = auth?.context?.user
const errorOrNoImage = !user?.image || avatarErrored
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 { reportRejection } from 'lib/trap'
import toast from 'react-hot-toast'
import { authActor } from 'machines/appMachine'
const subtleBorder =
'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 [userCode, setUserCode] = useState('')
const {
auth: { send },
settings: {
state: {
context: {
@ -70,7 +70,7 @@ const SignIn = () => {
toast.error('Error while trying to log in')
return
}
send({ type: 'Log in', token })
authActor.send({ type: 'Log in', token })
}
return (

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@ use std::collections::HashMap;
use async_recursion::async_recursion;
use super::cad_op::{OpArg, Operation};
use crate::{
errors::{KclError, KclErrorDetails},
execution::{
@ -19,8 +20,6 @@ use crate::{
},
};
use super::cad_op::{OpArg, Operation};
impl BinaryPart {
#[async_recursion]
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 uuid::Uuid;
use super::ExecutorContext;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{ExecState, ImportedGeometry},
@ -22,8 +23,6 @@ use crate::{
source_range::SourceRange,
};
use super::ExecutorContext;
// Zoo co-ordinate system.
//
// * Forward: -Y

View File

@ -581,10 +581,11 @@ impl KclValue {
}
// 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)]
#[serde(tag = "type")]
pub enum UnitLen {
#[default]
Mm,
Cm,
M,
@ -593,6 +594,19 @@ pub enum UnitLen {
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 {
type Error = ();
@ -644,6 +658,15 @@ pub enum UnitAngle {
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 {
type Error = ();

View File

@ -13,8 +13,7 @@ use kcmc::{
websocket::{ModelingSessionData, OkWebSocketResponseData},
ImageFormat, ModelingCmd,
};
use kittycad_modeling_cmds::length_unit::LengthUnit;
use kittycad_modeling_cmds::{self as kcmc, websocket::WebSocketResponse};
use kittycad_modeling_cmds::{self as kcmc, length_unit::LengthUnit, websocket::WebSocketResponse};
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
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};
use uuid::Uuid;
mod annotations;
pub(crate) mod annotations;
mod artifact;
pub(crate) mod cache;
mod cad_op;
mod exec_ast;
mod function_param;
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::{
engine::{EngineManager, ExecutionKind},
@ -52,10 +55,6 @@ use crate::{
ExecError, KclErrorWithOutputs, Program,
};
// Re-exports.
pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, ArtifactId};
pub use cad_op::Operation;
/// State for executing a program.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[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)]
#[serde(rename_all = "camelCase")]
pub struct MetaSettings {
@ -256,7 +255,11 @@ pub struct 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)?;
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 execution::{
cache::{CacheInformation, OldAstState},
ExecState, ExecutorContext, ExecutorSettings, Point2d,
ExecState, ExecutorContext, ExecutorSettings, MetaSettings, Point2d,
};
pub use lsp::{
copilot::Backend as CopilotLspBackend,
@ -121,8 +121,7 @@ pub mod std_utils {
}
pub mod pretty {
pub use crate::parsing::token::NumericSuffix;
pub use crate::unparser::format_number;
pub use crate::{parsing::token::NumericSuffix, unparser::format_number};
}
use serde::{Deserialize, Serialize};
@ -162,6 +161,18 @@ impl Program {
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> {
self.ast.lint_all()
}

View File

@ -6,6 +6,7 @@ use kcmc::{
};
use kittycad_modeling_cmds as kcmc;
use super::types::LiteralValue;
use crate::{
engine::EngineManager,
errors::{KclError, KclErrorDetails},
@ -18,8 +19,6 @@ use crate::{
Program,
};
use super::types::LiteralValue;
type Point3d = kcmc::shared::Point3d<f64>;
#[derive(Debug)]

View File

@ -17,7 +17,6 @@ use tower_lsp::lsp_types::{
CompletionItem, CompletionItemKind, DocumentSymbol, FoldingRange, FoldingRangeKind, Range as LspRange, SymbolKind,
};
use super::digest::Digest;
pub use crate::parsing::ast::types::{
condition::{ElseIf, IfExpression},
literal_value::LiteralValue,
@ -26,7 +25,8 @@ pub use crate::parsing::ast::types::{
use crate::{
docs::StdLibFn,
errors::KclError,
execution::{KclValue, Metadata, TagIdentifier},
execution::{annotations, KclValue, Metadata, TagIdentifier},
parsing::ast::digest::Digest,
parsing::PIPE_OPERATOR,
source_range::{ModuleId, SourceRange},
};
@ -254,6 +254,52 @@ impl Node<Program> {
}
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 {
@ -1078,6 +1124,24 @@ impl NonCodeValue {
_ => 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)]
@ -2337,6 +2401,14 @@ impl Node<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.
pub fn get_hover_value_for_position(&self, pos: usize, code: &str) -> Option<Hover> {
let value_source_range: SourceRange = self.value.clone().into();
@ -3756,4 +3828,98 @@ const cylinder = startSketchOn('-XZ')
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

@ -3,14 +3,13 @@
use anyhow::Result;
use derive_docs::stdlib;
use super::args::FromArgs;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{ExecState, KclValue},
std::Args,
};
use super::args::FromArgs;
/// Compute the remainder after dividing `num` by `div`.
/// If `num` is negative, the result will be too.
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 serde::{Deserialize, Serialize};
use crate::execution::{Artifact, ArtifactId};
use crate::{
errors::{KclError, KclErrorDetails},
execution::{
BasePath, ExecState, Face, GeoMeta, KclValue, Path, Plane, Point2d, Point3d, Sketch, SketchSet, SketchSurface,
Solid, TagEngineInfo, TagIdentifier,
Artifact, ArtifactId, BasePath, ExecState, Face, GeoMeta, KclValue, Path, Plane, Point2d, Point3d, Sketch,
SketchSet, SketchSurface, Solid, TagEngineInfo, TagIdentifier,
},
parsing::ast::types::TagNode,
std::{
@ -2250,7 +2249,10 @@ mod tests {
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]
fn test_deserialize_plane_data() {

View File

@ -614,3 +614,18 @@ pub fn calculate_circle_from_3_points(ax: f64, ay: f64, bx: f64, by: f64, cx: f6
radius: result.radius,
}
}
/// Takes a kcl string and Meta settings and changes the meta settings in the kcl string.
#[wasm_bindgen]
pub fn change_kcl_settings(code: &str, settings_str: &str) -> Result<String, String> {
console_error_panic_hook::set_once();
let settings: kcl_lib::MetaSettings = serde_json::from_str(settings_str).map_err(|e| e.to_string())?;
let mut program = Program::parse_no_errs(code).map_err(|e| e.to_string())?;
let new_program = program.change_meta_settings(settings).map_err(|e| e.to_string())?;
let formatted = new_program.recast();
Ok(formatted)
}