Compare commits

..

8 Commits

23 changed files with 316 additions and 296 deletions

View File

@ -1,3 +1,3 @@
[codespell] [codespell]
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall,ser,fromM,FromM ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall,ser,fromM,FromM
skip: **/target,node_modules,build,dist,./out,**/Cargo.lock,./docs/kcl/*.md,./e2e/playwright/lib/console-error-whitelist.ts,.package-lock.json,**/package-lock.json,./openapi/*.json,./packages/codemirror-lang-kcl/test/all.test.ts,./public/kcl-samples,./rust/kcl-lib/tests/kcl_samples,tsconfig.tsbuildinfo,./src/lib/machine-api.d.ts skip: **/target,node_modules,build,dist,./out,**/Cargo.lock,./docs/kcl/*.md,.package-lock.json,**/package-lock.json,./openapi/*.json,./packages/codemirror-lang-kcl/test/all.test.ts,./public/kcl-samples,./rust/kcl-lib/tests/kcl_samples,tsconfig.tsbuildinfo,./src/lib/machine-api.d.ts

View File

@ -226,8 +226,8 @@ jobs:
with: with:
shell: bash shell: bash
command: npm run test:snapshots command: npm run test:snapshots
timeout_minutes: 5 timeout_minutes: 30
max_attempts: 5 max_attempts: 3
env: env:
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }} snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}

View File

@ -15,8 +15,8 @@ ifdef WINDOWS
CARGO ?= $(USERPROFILE)/.cargo/bin/cargo.exe CARGO ?= $(USERPROFILE)/.cargo/bin/cargo.exe
WASM_PACK ?= $(USERPROFILE)/.cargo/bin/wasm-pack.exe WASM_PACK ?= $(USERPROFILE)/.cargo/bin/wasm-pack.exe
else else
CARGO ?= $(shell which cargo || echo ~/.cargo/bin/cargo) CARGO ?= ~/.cargo/bin/cargo
WASM_PACK ?= $(shell which wasm-pack || echo ~/.cargo/bin/wasm-pack) WASM_PACK ?= ~/.cargo/bin/wasm-pack
endif endif
.PHONY: install .PHONY: install

View File

@ -5,10 +5,7 @@ import type { Locator, Page } from '@playwright/test'
import type { EditorFixture } from '@e2e/playwright/fixtures/editorFixture' import type { EditorFixture } from '@e2e/playwright/fixtures/editorFixture'
import type { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture' import type { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture'
import type { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture' import type { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture'
import { import { orRunWhenFullSuiteEnabled } from '@e2e/playwright/test-utils'
orRunWhenFullSuiteEnabled,
runningOnWindows,
} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test' import { expect, test } from '@e2e/playwright/zoo-test'
// test file is for testing point an click code gen functionality that's not sketch mode related // test file is for testing point an click code gen functionality that's not sketch mode related
@ -3547,9 +3544,6 @@ tag=$rectangleSegmentC002,
toolbar, toolbar,
cmdBar, cmdBar,
}) => { }) => {
if (runningOnWindows()) {
test.fixme(orRunWhenFullSuiteEnabled())
}
const initialCode = `sketch001 = startSketchOn(XZ) const initialCode = `sketch001 = startSketchOn(XZ)
|> startProfileAt([-102.57, 101.72], %) |> startProfileAt([-102.57, 101.72], %)
|> angledLine(angle = 0, length = 202.6, tag = $rectangleSegmentA001) |> angledLine(angle = 0, length = 202.6, tag = $rectangleSegmentA001)

View File

@ -1,5 +1,4 @@
import { expect, test } from '@e2e/playwright/zoo-test' import { expect, test } from '@e2e/playwright/zoo-test'
import { orRunWhenFullSuiteEnabled } from '@e2e/playwright/test-utils'
/* eslint-disable jest/no-conditional-expect */ /* eslint-disable jest/no-conditional-expect */
@ -58,7 +57,6 @@ test.describe('edit with AI example snapshots', () => {
`change colour`, `change colour`,
{ tag: '@snapshot' }, { tag: '@snapshot' },
async ({ context, homePage, cmdBar, editor, page, scene }) => { async ({ context, homePage, cmdBar, editor, page, scene }) => {
test.fixme(orRunWhenFullSuiteEnabled())
await context.addInitScript((file) => { await context.addInitScript((file) => {
localStorage.setItem('persistCode', file) localStorage.setItem('persistCode', file)
}, file) }, file)

View File

@ -377,7 +377,8 @@ test.describe(
'extrude on default planes should be stable', 'extrude on default planes should be stable',
{ tag: '@snapshot' }, { tag: '@snapshot' },
() => { () => {
test.fixme(orRunWhenFullSuiteEnabled()) // FIXME: Skip on macos its being weird.
test.skip(process.platform === 'darwin', 'Skip on macos')
test('XY', async ({ page, context, cmdBar, scene }) => { test('XY', async ({ page, context, cmdBar, scene }) => {
await extrudeDefaultPlane(context, page, cmdBar, scene, 'XY') await extrudeDefaultPlane(context, page, cmdBar, scene, 'XY')
@ -409,7 +410,6 @@ test(
'Draft segments should look right', 'Draft segments should look right',
{ tag: '@snapshot' }, { tag: '@snapshot' },
async ({ page, scene, toolbar }) => { async ({ page, scene, toolbar }) => {
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
@ -534,7 +534,8 @@ test(
'Draft rectangles should look right', 'Draft rectangles should look right',
{ tag: '@snapshot' }, { tag: '@snapshot' },
async ({ page, context, cmdBar, scene }) => { async ({ page, context, cmdBar, scene }) => {
test.fixme(orRunWhenFullSuiteEnabled()) // FIXME: Skip on macos its being weird.
test.skip(process.platform === 'darwin', 'Skip on macos')
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
@ -628,7 +629,8 @@ test.describe(
'Client side scene scale should match engine scale', 'Client side scene scale should match engine scale',
{ tag: '@snapshot' }, { tag: '@snapshot' },
() => { () => {
test.fixme(orRunWhenFullSuiteEnabled()) // FIXME: Skip on macos its being weird.
test.skip(process.platform === 'darwin', 'Skip on macos')
test('Inch scale', async ({ page, cmdBar, scene }) => { test('Inch scale', async ({ page, cmdBar, scene }) => {
const u = await getUtils(page) const u = await getUtils(page)
@ -805,7 +807,8 @@ test(
'Sketch on face with none z-up', 'Sketch on face with none z-up',
{ tag: '@snapshot' }, { tag: '@snapshot' },
async ({ page, context, cmdBar, scene }) => { async ({ page, context, cmdBar, scene }) => {
test.fixme(orRunWhenFullSuiteEnabled()) // FIXME: Skip on macos its being weird.
test.skip(process.platform === 'darwin', 'Skip on macos')
const u = await getUtils(page) const u = await getUtils(page)
await context.addInitScript(async (KCL_DEFAULT_LENGTH) => { await context.addInitScript(async (KCL_DEFAULT_LENGTH) => {
@ -865,7 +868,8 @@ test(
'Zoom to fit on load - solid 2d', 'Zoom to fit on load - solid 2d',
{ tag: '@snapshot' }, { tag: '@snapshot' },
async ({ page, context, cmdBar, scene }) => { async ({ page, context, cmdBar, scene }) => {
test.fixme(orRunWhenFullSuiteEnabled()) // FIXME: Skip on macos its being weird.
test.skip(process.platform === 'darwin', 'Skip on macos')
const u = await getUtils(page) const u = await getUtils(page)
await context.addInitScript(async () => { await context.addInitScript(async () => {
@ -903,7 +907,8 @@ test(
'Zoom to fit on load - solid 3d', 'Zoom to fit on load - solid 3d',
{ tag: '@snapshot' }, { tag: '@snapshot' },
async ({ page, context, cmdBar, scene }) => { async ({ page, context, cmdBar, scene }) => {
test.fixme(orRunWhenFullSuiteEnabled()) // FIXME: Skip on macos its being weird.
test.skip(process.platform === 'darwin', 'Skip on macos')
const u = await getUtils(page) const u = await getUtils(page)
await context.addInitScript(async () => { await context.addInitScript(async () => {
@ -944,7 +949,6 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
cmdBar, cmdBar,
scene, scene,
}) => { }) => {
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page) const u = await getUtils(page)
const stream = page.getByTestId('stream') const stream = page.getByTestId('stream')
@ -1004,7 +1008,6 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
}) })
test('Grid turned off', async ({ page, cmdBar, scene }) => { test('Grid turned off', async ({ page, cmdBar, scene }) => {
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page) const u = await getUtils(page)
const stream = page.getByTestId('stream') const stream = page.getByTestId('stream')
@ -1026,7 +1029,6 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
}) })
test('Grid turned on', async ({ page, context, cmdBar, scene }) => { test('Grid turned on', async ({ page, context, cmdBar, scene }) => {
test.fixme(orRunWhenFullSuiteEnabled())
await context.addInitScript( await context.addInitScript(
async ({ settingsKey, settings }) => { async ({ settingsKey, settings }) => {
localStorage.setItem(settingsKey, settings) localStorage.setItem(settingsKey, settings)
@ -1136,7 +1138,6 @@ test('theme persists', async ({ page, context }) => {
test.describe('code color goober', { tag: '@snapshot' }, () => { test.describe('code color goober', { tag: '@snapshot' }, () => {
test('code color goober', async ({ page, context, scene, cmdBar }) => { test('code color goober', async ({ page, context, scene, cmdBar }) => {
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page) const u = await getUtils(page)
await context.addInitScript(async () => { await context.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(

4
rust/Cargo.lock generated
View File

@ -665,9 +665,9 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-channel" name = "crossbeam-channel"
version = "0.5.15" version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
dependencies = [ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]

View File

@ -43,6 +43,7 @@ import {
import { commandBarActor } from '@src/machines/commandBarMachine' import { commandBarActor } from '@src/machines/commandBarMachine'
import { EngineStreamTransition } from '@src/machines/engineStreamMachine' import { EngineStreamTransition } from '@src/machines/engineStreamMachine'
import { onboardingPaths } from '@src/routes/Onboarding/paths' import { onboardingPaths } from '@src/routes/Onboarding/paths'
import { ActionSidebar } from './components/ModelingSidebar/ActionSidebar'
// CYCLIC REF // CYCLIC REF
sceneInfra.camControls.engineStreamActor = engineStreamActor sceneInfra.camControls.engineStreamActor = engineStreamActor
@ -168,7 +169,10 @@ export function App() {
enableMenu={true} enableMenu={true}
/> />
<ModalContainer /> <ModalContainer />
<ModelingSidebar paneOpacity={paneOpacity} /> <div className="relative flex-1 flex flex-col">
<ModelingSidebar paneOpacity={paneOpacity} />
<ActionSidebar />
</div>
<EngineStream pool={pool} authToken={authToken} /> <EngineStream pool={pool} authToken={authToken} />
{/* <CamToggle /> */} {/* <CamToggle /> */}
<LowerRightControls coreDumpManager={coreDumpManager}> <LowerRightControls coreDumpManager={coreDumpManager}>

View File

@ -127,7 +127,7 @@ export const CreateNewVariable = ({
autoFocus={true} autoFocus={true}
autoCapitalize="off" autoCapitalize="off"
autoCorrect="off" autoCorrect="off"
className={`font-mono flex-1 sm:text-sm px-2 py-1 rounded-sm bg-chalkboard-10 dark:bg-chalkboard-90 text-chalkboard-90 dark:text-chalkboard-10 ${ className={`flex-1 sm:text-sm px-2 py-1 rounded-sm bg-chalkboard-10 dark:bg-chalkboard-90 text-chalkboard-90 dark:text-chalkboard-10 ${
!shouldCreateVariable ? 'opacity-50' : '' !shouldCreateVariable ? 'opacity-50' : ''
}`} }`}
value={newVariableName} value={newVariableName}

View File

@ -628,8 +628,8 @@ const CustomIconMap = {
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
fill-rule="evenodd" fillRule="evenodd"
clip-rule="evenodd" clipRule="evenodd"
d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V8.50001H11H10.5V8.00001V4H5V16H9.79076C9.93763 16.3628 10.1362 16.6992 10.3773 17H4.5H4V16.5V3.5V3ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711ZM13.8123 17.3904L16.3123 15.3904L15.6877 14.6096L14 15.9597V12H13V15.9597L11.3123 14.6096L10.6877 15.3904L13.1877 17.3904L13.5 17.6403L13.8123 17.3904Z" d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V8.50001H11H10.5V8.00001V4H5V16H9.79076C9.93763 16.3628 10.1362 16.6992 10.3773 17H4.5H4V16.5V3.5V3ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711ZM13.8123 17.3904L16.3123 15.3904L15.6877 14.6096L14 15.9597V12H13V15.9597L11.3123 14.6096L10.6877 15.3904L13.1877 17.3904L13.5 17.6403L13.8123 17.3904Z"
fill="currentColor" fill="currentColor"
/> />

View File

@ -3,7 +3,6 @@ import { useEffect, useState } from 'react'
import type { CommandLog } from '@src/lang/std/commandLog' import type { CommandLog } from '@src/lang/std/commandLog'
import { engineCommandManager } from '@src/lib/singletons' import { engineCommandManager } from '@src/lib/singletons'
import { reportRejection } from '@src/lib/trap' import { reportRejection } from '@src/lib/trap'
import { parseJson } from '@src/lib/utils'
export function useEngineCommands(): [CommandLog[], () => void] { export function useEngineCommands(): [CommandLog[], () => void] {
const [engineCommands, setEngineCommands] = useState<CommandLog[]>( const [engineCommands, setEngineCommands] = useState<CommandLog[]>(
@ -85,7 +84,7 @@ export const EngineCommands = () => {
data-testid="custom-cmd-send-button" data-testid="custom-cmd-send-button"
onClick={() => { onClick={() => {
engineCommandManager engineCommandManager
.sendSceneCommand(parseJson(customCmd)) .sendSceneCommand(JSON.parse(customCmd))
.catch(reportRejection) .catch(reportRejection)
}} }}
> >

View File

@ -0,0 +1,140 @@
import { isDesktop } from '@src/lib/isDesktop'
import { commandBarActor } from '@src/machines/commandBarMachine'
import { SidebarAction } from './ModelingPanes'
import { useAppState } from '@src/AppState'
import { useNetworkContext } from '@src/hooks/useNetworkContext'
import { NetworkHealthState } from '@src/hooks/useNetworkStatus'
import { useKclContext } from '@src/lang/KclProvider'
import { EngineConnectionStateType } from '@src/lang/std/engineConnection'
import { useContext, useMemo } from 'react'
import { MachineManagerContext } from '../MachineManagerProvider'
import { useSettings } from '@src/machines/appMachine'
import { getPlatformString } from '@src/lib/utils'
import { ModelingPaneButton } from './ModelingSidebarButton'
export function ActionSidebar() {
const machineManager = useContext(MachineManagerContext)
const kclContext = useKclContext()
const settings = useSettings()
const { overallState, immediateState } = useNetworkContext()
const { isExecuting } = useKclContext()
const { isStreamReady } = useAppState()
const reliesOnEngine =
(overallState !== NetworkHealthState.Ok &&
overallState !== NetworkHealthState.Weak) ||
isExecuting ||
immediateState.type !== EngineConnectionStateType.ConnectionEstablished ||
!isStreamReady
const paneCallbackProps = useMemo(
() => ({
kclContext,
settings,
platform: getPlatformString(),
}),
[kclContext.diagnostics, settings]
)
const sidebarActions: SidebarAction[] = [
{
id: 'load-external-model',
title: 'Load external model',
sidebarName: 'Load external model',
icon: 'importFile',
keybinding: 'Ctrl + Shift + I',
action: () =>
commandBarActor.send({
type: 'Find and select command',
data: { name: 'load-external-model', groupId: 'code' },
}),
},
{
id: 'share-link',
title: 'Create share link',
sidebarName: 'Create share link',
icon: 'link',
keybinding: 'Mod + Alt + S',
action: () =>
commandBarActor.send({
type: 'Find and select command',
data: { name: 'share-file-link', groupId: 'code' },
}),
},
{
id: 'export',
title: 'Export part',
sidebarName: 'Export part',
icon: 'floppyDiskArrow',
keybinding: 'Ctrl + Shift + E',
disable: () =>
reliesOnEngine ? 'Need engine connection to export' : undefined,
action: () =>
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Export', groupId: 'modeling' },
}),
},
{
id: 'make',
title: 'Make part',
sidebarName: 'Make part',
icon: 'printer3d',
keybinding: 'Ctrl + Shift + M',
// eslint-disable-next-line @typescript-eslint/no-misused-promises
action: async () => {
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Make', groupId: 'modeling' },
})
},
hide: () => !isDesktop(),
disable: () => {
return machineManager.noMachinesReason()
},
},
]
const filteredActions = sidebarActions.filter(
(action) =>
!action.hide ||
(action.hide instanceof Function && !action.hide(paneCallbackProps))
)
return (
<aside
id="action-sidebar"
className="absolute right-0 top-0 bottom-0 flex flex-col z-10 my-2"
>
<ul
className={
'relative rounded-l z-[2] pointer-events-auto p-0 col-start-1 col-span-1 h-fit w-fit flex flex-col ' +
'bg-chalkboard-10 border border-solid border-chalkboard-30 dark:bg-chalkboard-90 dark:border-chalkboard-80 group-focus-within:border-primary dark:group-focus-within:border-chalkboard-50 shadow-sm '
}
>
{filteredActions.length > 0 && (
<>
<ul id="sidebar-actions" className="w-fit p-2 flex flex-col gap-2">
{filteredActions.map((action) => (
<li className="contents">
<ModelingPaneButton
key={action.id}
paneConfig={{
id: action.id,
sidebarName: action.sidebarName,
icon: action.icon,
keybinding: action.keybinding,
iconClassName: action.iconClassName,
iconSize: 'md',
}}
onClick={action.action}
disabledText={action.disable?.()}
tooltipPosition="left"
/>
</li>
))}
</ul>
</>
)}
</ul>
</aside>
)
}

View File

@ -18,7 +18,7 @@
.header { .header {
@apply z-10 relative rounded-tr; @apply z-10 relative rounded-tr;
@apply flex h-[41px] items-center justify-between gap-2 px-2; @apply flex h-[41px] items-center justify-between gap-2 px-2;
@apply font-mono text-xs font-bold select-none text-chalkboard-90; @apply text-xs select-none text-chalkboard-90;
@apply bg-chalkboard-10 border-b border-chalkboard-30; @apply bg-chalkboard-10 border-b border-chalkboard-30;
} }

View File

@ -1,6 +1,6 @@
.button { .button {
@apply flex justify-between items-center gap-2 px-2 py-1 text-left border-none rounded-sm; @apply flex justify-between items-center gap-2 px-2 py-1 text-left border-none rounded-sm;
@apply font-mono !no-underline text-xs font-bold select-none text-chalkboard-90; @apply !no-underline text-xs select-none text-chalkboard-90;
@apply ui-active:bg-primary/10 ui-active:text-primary ui-active:text-inherit; @apply ui-active:bg-primary/10 ui-active:text-primary ui-active:text-inherit;
@apply transition-colors ease-out; @apply transition-colors ease-out;
@apply m-0; @apply m-0;

View File

@ -1,48 +1,28 @@
import type { IconDefinition } from '@fortawesome/free-solid-svg-icons' import type { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { Resizable } from 're-resizable' import { Resizable } from 're-resizable'
import type { MouseEventHandler } from 'react' import type { MouseEventHandler } from 'react'
import { useCallback, useContext, useEffect, useMemo } from 'react' import { useCallback, useEffect, useMemo } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { useAppState } from '@src/AppState'
import { ActionIcon } from '@src/components/ActionIcon' import { ActionIcon } from '@src/components/ActionIcon'
import type { CustomIconName } from '@src/components/CustomIcon' import type { CustomIconName } from '@src/components/CustomIcon'
import { MachineManagerContext } from '@src/components/MachineManagerProvider'
import { ModelingPane } from '@src/components/ModelingSidebar/ModelingPane' import { ModelingPane } from '@src/components/ModelingSidebar/ModelingPane'
import type { import type { SidebarType } from '@src/components/ModelingSidebar/ModelingPanes'
SidebarAction,
SidebarType,
} from '@src/components/ModelingSidebar/ModelingPanes'
import { sidebarPanes } from '@src/components/ModelingSidebar/ModelingPanes' import { sidebarPanes } from '@src/components/ModelingSidebar/ModelingPanes'
import Tooltip from '@src/components/Tooltip' import Tooltip from '@src/components/Tooltip'
import { useModelingContext } from '@src/hooks/useModelingContext' import { useModelingContext } from '@src/hooks/useModelingContext'
import { useNetworkContext } from '@src/hooks/useNetworkContext'
import { NetworkHealthState } from '@src/hooks/useNetworkStatus'
import { useKclContext } from '@src/lang/KclProvider' import { useKclContext } from '@src/lang/KclProvider'
import { EngineConnectionStateType } from '@src/lang/std/engineConnection'
import { SIDEBAR_BUTTON_SUFFIX } from '@src/lib/constants' import { SIDEBAR_BUTTON_SUFFIX } from '@src/lib/constants'
import { isDesktop } from '@src/lib/isDesktop'
import { useSettings } from '@src/machines/appMachine' import { useSettings } from '@src/machines/appMachine'
import { commandBarActor } from '@src/machines/commandBarMachine'
import { onboardingPaths } from '@src/routes/Onboarding/paths' import { onboardingPaths } from '@src/routes/Onboarding/paths'
import { getPlatformString } from '@src/lib/utils'
import { BadgeInfoComputed, ModelingPaneButton } from './ModelingSidebarButton'
interface ModelingSidebarProps { interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40' paneOpacity: '' | 'opacity-20' | 'opacity-40'
} }
interface BadgeInfoComputed {
value: number | boolean | string
onClick?: MouseEventHandler<any>
className?: string
title?: string
}
function getPlatformString(): 'web' | 'desktop' {
return isDesktop() ? 'desktop' : 'web'
}
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) { export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
const machineManager = useContext(MachineManagerContext)
const kclContext = useKclContext() const kclContext = useKclContext()
const settings = useSettings() const settings = useSettings()
const onboardingStatus = settings.app.onboardingStatus const onboardingStatus = settings.app.onboardingStatus
@ -54,16 +34,6 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
: 'pointer-events-auto ' : 'pointer-events-auto '
const showDebugPanel = settings.app.showDebugPanel const showDebugPanel = settings.app.showDebugPanel
const { overallState, immediateState } = useNetworkContext()
const { isExecuting } = useKclContext()
const { isStreamReady } = useAppState()
const reliesOnEngine =
(overallState !== NetworkHealthState.Ok &&
overallState !== NetworkHealthState.Weak) ||
isExecuting ||
immediateState.type !== EngineConnectionStateType.ConnectionEstablished ||
!isStreamReady
const paneCallbackProps = useMemo( const paneCallbackProps = useMemo(
() => ({ () => ({
kclContext, kclContext,
@ -73,70 +43,6 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
[kclContext.diagnostics, settings] [kclContext.diagnostics, settings]
) )
const sidebarActions: SidebarAction[] = [
{
id: 'load-external-model',
title: 'Load external model',
sidebarName: 'Load external model',
icon: 'importFile',
keybinding: 'Ctrl + Shift + I',
action: () =>
commandBarActor.send({
type: 'Find and select command',
data: { name: 'load-external-model', groupId: 'code' },
}),
},
{
id: 'share-link',
title: 'Create share link',
sidebarName: 'Create share link',
icon: 'link',
keybinding: 'Mod + Alt + S',
action: () =>
commandBarActor.send({
type: 'Find and select command',
data: { name: 'share-file-link', groupId: 'code' },
}),
},
{
id: 'export',
title: 'Export part',
sidebarName: 'Export part',
icon: 'floppyDiskArrow',
keybinding: 'Ctrl + Shift + E',
disable: () =>
reliesOnEngine ? 'Need engine connection to export' : undefined,
action: () =>
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Export', groupId: 'modeling' },
}),
},
{
id: 'make',
title: 'Make part',
sidebarName: 'Make part',
icon: 'printer3d',
keybinding: 'Ctrl + Shift + M',
// eslint-disable-next-line @typescript-eslint/no-misused-promises
action: async () => {
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Make', groupId: 'modeling' },
})
},
hide: () => !isDesktop(),
disable: () => {
return machineManager.noMachinesReason()
},
},
]
const filteredActions: SidebarAction[] = sidebarActions.filter(
(action) =>
!action.hide ||
(action.hide instanceof Function && !action.hide(paneCallbackProps))
)
// // Filter out the debug panel if it's not supposed to be shown // // Filter out the debug panel if it's not supposed to be shown
// // TODO: abstract out for allowing user to configure which panes to show // // TODO: abstract out for allowing user to configure which panes to show
const filteredPanes = useMemo( const filteredPanes = useMemo(
@ -257,31 +163,6 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
/> />
))} ))}
</ul> </ul>
{filteredActions.length > 0 && (
<>
<hr className="w-full border-chalkboard-30 dark:border-chalkboard-80" />
<ul
id="sidebar-actions"
className="w-fit p-2 flex flex-col gap-2"
>
{filteredActions.map((action) => (
<ModelingPaneButton
key={action.id}
paneConfig={{
id: action.id,
sidebarName: action.sidebarName,
icon: action.icon,
keybinding: action.keybinding,
iconClassName: action.iconClassName,
iconSize: 'md',
}}
onClick={action.action}
disabledText={action.disable?.()}
/>
))}
</ul>
</>
)}
</ul> </ul>
<ul <ul
id="pane-section" id="pane-section"
@ -315,105 +196,3 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
</Resizable> </Resizable>
) )
} }
interface ModelingPaneButtonProps
extends React.HTMLAttributes<HTMLButtonElement> {
paneConfig: {
id: string
sidebarName: string
icon: CustomIconName | IconDefinition
keybinding: string
iconClassName?: string
iconSize?: 'sm' | 'md' | 'lg'
}
onClick: () => void
paneIsOpen?: boolean
showBadge?: BadgeInfoComputed
disabledText?: string
}
function ModelingPaneButton({
paneConfig,
onClick,
paneIsOpen,
showBadge,
disabledText,
...props
}: ModelingPaneButtonProps) {
useHotkeys(paneConfig.keybinding, onClick, {
scopes: ['modeling'],
})
return (
<div id={paneConfig.id + '-button-holder'} className="relative">
<button
className="group pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent disabled:!border-transparent p-0 m-0 rounded-sm !outline-0 focus-visible:border-primary"
onClick={onClick}
name={paneConfig.sidebarName}
data-testid={paneConfig.id + SIDEBAR_BUTTON_SUFFIX}
disabled={disabledText !== undefined}
aria-disabled={disabledText !== undefined}
{...props}
>
<ActionIcon
icon={paneConfig.icon}
className={paneConfig.iconClassName || ''}
size={paneConfig.iconSize || 'md'}
iconClassName={paneIsOpen ? ' !text-chalkboard-10' : ''}
bgClassName={
'rounded-sm ' + (paneIsOpen ? '!bg-primary' : '!bg-transparent')
}
/>
<span className="sr-only">
{paneConfig.sidebarName}
{paneIsOpen !== undefined ? ` pane` : ''}
</span>
<Tooltip
position="right"
contentClassName="max-w-none flex items-center gap-4"
hoverOnly
>
<span className="flex-1">
{paneConfig.sidebarName}
{disabledText !== undefined ? ` (${disabledText})` : ''}
{paneIsOpen !== undefined ? ` pane` : ''}
</span>
<kbd className="hotkey text-xs capitalize">
{paneConfig.keybinding}
</kbd>
</Tooltip>
</button>
{!!showBadge?.value && (
<p
id={`${paneConfig.id}-badge`}
className={
showBadge.className
? showBadge.className
: 'absolute m-0 p-0 bottom-4 left-4 w-3 h-3 flex items-center justify-center text-[10px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200'
}
onClick={showBadge.onClick}
title={
showBadge.title
? showBadge.title
: `Click to view ${showBadge.value} notification${
Number(showBadge.value) > 1 ? 's' : ''
}`
}
>
<span className="sr-only">&nbsp;has&nbsp;</span>
{typeof showBadge.value === 'number' ||
typeof showBadge.value === 'string' ? (
<span>{showBadge.value}</span>
) : (
<span className="sr-only">a</span>
)}
{typeof showBadge.value === 'number' && (
<span className="sr-only">
&nbsp;notification{Number(showBadge.value) > 1 ? 's' : ''}
</span>
)}
</p>
)}
</div>
)
}

View File

@ -0,0 +1,118 @@
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { SIDEBAR_BUTTON_SUFFIX } from '@src/lib/constants'
import { useHotkeys } from 'react-hotkeys-hook'
import { ActionIcon } from '../ActionIcon'
import { CustomIconName } from '../CustomIcon'
import Tooltip from '../Tooltip'
import { MouseEventHandler } from 'react'
export interface BadgeInfoComputed {
value: number | boolean | string
onClick?: MouseEventHandler<any>
className?: string
title?: string
}
interface ModelingPaneButtonProps
extends React.HTMLAttributes<HTMLButtonElement> {
paneConfig: {
id: string
sidebarName: string
icon: CustomIconName | IconDefinition
keybinding: string
iconClassName?: string
iconSize?: 'sm' | 'md' | 'lg'
}
onClick: () => void
paneIsOpen?: boolean
showBadge?: BadgeInfoComputed
disabledText?: string
tooltipPosition?: 'right' | 'left'
}
export function ModelingPaneButton({
paneConfig,
onClick,
paneIsOpen,
showBadge,
disabledText,
tooltipPosition = 'right',
...props
}: ModelingPaneButtonProps) {
useHotkeys(paneConfig.keybinding, onClick, {
scopes: ['modeling'],
})
return (
<div id={paneConfig.id + '-button-holder'} className="relative">
<button
className="group pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent disabled:!border-transparent p-0 m-0 rounded-sm !outline-0 focus-visible:border-primary"
onClick={onClick}
name={paneConfig.sidebarName}
data-testid={paneConfig.id + SIDEBAR_BUTTON_SUFFIX}
disabled={disabledText !== undefined}
aria-disabled={disabledText !== undefined}
{...props}
>
<ActionIcon
icon={paneConfig.icon}
className={paneConfig.iconClassName || ''}
size={paneConfig.iconSize || 'md'}
iconClassName={paneIsOpen ? ' !text-chalkboard-10' : ''}
bgClassName={
'rounded-sm ' + (paneIsOpen ? '!bg-primary' : '!bg-transparent')
}
/>
<span className="sr-only">
{paneConfig.sidebarName}
{paneIsOpen !== undefined ? ` pane` : ''}
</span>
<Tooltip
position={tooltipPosition}
contentClassName="max-w-none flex items-center gap-4"
hoverOnly
>
<span className="flex-1">
{paneConfig.sidebarName}
{disabledText !== undefined ? ` (${disabledText})` : ''}
{paneIsOpen !== undefined ? ` pane` : ''}
</span>
<kbd className="hotkey text-xs capitalize">
{paneConfig.keybinding}
</kbd>
</Tooltip>
</button>
{!!showBadge?.value && (
<p
id={`${paneConfig.id}-badge`}
className={
showBadge.className
? showBadge.className
: 'absolute m-0 p-0 bottom-4 left-4 w-3 h-3 flex items-center justify-center text-[10px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200'
}
onClick={showBadge.onClick}
title={
showBadge.title
? showBadge.title
: `Click to view ${showBadge.value} notification${
Number(showBadge.value) > 1 ? 's' : ''
}`
}
>
<span className="sr-only">&nbsp;has&nbsp;</span>
{typeof showBadge.value === 'number' ||
typeof showBadge.value === 'string' ? (
<span>{showBadge.value}</span>
) : (
<span className="sr-only">a</span>
)}
{typeof showBadge.value === 'number' && (
<span className="sr-only">
&nbsp;notification{Number(showBadge.value) > 1 ? 's' : ''}
</span>
)}
</p>
)}
</div>
)
}

View File

@ -37,7 +37,7 @@ export class KCLError extends Error {
filenames: { [x: number]: ModulePath | undefined }, filenames: { [x: number]: ModulePath | undefined },
defaultPlanes: DefaultPlanes | null defaultPlanes: DefaultPlanes | null
) { ) {
super(`${kind}: ${msg}`) super()
this.kind = kind this.kind = kind
this.msg = msg this.msg = msg
this.sourceRange = sourceRange this.sourceRange = sourceRange

View File

@ -4,7 +4,6 @@ import path from 'node:path'
import { assertParse } from '@src/lang/wasm' import { assertParse } from '@src/lang/wasm'
import { initPromise } from '@src/lang/wasmUtils' import { initPromise } from '@src/lang/wasmUtils'
import { enginelessExecutor } from '@src/lib/testHelpers' import { enginelessExecutor } from '@src/lib/testHelpers'
import { parseJson } from '@src/lib/utils'
// The purpose of these tests is to act as a first line of defense // The purpose of these tests is to act as a first line of defense
// if something gets real screwy with our KCL ecosystem. // if something gets real screwy with our KCL ecosystem.
@ -28,7 +27,7 @@ const manifestJsonStr = await fs.readFile(
path.resolve(DIR_KCL_SAMPLES, 'manifest.json'), path.resolve(DIR_KCL_SAMPLES, 'manifest.json'),
'utf-8' 'utf-8'
) )
const manifest: KclSampleFile[] = parseJson(manifestJsonStr) const manifest = JSON.parse(manifestJsonStr)
process.chdir(DIR_KCL_SAMPLES) process.chdir(DIR_KCL_SAMPLES)

View File

@ -23,7 +23,7 @@ import {
getThemeColorForEngine, getThemeColorForEngine,
} from '@src/lib/theme' } from '@src/lib/theme'
import { reportRejection } from '@src/lib/trap' import { reportRejection } from '@src/lib/trap'
import { binaryToUuid, parseJson, uuidv4 } from '@src/lib/utils' import { binaryToUuid, uuidv4 } from '@src/lib/utils'
const pingIntervalMs = 1_000 const pingIntervalMs = 1_000
@ -390,7 +390,7 @@ class EngineConnection extends EventTarget {
this.websocket.addEventListener('open', this.onWebSocketOpen) this.websocket.addEventListener('open', this.onWebSocketOpen)
this.websocket?.addEventListener('message', ((event: MessageEvent) => { this.websocket?.addEventListener('message', ((event: MessageEvent) => {
const message: Models['WebSocketResponse_type'] = parseJson(event.data) const message: Models['WebSocketResponse_type'] = JSON.parse(event.data)
const pending = const pending =
this.engineCommandManager.pendingCommands[message.request_id || ''] this.engineCommandManager.pendingCommands[message.request_id || '']
if (!('resp' in message)) return if (!('resp' in message)) return
@ -862,7 +862,7 @@ class EngineConnection extends EventTarget {
) )
this.onDataChannelMessage = (event) => { this.onDataChannelMessage = (event) => {
const result: UnreliableResponses = parseJson(event.data) const result: UnreliableResponses = JSON.parse(event.data)
Object.values( Object.values(
this.engineCommandManager.unreliableSubscriptions[result.type] || this.engineCommandManager.unreliableSubscriptions[result.type] ||
{} {}
@ -976,7 +976,7 @@ class EngineConnection extends EventTarget {
return return
} }
const message: Models['WebSocketResponse_type'] = parseJson( const message: Models['WebSocketResponse_type'] = JSON.parse(
event.data event.data
) )
@ -1564,7 +1564,7 @@ export class EngineCommandManager extends EventTarget {
unreliableDataChannel.addEventListener( unreliableDataChannel.addEventListener(
'message', 'message',
(event: MessageEvent) => { (event: MessageEvent) => {
const result: UnreliableResponses = parseJson(event.data) const result: UnreliableResponses = JSON.parse(event.data)
Object.values( Object.values(
this.unreliableSubscriptions[result.type] || {} this.unreliableSubscriptions[result.type] || {}
).forEach( ).forEach(
@ -1608,7 +1608,7 @@ export class EngineCommandManager extends EventTarget {
message.request_id = binaryToUuid(message.request_id) message.request_id = binaryToUuid(message.request_id)
} }
} else { } else {
message = parseJson(event.data) message = JSON.parse(event.data)
} }
if (message === null) { if (message === null) {

View File

@ -40,7 +40,7 @@ import type { CoreDumpManager } from '@src/lib/coredump'
import openWindow from '@src/lib/openWindow' import openWindow from '@src/lib/openWindow'
import { Reason, err } from '@src/lib/trap' import { Reason, err } from '@src/lib/trap'
import type { DeepPartial } from '@src/lib/types' import type { DeepPartial } from '@src/lib/types'
import { isArray, parseJson } from '@src/lib/utils' import { isArray } from '@src/lib/utils'
import { import {
base64_decode, base64_decode,
change_kcl_settings, change_kcl_settings,
@ -217,7 +217,9 @@ export const parse = (code: string | Error): ParseResult | Error => {
let errs = splitErrors(parsed[1]) let errs = splitErrors(parsed[1])
return new ParseResult(parsed[0], errs.errors, errs.warnings) return new ParseResult(parsed[0], errs.errors, errs.warnings)
} catch (e: any) { } catch (e: any) {
const parsed: RustKclError = parseJson(e.toString()) // throw e
console.error(e.toString())
const parsed: RustKclError = JSON.parse(e.toString())
return new KCLError( return new KCLError(
parsed.kind, parsed.kind,
parsed.msg, parsed.msg,
@ -379,7 +381,7 @@ export function sketchFromKclValue(
} }
export const errFromErrWithOutputs = (e: any): KCLError => { export const errFromErrWithOutputs = (e: any): KCLError => {
const parsed: KclErrorWithOutputs = parseJson(e.toString()) const parsed: KclErrorWithOutputs = JSON.parse(e.toString())
return new KCLError( return new KCLError(
parsed.error.kind, parsed.error.kind,
parsed.error.msg, parsed.error.msg,

View File

@ -3,7 +3,7 @@ import type { ApiError_type } from '@kittycad/lib/dist/types/src/models'
import type { Selections } from '@src/lib/selections' import type { Selections } from '@src/lib/selections'
import { engineCommandManager, kclManager } from '@src/lib/singletons' import { engineCommandManager, kclManager } from '@src/lib/singletons'
import { parseJson, uuidv4 } from '@src/lib/utils' import { uuidv4 } from '@src/lib/utils'
import type { CommandBarContext } from '@src/machines/commandBarMachine' import type { CommandBarContext } from '@src/machines/commandBarMachine'
export const disableDryRunWithRetry = async (numberOfRetries = 3) => { export const disableDryRunWithRetry = async (numberOfRetries = 3) => {
@ -54,7 +54,7 @@ export function parseEngineErrorMessage(engineError: string) {
return undefined return undefined
} }
const errors = parseJson<ApiError_type[]>(parts[1]) const errors = JSON.parse(parts[1]) as ApiError_type[]
if (!errors[0]) { if (!errors[0]) {
return undefined return undefined
} }

View File

@ -26,7 +26,6 @@ import type { DeepPartial } from '@src/lib/types'
import type { ModuleType } from '@src/lib/wasm_lib_wrapper' import type { ModuleType } from '@src/lib/wasm_lib_wrapper'
import { getModule } from '@src/lib/wasm_lib_wrapper' import { getModule } from '@src/lib/wasm_lib_wrapper'
import type { Models } from '@kittycad/lib/dist/types/src' import type { Models } from '@kittycad/lib/dist/types/src'
import { parseJson } from '@src/lib/utils'
export default class RustContext { export default class RustContext {
private wasmInitFailed: boolean = true private wasmInitFailed: boolean = true
@ -141,7 +140,7 @@ export default class RustContext {
JSON.stringify(settings) JSON.stringify(settings)
) )
} catch (e: any) { } catch (e: any) {
const parsed: RustKclError = parseJson(e.toString()) const parsed: RustKclError = JSON.parse(e.toString())
toast.error(parsed.msg, { id: toastId }) toast.error(parsed.msg, { id: toastId })
return return
} }

View File

@ -8,6 +8,13 @@ import type { AsyncFn } from '@src/lib/types'
export const uuidv4 = v4 export const uuidv4 = v4
/**
* Get the current platform as a string.
*/
export function getPlatformString(): 'web' | 'desktop' {
return isDesktop() ? 'desktop' : 'web'
}
/** /**
* Get all labels for a keyword call expression. * Get all labels for a keyword call expression.
*/ */
@ -60,26 +67,6 @@ export function isNonNullable<T>(val: T): val is NonNullable<T> {
return val !== null && val !== undefined return val !== null && val !== undefined
} }
/**
* Same as JSON.parse() but if it fails, includes the string that was attempted
* to be parsed in the error message. This is useful since a lot of times this
* is called on a string that isn't actually JSON, like another error message.
*
* You can also use the type parameter to assert the expected type of the parsed
* object.
*/
export function parseJson<T>(
text: string,
reviver?: (this: any, key: string, value: any) => any
): T {
try {
return JSON.parse(text, reviver)
} catch (e) {
// eslint-disable-next-line suggest-no-throw/suggest-no-throw
throw new Error(`Failed to parse JSON: ${text}`, { cause: e })
}
}
export function isOverlap(a: SourceRange, b: SourceRange) { export function isOverlap(a: SourceRange, b: SourceRange) {
const [startingRange, secondRange] = a[0] < b[0] ? [a, b] : [b, a] const [startingRange, secondRange] = a[0] < b[0] ? [a, b] : [b, a]
const [lastOfFirst, firstOfSecond] = [startingRange[1], secondRange[0]] const [lastOfFirst, firstOfSecond] = [startingRange[1], secondRange[0]]