Merge remote-tracking branch 'origin' into kurt-circle
This commit is contained in:
@ -13,6 +13,8 @@
|
|||||||
"plugin:css-modules/recommended"
|
"plugin:css-modules/recommended"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"@typescript-eslint/no-floating-promises": "error",
|
||||||
|
"@typescript-eslint/no-misused-promises": "error",
|
||||||
"semi": [
|
"semi": [
|
||||||
"error",
|
"error",
|
||||||
"never"
|
"never"
|
||||||
@ -24,7 +26,6 @@
|
|||||||
{
|
{
|
||||||
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
|
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/no-floating-promises": "warn",
|
|
||||||
"suggest-no-throw/suggest-no-throw": "off",
|
"suggest-no-throw/suggest-no-throw": "off",
|
||||||
"testing-library/prefer-screen-queries": "off",
|
"testing-library/prefer-screen-queries": "off",
|
||||||
"jest/valid-expect": "off"
|
"jest/valid-expect": "off"
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
@ -27,6 +27,7 @@ import * as TOML from '@iarna/toml'
|
|||||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||||
import { SETTINGS_FILE_NAME } from 'lib/constants'
|
import { SETTINGS_FILE_NAME } from 'lib/constants'
|
||||||
import { isArray } from 'lib/utils'
|
import { isArray } from 'lib/utils'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
type TestColor = [number, number, number]
|
type TestColor = [number, number, number]
|
||||||
export const TEST_COLORS = {
|
export const TEST_COLORS = {
|
||||||
@ -439,46 +440,50 @@ export async function getUtils(page: Page, test_?: typeof test) {
|
|||||||
}
|
}
|
||||||
return maxDiff
|
return maxDiff
|
||||||
},
|
},
|
||||||
doAndWaitForImageDiff: (fn: () => Promise<any>, diffCount = 200) =>
|
doAndWaitForImageDiff: (fn: () => Promise<unknown>, diffCount = 200) =>
|
||||||
new Promise(async (resolve) => {
|
new Promise<boolean>((resolve) => {
|
||||||
await page.screenshot({
|
;(async () => {
|
||||||
path: './e2e/playwright/temp1.png',
|
|
||||||
fullPage: true,
|
|
||||||
})
|
|
||||||
await fn()
|
|
||||||
const isImageDiff = async () => {
|
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: './e2e/playwright/temp2.png',
|
path: './e2e/playwright/temp1.png',
|
||||||
fullPage: true,
|
fullPage: true,
|
||||||
})
|
})
|
||||||
const screenshot1 = PNG.sync.read(
|
await fn()
|
||||||
await fsp.readFile('./e2e/playwright/temp1.png')
|
const isImageDiff = async () => {
|
||||||
)
|
await page.screenshot({
|
||||||
const screenshot2 = PNG.sync.read(
|
path: './e2e/playwright/temp2.png',
|
||||||
await fsp.readFile('./e2e/playwright/temp2.png')
|
fullPage: true,
|
||||||
)
|
})
|
||||||
const actualDiffCount = pixelMatch(
|
const screenshot1 = PNG.sync.read(
|
||||||
screenshot1.data,
|
await fsp.readFile('./e2e/playwright/temp1.png')
|
||||||
screenshot2.data,
|
)
|
||||||
null,
|
const screenshot2 = PNG.sync.read(
|
||||||
screenshot1.width,
|
await fsp.readFile('./e2e/playwright/temp2.png')
|
||||||
screenshot2.height
|
)
|
||||||
)
|
const actualDiffCount = pixelMatch(
|
||||||
return actualDiffCount > diffCount
|
screenshot1.data,
|
||||||
}
|
screenshot2.data,
|
||||||
|
null,
|
||||||
// run isImageDiff every 50ms until it returns true or 5 seconds have passed (100 times)
|
screenshot1.width,
|
||||||
let count = 0
|
screenshot2.height
|
||||||
const interval = setInterval(async () => {
|
)
|
||||||
count++
|
return actualDiffCount > diffCount
|
||||||
if (await isImageDiff()) {
|
|
||||||
clearInterval(interval)
|
|
||||||
resolve(true)
|
|
||||||
} else if (count > 100) {
|
|
||||||
clearInterval(interval)
|
|
||||||
resolve(false)
|
|
||||||
}
|
}
|
||||||
}, 50)
|
|
||||||
|
// run isImageDiff every 50ms until it returns true or 5 seconds have passed (100 times)
|
||||||
|
let count = 0
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
;(async () => {
|
||||||
|
count++
|
||||||
|
if (await isImageDiff()) {
|
||||||
|
clearInterval(interval)
|
||||||
|
resolve(true)
|
||||||
|
} else if (count > 100) {
|
||||||
|
clearInterval(interval)
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
})().catch(reportRejection)
|
||||||
|
}, 50)
|
||||||
|
})().catch(reportRejection)
|
||||||
}),
|
}),
|
||||||
emulateNetworkConditions: async (
|
emulateNetworkConditions: async (
|
||||||
networkOptions: Protocol.Network.emulateNetworkConditionsParameters
|
networkOptions: Protocol.Network.emulateNetworkConditionsParameters
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
"@ts-stack/markdown": "^1.5.0",
|
"@ts-stack/markdown": "^1.5.0",
|
||||||
"@tweenjs/tween.js": "^23.1.1",
|
"@tweenjs/tween.js": "^23.1.1",
|
||||||
"@xstate/inspect": "^0.8.0",
|
"@xstate/inspect": "^0.8.0",
|
||||||
"@xstate/react": "^3.2.2",
|
"@xstate/react": "^4.1.1",
|
||||||
"bonjour-service": "^1.2.1",
|
"bonjour-service": "^1.2.1",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"decamelize": "^6.0.0",
|
"decamelize": "^6.0.0",
|
||||||
@ -64,7 +64,7 @@
|
|||||||
"vscode-languageserver-protocol": "^3.17.5",
|
"vscode-languageserver-protocol": "^3.17.5",
|
||||||
"vscode-uri": "^3.0.8",
|
"vscode-uri": "^3.0.8",
|
||||||
"web-vitals": "^3.5.2",
|
"web-vitals": "^3.5.2",
|
||||||
"xstate": "^4.38.2"
|
"xstate": "^5.17.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
|
@ -72,6 +72,7 @@ export class LanguageServerClient {
|
|||||||
async initialize() {
|
async initialize() {
|
||||||
// Start the client in the background.
|
// Start the client in the background.
|
||||||
this.client.setNotifyFn(this.processNotifications.bind(this))
|
this.client.setNotifyFn(this.processNotifications.bind(this))
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.client.start()
|
this.client.start()
|
||||||
|
|
||||||
this.ready = true
|
this.ready = true
|
||||||
@ -195,6 +196,9 @@ export class LanguageServerClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private processNotifications(notification: LSP.NotificationMessage) {
|
private processNotifications(notification: LSP.NotificationMessage) {
|
||||||
for (const plugin of this.plugins) plugin.processNotification(notification)
|
for (const plugin of this.plugins) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
plugin.processNotification(notification)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ export default function lspFormatExt(
|
|||||||
run: (view: EditorView) => {
|
run: (view: EditorView) => {
|
||||||
let value = view.plugin(plugin)
|
let value = view.plugin(plugin)
|
||||||
if (!value) return false
|
if (!value) return false
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
value.requestFormatting()
|
value.requestFormatting()
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
@ -117,6 +117,7 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
|
|
||||||
this.processLspNotification = options.processLspNotification
|
this.processLspNotification = options.processLspNotification
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.initialize({
|
this.initialize({
|
||||||
documentText: this.getDocText(),
|
documentText: this.getDocText(),
|
||||||
})
|
})
|
||||||
@ -149,6 +150,7 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initialize({ documentText }: { documentText: string }) {
|
async initialize({ documentText }: { documentText: string }) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
if (this.client.initializePromise) {
|
if (this.client.initializePromise) {
|
||||||
await this.client.initializePromise
|
await this.client.initializePromise
|
||||||
}
|
}
|
||||||
@ -162,7 +164,9 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.requestSemanticTokens()
|
this.requestSemanticTokens()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.updateFoldingRanges()
|
this.updateFoldingRanges()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,7 +229,9 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
contentChanges: [{ text: this.view.state.doc.toString() }],
|
contentChanges: [{ text: this.view.state.doc.toString() }],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.requestSemanticTokens()
|
this.requestSemanticTokens()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.updateFoldingRanges()
|
this.updateFoldingRanges()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
@ -26,6 +26,7 @@ import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
|||||||
import Gizmo from 'components/Gizmo'
|
import Gizmo from 'components/Gizmo'
|
||||||
import { CoreDumpManager } from 'lib/coredump'
|
import { CoreDumpManager } from 'lib/coredump'
|
||||||
import { UnitsMenu } from 'components/UnitsMenu'
|
import { UnitsMenu } from 'components/UnitsMenu'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const { project, file } = useLoaderData() as IndexLoaderData
|
const { project, file } = useLoaderData() as IndexLoaderData
|
||||||
@ -80,7 +81,7 @@ export function App() {
|
|||||||
useEngineConnectionSubscriptions()
|
useEngineConnectionSubscriptions()
|
||||||
|
|
||||||
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
||||||
engineCommandManager.sendSceneCommand(message)
|
engineCommandManager.sendSceneCommand(message).catch(reportRejection)
|
||||||
}, 1000 / 15)
|
}, 1000 / 15)
|
||||||
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
|
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||||
if (state.matches('Sketch')) {
|
if (state.matches('Sketch')) {
|
||||||
@ -95,7 +96,7 @@ export function App() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const newCmdId = uuidv4()
|
const newCmdId = uuidv4()
|
||||||
if (state.matches('idle.showPlanes')) return
|
if (state.matches({ idle: 'showPlanes' })) return
|
||||||
if (context.store?.buttonDownInStream !== undefined) return
|
if (context.store?.buttonDownInStream !== undefined) return
|
||||||
debounceSocketSend({
|
debounceSocketSend({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
|
@ -41,6 +41,7 @@ import toast from 'react-hot-toast'
|
|||||||
import { coreDump } from 'lang/wasm'
|
import { coreDump } from 'lang/wasm'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { AppStateProvider } from 'AppState'
|
import { AppStateProvider } from 'AppState'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
||||||
|
|
||||||
@ -173,21 +174,23 @@ function CoreDump() {
|
|||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
useHotkeyWrapper(['mod + shift + .'], () => {
|
useHotkeyWrapper(['mod + shift + .'], () => {
|
||||||
toast.promise(
|
toast
|
||||||
coreDump(coreDumpManager, true),
|
.promise(
|
||||||
{
|
coreDump(coreDumpManager, true),
|
||||||
loading: 'Starting core dump...',
|
{
|
||||||
success: 'Core dump completed successfully',
|
loading: 'Starting core dump...',
|
||||||
error: 'Error while exporting core dump',
|
success: 'Core dump completed successfully',
|
||||||
},
|
error: 'Error while exporting core dump',
|
||||||
{
|
|
||||||
success: {
|
|
||||||
// Note: this extended duration is especially important for Playwright e2e testing
|
|
||||||
// default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
|
|
||||||
duration: 6000,
|
|
||||||
},
|
},
|
||||||
}
|
{
|
||||||
)
|
success: {
|
||||||
|
// Note: this extended duration is especially important for Playwright e2e testing
|
||||||
|
// default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
|
||||||
|
duration: 6000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch(reportRejection)
|
||||||
})
|
})
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -70,12 +70,12 @@ export function Toolbar({
|
|||||||
*/
|
*/
|
||||||
const configCallbackProps: ToolbarItemCallbackProps = useMemo(
|
const configCallbackProps: ToolbarItemCallbackProps = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
modelingStateMatches: state.matches,
|
modelingState: state,
|
||||||
modelingSend: send,
|
modelingSend: send,
|
||||||
commandBarSend,
|
commandBarSend,
|
||||||
sketchPathId,
|
sketchPathId,
|
||||||
}),
|
}),
|
||||||
[state.matches, send, commandBarSend, sketchPathId]
|
[state, send, commandBarSend, sketchPathId]
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,11 +22,12 @@ import {
|
|||||||
UnreliableSubscription,
|
UnreliableSubscription,
|
||||||
} from 'lang/std/engineConnection'
|
} from 'lang/std/engineConnection'
|
||||||
import { EngineCommand } from 'lang/std/artifactGraph'
|
import { EngineCommand } from 'lang/std/artifactGraph'
|
||||||
import { uuidv4 } from 'lib/utils'
|
import { toSync, uuidv4 } from 'lib/utils'
|
||||||
import { deg2Rad } from 'lib/utils2d'
|
import { deg2Rad } from 'lib/utils2d'
|
||||||
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
|
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
|
||||||
import * as TWEEN from '@tweenjs/tween.js'
|
import * as TWEEN from '@tweenjs/tween.js'
|
||||||
import { isQuaternionVertical } from './helpers'
|
import { isQuaternionVertical } from './helpers'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
const ORTHOGRAPHIC_CAMERA_SIZE = 20
|
const ORTHOGRAPHIC_CAMERA_SIZE = 20
|
||||||
const FRAMES_TO_ANIMATE_IN = 30
|
const FRAMES_TO_ANIMATE_IN = 30
|
||||||
@ -100,6 +101,7 @@ export class CameraControls {
|
|||||||
camProps.type === 'perspective' &&
|
camProps.type === 'perspective' &&
|
||||||
this.camera instanceof OrthographicCamera
|
this.camera instanceof OrthographicCamera
|
||||||
) {
|
) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.usePerspectiveCamera()
|
this.usePerspectiveCamera()
|
||||||
} else if (
|
} else if (
|
||||||
camProps.type === 'orthographic' &&
|
camProps.type === 'orthographic' &&
|
||||||
@ -127,6 +129,7 @@ export class CameraControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
throttledEngCmd = throttle((cmd: EngineCommand) => {
|
throttledEngCmd = throttle((cmd: EngineCommand) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.engineCommandManager.sendSceneCommand(cmd)
|
this.engineCommandManager.sendSceneCommand(cmd)
|
||||||
}, 1000 / 30)
|
}, 1000 / 30)
|
||||||
|
|
||||||
@ -139,6 +142,7 @@ export class CameraControls {
|
|||||||
...convertThreeCamValuesToEngineCam(threeValues),
|
...convertThreeCamValuesToEngineCam(threeValues),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.engineCommandManager.sendSceneCommand(cmd)
|
this.engineCommandManager.sendSceneCommand(cmd)
|
||||||
}, 1000 / 15)
|
}, 1000 / 15)
|
||||||
|
|
||||||
@ -151,6 +155,7 @@ export class CameraControls {
|
|||||||
this.lastPerspectiveCmd &&
|
this.lastPerspectiveCmd &&
|
||||||
Date.now() - this.lastPerspectiveCmdTime >= lastCmdDelay
|
Date.now() - this.lastPerspectiveCmdTime >= lastCmdDelay
|
||||||
) {
|
) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.engineCommandManager.sendSceneCommand(this.lastPerspectiveCmd, true)
|
this.engineCommandManager.sendSceneCommand(this.lastPerspectiveCmd, true)
|
||||||
this.lastPerspectiveCmdTime = Date.now()
|
this.lastPerspectiveCmdTime = Date.now()
|
||||||
}
|
}
|
||||||
@ -218,6 +223,7 @@ export class CameraControls {
|
|||||||
this.useOrthographicCamera()
|
this.useOrthographicCamera()
|
||||||
}
|
}
|
||||||
if (this.camera instanceof OrthographicCamera && !camSettings.ortho) {
|
if (this.camera instanceof OrthographicCamera && !camSettings.ortho) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.usePerspectiveCamera()
|
this.usePerspectiveCamera()
|
||||||
}
|
}
|
||||||
if (this.camera instanceof PerspectiveCamera && camSettings.fov_y) {
|
if (this.camera instanceof PerspectiveCamera && camSettings.fov_y) {
|
||||||
@ -249,6 +255,7 @@ export class CameraControls {
|
|||||||
const doZoom = () => {
|
const doZoom = () => {
|
||||||
if (this.zoomDataFromLastFrame !== undefined) {
|
if (this.zoomDataFromLastFrame !== undefined) {
|
||||||
this.handleStart()
|
this.handleStart()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.engineCommandManager.sendSceneCommand({
|
this.engineCommandManager.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd: {
|
cmd: {
|
||||||
@ -266,6 +273,7 @@ export class CameraControls {
|
|||||||
|
|
||||||
const doMove = () => {
|
const doMove = () => {
|
||||||
if (this.moveDataFromLastFrame !== undefined) {
|
if (this.moveDataFromLastFrame !== undefined) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.engineCommandManager.sendSceneCommand({
|
this.engineCommandManager.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd: {
|
cmd: {
|
||||||
@ -459,6 +467,7 @@ export class CameraControls {
|
|||||||
|
|
||||||
this.camera.quaternion.set(qx, qy, qz, qw)
|
this.camera.quaternion.set(qx, qy, qz, qw)
|
||||||
this.camera.updateProjectionMatrix()
|
this.camera.updateProjectionMatrix()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.engineCommandManager.sendSceneCommand({
|
this.engineCommandManager.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
@ -929,6 +938,7 @@ export class CameraControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isReducedMotion()) {
|
if (isReducedMotion()) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
onComplete()
|
onComplete()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -937,7 +947,7 @@ export class CameraControls {
|
|||||||
.to({ t: tweenEnd }, duration)
|
.to({ t: tweenEnd }, duration)
|
||||||
.easing(TWEEN.Easing.Quadratic.InOut)
|
.easing(TWEEN.Easing.Quadratic.InOut)
|
||||||
.onUpdate(({ t }) => cameraAtTime(t))
|
.onUpdate(({ t }) => cameraAtTime(t))
|
||||||
.onComplete(onComplete)
|
.onComplete(toSync(onComplete, reportRejection))
|
||||||
.start()
|
.start()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -962,6 +972,7 @@ export class CameraControls {
|
|||||||
// Decrease the FOV
|
// Decrease the FOV
|
||||||
currentFov = Math.max(currentFov - fovAnimationStep, targetFov)
|
currentFov = Math.max(currentFov - fovAnimationStep, targetFov)
|
||||||
this.camera.updateProjectionMatrix()
|
this.camera.updateProjectionMatrix()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.dollyZoom(currentFov)
|
this.dollyZoom(currentFov)
|
||||||
requestAnimationFrame(animateFovChange) // Continue the animation
|
requestAnimationFrame(animateFovChange) // Continue the animation
|
||||||
} else if (frameWaitOnFinish > 0) {
|
} else if (frameWaitOnFinish > 0) {
|
||||||
@ -991,6 +1002,7 @@ export class CameraControls {
|
|||||||
this.lastPerspectiveFov = 4
|
this.lastPerspectiveFov = 4
|
||||||
let currentFov = 4
|
let currentFov = 4
|
||||||
const initialCameraUp = this.camera.up.clone()
|
const initialCameraUp = this.camera.up.clone()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.usePerspectiveCamera()
|
this.usePerspectiveCamera()
|
||||||
const tempVec = new Vector3()
|
const tempVec = new Vector3()
|
||||||
|
|
||||||
@ -999,6 +1011,7 @@ export class CameraControls {
|
|||||||
this.lastPerspectiveFov + (targetFov - this.lastPerspectiveFov) * t
|
this.lastPerspectiveFov + (targetFov - this.lastPerspectiveFov) * t
|
||||||
const currentUp = tempVec.lerpVectors(initialCameraUp, targetCamUp, t)
|
const currentUp = tempVec.lerpVectors(initialCameraUp, targetCamUp, t)
|
||||||
this.camera.up.copy(currentUp)
|
this.camera.up.copy(currentUp)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.dollyZoom(currentFov)
|
this.dollyZoom(currentFov)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1027,6 +1040,7 @@ export class CameraControls {
|
|||||||
this.lastPerspectiveFov = 4
|
this.lastPerspectiveFov = 4
|
||||||
let currentFov = 4
|
let currentFov = 4
|
||||||
const initialCameraUp = this.camera.up.clone()
|
const initialCameraUp = this.camera.up.clone()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.usePerspectiveCamera()
|
this.usePerspectiveCamera()
|
||||||
const tempVec = new Vector3()
|
const tempVec = new Vector3()
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import { cameraMouseDragGuards } from 'lib/cameraControls'
|
|||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
|
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
|
||||||
import { ReactCameraProperties } from './CameraControls'
|
import { ReactCameraProperties } from './CameraControls'
|
||||||
import { throttle } from 'lib/utils'
|
import { throttle, toSync } from 'lib/utils'
|
||||||
import {
|
import {
|
||||||
sceneInfra,
|
sceneInfra,
|
||||||
kclManager,
|
kclManager,
|
||||||
@ -43,7 +43,7 @@ import {
|
|||||||
removeSingleConstraintInfo,
|
removeSingleConstraintInfo,
|
||||||
} from 'lang/modifyAst'
|
} from 'lang/modifyAst'
|
||||||
import { ActionButton } from 'components/ActionButton'
|
import { ActionButton } from 'components/ActionButton'
|
||||||
import { err, trap } from 'lib/trap'
|
import { err, reportRejection, trap } from 'lib/trap'
|
||||||
|
|
||||||
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
||||||
const [isCamMoving, setIsCamMoving] = useState(false)
|
const [isCamMoving, setIsCamMoving] = useState(false)
|
||||||
@ -123,10 +123,10 @@ export const ClientSideScene = ({
|
|||||||
} else if (context.mouseState.type === 'isDragging') {
|
} else if (context.mouseState.type === 'isDragging') {
|
||||||
cursor = 'grabbing'
|
cursor = 'grabbing'
|
||||||
} else if (
|
} else if (
|
||||||
state.matches('Sketch.Line tool') ||
|
state.matches({ Sketch: 'Line tool' }) ||
|
||||||
state.matches('Sketch.Tangential arc to') ||
|
state.matches({ Sketch: 'Tangential arc to' }) ||
|
||||||
state.matches('Sketch.Rectangle tool') ||
|
state.matches({ Sketch: 'Rectangle tool' }) ||
|
||||||
state.matches('Sketch.Circle tool')
|
state.matches({ Sketch: 'Circle tool' })
|
||||||
) {
|
) {
|
||||||
cursor = 'crosshair'
|
cursor = 'crosshair'
|
||||||
} else {
|
} else {
|
||||||
@ -214,9 +214,9 @@ const Overlay = ({
|
|||||||
overlay.visible &&
|
overlay.visible &&
|
||||||
typeof context?.segmentHoverMap?.[pathToNodeString] === 'number' &&
|
typeof context?.segmentHoverMap?.[pathToNodeString] === 'number' &&
|
||||||
!(
|
!(
|
||||||
state.matches('Sketch.Line tool') ||
|
state.matches({ Sketch: 'Line tool' }) ||
|
||||||
state.matches('Sketch.Tangential arc to') ||
|
state.matches({ Sketch: 'Tangential arc to' }) ||
|
||||||
state.matches('Sketch.Rectangle tool')
|
state.matches({ Sketch: 'Rectangle tool' })
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -587,7 +587,7 @@ const ConstraintSymbol = ({
|
|||||||
}}
|
}}
|
||||||
// disabled={isConstrained || !convertToVarEnabled}
|
// disabled={isConstrained || !convertToVarEnabled}
|
||||||
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
|
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
|
||||||
onClick={async () => {
|
onClick={toSync(async () => {
|
||||||
if (!isConstrained) {
|
if (!isConstrained) {
|
||||||
send({
|
send({
|
||||||
type: 'Convert to variable',
|
type: 'Convert to variable',
|
||||||
@ -618,13 +618,14 @@ const ConstraintSymbol = ({
|
|||||||
)
|
)
|
||||||
if (!transform) return
|
if (!transform) return
|
||||||
const { modifiedAst } = transform
|
const { modifiedAst } = transform
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
kclManager.updateAst(modifiedAst, true)
|
kclManager.updateAst(modifiedAst, true)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('error', e)
|
console.log('error', e)
|
||||||
}
|
}
|
||||||
toast.success('Constraint removed')
|
toast.success('Constraint removed')
|
||||||
}
|
}
|
||||||
}}
|
}, reportRejection)}
|
||||||
>
|
>
|
||||||
<CustomIcon name={name} />
|
<CustomIcon name={name} />
|
||||||
</button>
|
</button>
|
||||||
@ -690,7 +691,7 @@ const ConstraintSymbol = ({
|
|||||||
|
|
||||||
const throttled = throttle((a: ReactCameraProperties) => {
|
const throttled = throttle((a: ReactCameraProperties) => {
|
||||||
if (a.type === 'perspective' && a.fov) {
|
if (a.type === 'perspective' && a.fov) {
|
||||||
sceneInfra.camControls.dollyZoom(a.fov)
|
sceneInfra.camControls.dollyZoom(a.fov).catch(reportRejection)
|
||||||
}
|
}
|
||||||
}, 1000 / 15)
|
}, 1000 / 15)
|
||||||
|
|
||||||
@ -720,6 +721,7 @@ export const CamDebugSettings = () => {
|
|||||||
if (camSettings.type === 'perspective') {
|
if (camSettings.type === 'perspective') {
|
||||||
sceneInfra.camControls.useOrthographicCamera()
|
sceneInfra.camControls.useOrthographicCamera()
|
||||||
} else {
|
} else {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
sceneInfra.camControls.usePerspectiveCamera(true)
|
sceneInfra.camControls.usePerspectiveCamera(true)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -727,7 +729,7 @@ export const CamDebugSettings = () => {
|
|||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
sceneInfra.camControls.resetCameraPosition()
|
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reset Camera Position
|
Reset Camera Position
|
||||||
|
@ -106,7 +106,7 @@ import {
|
|||||||
updateRectangleSketch,
|
updateRectangleSketch,
|
||||||
} from 'lib/rectangleTool'
|
} from 'lib/rectangleTool'
|
||||||
import { getThemeColorForThreeJs } from 'lib/theme'
|
import { getThemeColorForThreeJs } from 'lib/theme'
|
||||||
import { err, trap } from 'lib/trap'
|
import { err, reportRejection, trap } from 'lib/trap'
|
||||||
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
|
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
|
||||||
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
|
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
|
||||||
|
|
||||||
@ -351,6 +351,7 @@ export class SceneEntities {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
sceneInfra.setCallbacks({
|
sceneInfra.setCallbacks({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
onClick: async (args) => {
|
onClick: async (args) => {
|
||||||
if (!args) return
|
if (!args) return
|
||||||
if (args.mouseEvent.which !== 1) return
|
if (args.mouseEvent.which !== 1) return
|
||||||
@ -692,6 +693,7 @@ export class SceneEntities {
|
|||||||
draftExpressionsIndices,
|
draftExpressionsIndices,
|
||||||
})
|
})
|
||||||
sceneInfra.setCallbacks({
|
sceneInfra.setCallbacks({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
onClick: async (args) => {
|
onClick: async (args) => {
|
||||||
if (!args) return
|
if (!args) return
|
||||||
if (args.mouseEvent.which !== 1) return
|
if (args.mouseEvent.which !== 1) return
|
||||||
@ -762,7 +764,7 @@ export class SceneEntities {
|
|||||||
if (profileStart) {
|
if (profileStart) {
|
||||||
sceneInfra.modelingSend({ type: 'CancelSketch' })
|
sceneInfra.modelingSend({ type: 'CancelSketch' })
|
||||||
} else {
|
} else {
|
||||||
this.setUpDraftSegment(
|
await this.setUpDraftSegment(
|
||||||
sketchPathToNode,
|
sketchPathToNode,
|
||||||
forward,
|
forward,
|
||||||
up,
|
up,
|
||||||
@ -837,6 +839,7 @@ export class SceneEntities {
|
|||||||
})
|
})
|
||||||
|
|
||||||
sceneInfra.setCallbacks({
|
sceneInfra.setCallbacks({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
onMove: async (args) => {
|
onMove: async (args) => {
|
||||||
// Update the width and height of the draft rectangle
|
// Update the width and height of the draft rectangle
|
||||||
const pathToNodeTwo = structuredClone(sketchPathToNode)
|
const pathToNodeTwo = structuredClone(sketchPathToNode)
|
||||||
@ -884,6 +887,7 @@ export class SceneEntities {
|
|||||||
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketchGroup)
|
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketchGroup)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
onClick: async (args) => {
|
onClick: async (args) => {
|
||||||
// Commit the rectangle to the full AST/code and return to sketch.idle
|
// Commit the rectangle to the full AST/code and return to sketch.idle
|
||||||
const cornerPoint = args.intersectionPoint?.twoD
|
const cornerPoint = args.intersectionPoint?.twoD
|
||||||
@ -1103,9 +1107,11 @@ export class SceneEntities {
|
|||||||
}) => {
|
}) => {
|
||||||
let addingNewSegmentStatus: 'nothing' | 'pending' | 'added' = 'nothing'
|
let addingNewSegmentStatus: 'nothing' | 'pending' | 'added' = 'nothing'
|
||||||
sceneInfra.setCallbacks({
|
sceneInfra.setCallbacks({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
onDragEnd: async () => {
|
onDragEnd: async () => {
|
||||||
if (addingNewSegmentStatus !== 'nothing') {
|
if (addingNewSegmentStatus !== 'nothing') {
|
||||||
await this.tearDownSketch({ removeAxis: false })
|
await this.tearDownSketch({ removeAxis: false })
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.setupSketch({
|
this.setupSketch({
|
||||||
sketchPathToNode: pathToNode,
|
sketchPathToNode: pathToNode,
|
||||||
maybeModdedAst: kclManager.ast,
|
maybeModdedAst: kclManager.ast,
|
||||||
@ -1122,6 +1128,7 @@ export class SceneEntities {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
onDrag: async ({
|
onDrag: async ({
|
||||||
selected,
|
selected,
|
||||||
intersectionPoint,
|
intersectionPoint,
|
||||||
@ -1172,6 +1179,7 @@ export class SceneEntities {
|
|||||||
|
|
||||||
await kclManager.executeAstMock(mod.modifiedAst)
|
await kclManager.executeAstMock(mod.modifiedAst)
|
||||||
await this.tearDownSketch({ removeAxis: false })
|
await this.tearDownSketch({ removeAxis: false })
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.setupSketch({
|
this.setupSketch({
|
||||||
sketchPathToNode: pathToNode,
|
sketchPathToNode: pathToNode,
|
||||||
maybeModdedAst: kclManager.ast,
|
maybeModdedAst: kclManager.ast,
|
||||||
@ -1420,7 +1428,7 @@ export class SceneEntities {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
sceneInfra.overlayCallbacks(callBacks)
|
sceneInfra.overlayCallbacks(callBacks)
|
||||||
})()
|
})().catch(reportRejection)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -111,21 +111,21 @@ export class SceneInfra {
|
|||||||
}
|
}
|
||||||
extraSegmentTexture: Texture
|
extraSegmentTexture: Texture
|
||||||
lastMouseState: MouseState = { type: 'idle' }
|
lastMouseState: MouseState = { type: 'idle' }
|
||||||
onDragStartCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
onDragStartCallback: (arg: OnDragCallbackArgs) => any = () => {}
|
||||||
onDragEndCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
onDragEndCallback: (arg: OnDragCallbackArgs) => any = () => {}
|
||||||
onDragCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
onDragCallback: (arg: OnDragCallbackArgs) => any = () => {}
|
||||||
onMoveCallback: (arg: OnMoveCallbackArgs) => void = () => {}
|
onMoveCallback: (arg: OnMoveCallbackArgs) => any = () => {}
|
||||||
onClickCallback: (arg: OnClickCallbackArgs) => void = () => {}
|
onClickCallback: (arg: OnClickCallbackArgs) => any = () => {}
|
||||||
onMouseEnter: (arg: OnMouseEnterLeaveArgs) => void = () => {}
|
onMouseEnter: (arg: OnMouseEnterLeaveArgs) => any = () => {}
|
||||||
onMouseLeave: (arg: OnMouseEnterLeaveArgs) => void = () => {}
|
onMouseLeave: (arg: OnMouseEnterLeaveArgs) => any = () => {}
|
||||||
setCallbacks = (callbacks: {
|
setCallbacks = (callbacks: {
|
||||||
onDragStart?: (arg: OnDragCallbackArgs) => void
|
onDragStart?: (arg: OnDragCallbackArgs) => any
|
||||||
onDragEnd?: (arg: OnDragCallbackArgs) => void
|
onDragEnd?: (arg: OnDragCallbackArgs) => any
|
||||||
onDrag?: (arg: OnDragCallbackArgs) => void
|
onDrag?: (arg: OnDragCallbackArgs) => any
|
||||||
onMove?: (arg: OnMoveCallbackArgs) => void
|
onMove?: (arg: OnMoveCallbackArgs) => any
|
||||||
onClick?: (arg: OnClickCallbackArgs) => void
|
onClick?: (arg: OnClickCallbackArgs) => any
|
||||||
onMouseEnter?: (arg: OnMouseEnterLeaveArgs) => void
|
onMouseEnter?: (arg: OnMouseEnterLeaveArgs) => any
|
||||||
onMouseLeave?: (arg: OnMouseEnterLeaveArgs) => void
|
onMouseLeave?: (arg: OnMouseEnterLeaveArgs) => any
|
||||||
}) => {
|
}) => {
|
||||||
this.onDragStartCallback = callbacks.onDragStart || this.onDragStartCallback
|
this.onDragStartCallback = callbacks.onDragStart || this.onDragStartCallback
|
||||||
this.onDragEndCallback = callbacks.onDragEnd || this.onDragEndCallback
|
this.onDragEndCallback = callbacks.onDragEnd || this.onDragEndCallback
|
||||||
|
@ -151,6 +151,7 @@ export function useCalc({
|
|||||||
})
|
})
|
||||||
if (trap(error)) return
|
if (trap(error)) return
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
executeAst({
|
executeAst({
|
||||||
ast,
|
ast,
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
|
@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
|
|||||||
import { EngineCommandManagerEvents } from 'lang/std/engineConnection'
|
import { EngineCommandManagerEvents } from 'lang/std/engineConnection'
|
||||||
import { engineCommandManager, sceneInfra } from 'lib/singletons'
|
import { engineCommandManager, sceneInfra } from 'lib/singletons'
|
||||||
import { throttle, isReducedMotion } from 'lib/utils'
|
import { throttle, isReducedMotion } from 'lib/utils'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
const updateDollyZoom = throttle(
|
const updateDollyZoom = throttle(
|
||||||
(newFov: number) => sceneInfra.camControls.dollyZoom(newFov),
|
(newFov: number) => sceneInfra.camControls.dollyZoom(newFov),
|
||||||
@ -16,8 +17,8 @@ export const CamToggle = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
engineCommandManager.addEventListener(
|
engineCommandManager.addEventListener(
|
||||||
EngineCommandManagerEvents.SceneReady,
|
EngineCommandManagerEvents.SceneReady,
|
||||||
async () => {
|
() => {
|
||||||
sceneInfra.camControls.dollyZoom(fov)
|
sceneInfra.camControls.dollyZoom(fov).catch(reportRejection)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}, [])
|
}, [])
|
||||||
@ -26,11 +27,11 @@ export const CamToggle = () => {
|
|||||||
if (isPerspective) {
|
if (isPerspective) {
|
||||||
isReducedMotion()
|
isReducedMotion()
|
||||||
? sceneInfra.camControls.useOrthographicCamera()
|
? sceneInfra.camControls.useOrthographicCamera()
|
||||||
: sceneInfra.camControls.animateToOrthographic()
|
: sceneInfra.camControls.animateToOrthographic().catch(reportRejection)
|
||||||
} else {
|
} else {
|
||||||
isReducedMotion()
|
isReducedMotion()
|
||||||
? sceneInfra.camControls.usePerspectiveCamera()
|
? sceneInfra.camControls.usePerspectiveCamera().catch(reportRejection)
|
||||||
: sceneInfra.camControls.animateToPerspective()
|
: sceneInfra.camControls.animateToPerspective().catch(reportRejection)
|
||||||
}
|
}
|
||||||
setIsPerspective(!isPerspective)
|
setIsPerspective(!isPerspective)
|
||||||
}
|
}
|
||||||
|
@ -1,53 +1,43 @@
|
|||||||
import { useMachine } from '@xstate/react'
|
import { createActorContext } from '@xstate/react'
|
||||||
import { editorManager } from 'lib/singletons'
|
import { editorManager } from 'lib/singletons'
|
||||||
import { commandBarMachine } from 'machines/commandBarMachine'
|
import { commandBarMachine } from 'machines/commandBarMachine'
|
||||||
import { createContext, useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { EventFrom, StateFrom } from 'xstate'
|
|
||||||
|
|
||||||
type CommandsContextType = {
|
export const CommandsContext = createActorContext(
|
||||||
commandBarState: StateFrom<typeof commandBarMachine>
|
commandBarMachine.provide({
|
||||||
commandBarSend: (event: EventFrom<typeof commandBarMachine>) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CommandsContext = createContext<CommandsContextType>({
|
|
||||||
commandBarState: commandBarMachine.initialState,
|
|
||||||
commandBarSend: () => {},
|
|
||||||
})
|
|
||||||
|
|
||||||
export const CommandBarProvider = ({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) => {
|
|
||||||
const [commandBarState, commandBarSend] = useMachine(commandBarMachine, {
|
|
||||||
devTools: true,
|
|
||||||
guards: {
|
guards: {
|
||||||
'Command has no arguments': (context, _event) => {
|
'Command has no arguments': ({ context }) => {
|
||||||
return (
|
return (
|
||||||
!context.selectedCommand?.args ||
|
!context.selectedCommand?.args ||
|
||||||
Object.keys(context.selectedCommand?.args).length === 0
|
Object.keys(context.selectedCommand?.args).length === 0
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
'All arguments are skippable': (context, _event) => {
|
'All arguments are skippable': ({ context }) => {
|
||||||
return Object.values(context.selectedCommand!.args!).every(
|
return Object.values(context.selectedCommand!.args!).every(
|
||||||
(argConfig) => argConfig.skip
|
(argConfig) => argConfig.skip
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
export const CommandBarProvider = ({
|
||||||
editorManager.setCommandBarSend(commandBarSend)
|
children,
|
||||||
})
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<CommandsContext.Provider
|
<CommandsContext.Provider>
|
||||||
value={{
|
<CommandBarProviderInner>{children}</CommandBarProviderInner>
|
||||||
commandBarState,
|
|
||||||
commandBarSend,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</CommandsContext.Provider>
|
</CommandsContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
function CommandBarProviderInner({ children }: { children: React.ReactNode }) {
|
||||||
|
const commandBarActor = CommandsContext.useActorRef()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
editorManager.setCommandBarSend(commandBarActor.send)
|
||||||
|
})
|
||||||
|
|
||||||
|
return children
|
||||||
|
}
|
||||||
|
@ -52,7 +52,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
commandBarSend({
|
commandBarSend({
|
||||||
type: 'Submit command',
|
type: 'Submit command',
|
||||||
data: argumentsToSubmit,
|
output: argumentsToSubmit,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
getSelectionTypeDisplayText,
|
getSelectionTypeDisplayText,
|
||||||
} from 'lib/selections'
|
} from 'lib/selections'
|
||||||
import { modelingMachine } from 'machines/modelingMachine'
|
import { modelingMachine } from 'machines/modelingMachine'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { StateFrom } from 'xstate'
|
import { StateFrom } from 'xstate'
|
||||||
|
|
||||||
const semanticEntityNames: { [key: string]: Array<Selection['type']> } = {
|
const semanticEntityNames: { [key: string]: Array<Selection['type']> } = {
|
||||||
@ -48,15 +48,15 @@ function CommandBarSelectionInput({
|
|||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
const [hasSubmitted, setHasSubmitted] = useState(false)
|
const [hasSubmitted, setHasSubmitted] = useState(false)
|
||||||
const selection = useSelector(arg.machineActor, selectionSelector)
|
const selection = useSelector(arg.machineActor, selectionSelector)
|
||||||
const initSelectionsByType = useCallback(() => {
|
const selectionsByType = useMemo(() => {
|
||||||
const selectionRangeEnd = selection.codeBasedSelections[0]?.range[1]
|
const selectionRangeEnd = selection.codeBasedSelections[0]?.range[1]
|
||||||
return !selectionRangeEnd || selectionRangeEnd === code.length
|
return !selectionRangeEnd || selectionRangeEnd === code.length
|
||||||
? 'none'
|
? 'none'
|
||||||
: getSelectionType(selection)
|
: getSelectionType(selection)
|
||||||
}, [selection, code])
|
}, [selection, code])
|
||||||
const selectionsByType = initSelectionsByType()
|
const canSubmitSelection = useMemo<boolean>(
|
||||||
const [canSubmitSelection, setCanSubmitSelection] = useState<boolean>(
|
() => canSubmitSelectionArg(selectionsByType, arg),
|
||||||
canSubmitSelectionArg(selectionsByType, arg)
|
[selectionsByType]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -66,26 +66,18 @@ function CommandBarSelectionInput({
|
|||||||
// Fast-forward through this arg if it's marked as skippable
|
// Fast-forward through this arg if it's marked as skippable
|
||||||
// and we have a valid selection already
|
// and we have a valid selection already
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('selection input effect', {
|
|
||||||
selectionsByType,
|
|
||||||
canSubmitSelection,
|
|
||||||
arg,
|
|
||||||
})
|
|
||||||
setCanSubmitSelection(canSubmitSelectionArg(selectionsByType, arg))
|
|
||||||
const argValue = commandBarState.context.argumentsToSubmit[arg.name]
|
const argValue = commandBarState.context.argumentsToSubmit[arg.name]
|
||||||
if (canSubmitSelection && arg.skip && argValue === undefined) {
|
if (canSubmitSelection && arg.skip && argValue === undefined) {
|
||||||
handleSubmit({
|
handleSubmit()
|
||||||
preventDefault: () => {},
|
|
||||||
} as React.FormEvent<HTMLFormElement>)
|
|
||||||
}
|
}
|
||||||
}, [selectionsByType, arg])
|
}, [canSubmitSelection])
|
||||||
|
|
||||||
function handleChange() {
|
function handleChange() {
|
||||||
inputRef.current?.focus()
|
inputRef.current?.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
|
||||||
e.preventDefault()
|
e?.preventDefault()
|
||||||
|
|
||||||
if (!canSubmitSelection) {
|
if (!canSubmitSelection) {
|
||||||
setHasSubmitted(true)
|
setHasSubmitted(true)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { CommandLog } from 'lang/std/engineConnection'
|
import { CommandLog } from 'lang/std/engineConnection'
|
||||||
import { engineCommandManager } from 'lib/singletons'
|
import { engineCommandManager } from 'lib/singletons'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
export function useEngineCommands(): [CommandLog[], () => void] {
|
export function useEngineCommands(): [CommandLog[], () => void] {
|
||||||
@ -77,9 +78,11 @@ export const EngineCommands = () => {
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
data-testid="custom-cmd-send-button"
|
data-testid="custom-cmd-send-button"
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
engineCommandManager.sendSceneCommand(JSON.parse(customCmd))
|
engineCommandManager
|
||||||
}
|
.sendSceneCommand(JSON.parse(customCmd))
|
||||||
|
.catch(reportRejection)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Send custom command
|
Send custom command
|
||||||
</button>
|
</button>
|
||||||
|
@ -5,13 +5,12 @@ import { PATHS } from 'lib/paths'
|
|||||||
import React, { createContext } from 'react'
|
import React, { createContext } from 'react'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import {
|
import {
|
||||||
|
Actor,
|
||||||
AnyStateMachine,
|
AnyStateMachine,
|
||||||
ContextFrom,
|
ContextFrom,
|
||||||
EventFrom,
|
|
||||||
InterpreterFrom,
|
|
||||||
Prop,
|
Prop,
|
||||||
StateFrom,
|
StateFrom,
|
||||||
assign,
|
fromPromise,
|
||||||
} from 'xstate'
|
} from 'xstate'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { fileMachine } from 'machines/fileMachine'
|
import { fileMachine } from 'machines/fileMachine'
|
||||||
@ -27,7 +26,7 @@ import { getNextDirName, getNextFileName } from 'lib/desktopFS'
|
|||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
context: ContextFrom<T>
|
context: ContextFrom<T>
|
||||||
send: Prop<InterpreterFrom<T>, 'send'>
|
send: Prop<Actor<T>, 'send'>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileContext = createContext(
|
export const FileContext = createContext(
|
||||||
@ -43,239 +42,234 @@ export const FileMachineProvider = ({
|
|||||||
const { commandBarSend } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||||
|
|
||||||
const [state, send] = useMachine(fileMachine, {
|
const [state, send] = useMachine(
|
||||||
context: {
|
fileMachine.provide({
|
||||||
project,
|
actions: {
|
||||||
selectedDirectory: project,
|
renameToastSuccess: ({ event }) => {
|
||||||
},
|
if (event.type !== 'xstate.done.actor.rename-file') return
|
||||||
actions: {
|
toast.success(event.output.message)
|
||||||
navigateToFile: (context, event) => {
|
},
|
||||||
if (event.data && 'name' in event.data) {
|
createToastSuccess: ({ event }) => {
|
||||||
commandBarSend({ type: 'Close' })
|
if (event.type !== 'xstate.done.actor.create-and-open-file') return
|
||||||
navigate(
|
toast.success(event.output.message)
|
||||||
`..${PATHS.FILE}/${encodeURIComponent(
|
},
|
||||||
context.selectedDirectory +
|
toastSuccess: ({ event }) => {
|
||||||
window.electron.path.sep +
|
if (
|
||||||
event.data.name
|
event.type !== 'xstate.done.actor.rename-file' &&
|
||||||
)}`
|
event.type !== 'xstate.done.actor.delete-file'
|
||||||
)
|
)
|
||||||
} else if (
|
return
|
||||||
event.data &&
|
toast.success(event.output.message)
|
||||||
'path' in event.data &&
|
},
|
||||||
event.data.path.endsWith(FILE_EXT)
|
toastError: ({ event }) => {
|
||||||
) {
|
if (event.type !== 'xstate.done.actor.rename-file') return
|
||||||
// Don't navigate to newly created directories
|
toast.error(event.output.message)
|
||||||
navigate(`..${PATHS.FILE}/${encodeURIComponent(event.data.path)}`)
|
},
|
||||||
}
|
navigateToFile: ({ context, event }) => {
|
||||||
|
if (event.type !== 'xstate.done.actor.create-and-open-file') return
|
||||||
|
if (event.output && 'name' in event.output) {
|
||||||
|
commandBarSend({ type: 'Close' })
|
||||||
|
navigate(
|
||||||
|
`..${PATHS.FILE}/${encodeURIComponent(
|
||||||
|
context.selectedDirectory +
|
||||||
|
window.electron.path.sep +
|
||||||
|
event.output.name
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
} else if (
|
||||||
|
event.output &&
|
||||||
|
'path' in event.output &&
|
||||||
|
event.output.path.endsWith(FILE_EXT)
|
||||||
|
) {
|
||||||
|
// Don't navigate to newly created directories
|
||||||
|
navigate(`..${PATHS.FILE}/${encodeURIComponent(event.output.path)}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
addFileToRenamingQueue: assign({
|
actors: {
|
||||||
itemsBeingRenamed: (context, event) => [
|
readFiles: fromPromise(async ({ input }) => {
|
||||||
...context.itemsBeingRenamed,
|
const newFiles =
|
||||||
event.data.path,
|
(isDesktop() ? (await getProjectInfo(input.path)).children : []) ??
|
||||||
],
|
[]
|
||||||
}),
|
|
||||||
removeFileFromRenamingQueue: assign({
|
|
||||||
itemsBeingRenamed: (
|
|
||||||
context,
|
|
||||||
event: EventFrom<typeof fileMachine, 'done.invoke.rename-file'>
|
|
||||||
) =>
|
|
||||||
context.itemsBeingRenamed.filter(
|
|
||||||
(path) => path !== event.data.oldPath
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
renameToastSuccess: (_, event) => toast.success(event.data.message),
|
|
||||||
createToastSuccess: (_, event) => toast.success(event.data.message),
|
|
||||||
toastSuccess: (_, event) =>
|
|
||||||
event.data && toast.success((event.data || '') + ''),
|
|
||||||
toastError: (_, event) => toast.error((event.data || '') + ''),
|
|
||||||
},
|
|
||||||
services: {
|
|
||||||
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
|
|
||||||
const newFiles = isDesktop()
|
|
||||||
? (await getProjectInfo(context.project.path)).children
|
|
||||||
: []
|
|
||||||
return {
|
|
||||||
...context.project,
|
|
||||||
children: newFiles,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
createAndOpenFile: async (context, event) => {
|
|
||||||
let createdName = event.data.name.trim() || DEFAULT_FILE_NAME
|
|
||||||
let createdPath: string
|
|
||||||
|
|
||||||
if (event.data.makeDir) {
|
|
||||||
let { name, path } = getNextDirName({
|
|
||||||
entryName: createdName,
|
|
||||||
baseDir: context.selectedDirectory.path,
|
|
||||||
})
|
|
||||||
createdName = name
|
|
||||||
createdPath = path
|
|
||||||
await window.electron.mkdir(createdPath)
|
|
||||||
} else {
|
|
||||||
const { name, path } = getNextFileName({
|
|
||||||
entryName: createdName,
|
|
||||||
baseDir: context.selectedDirectory.path,
|
|
||||||
})
|
|
||||||
createdName = name
|
|
||||||
createdPath = path
|
|
||||||
await window.electron.writeFile(createdPath, event.data.content ?? '')
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: `Successfully created "${createdName}"`,
|
|
||||||
path: createdPath,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
createFile: async (context, event) => {
|
|
||||||
let createdName = event.data.name.trim() || DEFAULT_FILE_NAME
|
|
||||||
let createdPath: string
|
|
||||||
|
|
||||||
if (event.data.makeDir) {
|
|
||||||
let { name, path } = getNextDirName({
|
|
||||||
entryName: createdName,
|
|
||||||
baseDir: context.selectedDirectory.path,
|
|
||||||
})
|
|
||||||
createdName = name
|
|
||||||
createdPath = path
|
|
||||||
await window.electron.mkdir(createdPath)
|
|
||||||
} else {
|
|
||||||
const { name, path } = getNextFileName({
|
|
||||||
entryName: createdName,
|
|
||||||
baseDir: context.selectedDirectory.path,
|
|
||||||
})
|
|
||||||
createdName = name
|
|
||||||
createdPath = path
|
|
||||||
await window.electron.writeFile(createdPath, event.data.content ?? '')
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
path: createdPath,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
renameFile: async (
|
|
||||||
context: ContextFrom<typeof fileMachine>,
|
|
||||||
event: EventFrom<typeof fileMachine, 'Rename file'>
|
|
||||||
) => {
|
|
||||||
const { oldName, newName, isDir } = event.data
|
|
||||||
const name = newName
|
|
||||||
? newName.endsWith(FILE_EXT) || isDir
|
|
||||||
? newName
|
|
||||||
: newName + FILE_EXT
|
|
||||||
: DEFAULT_FILE_NAME
|
|
||||||
const oldPath = window.electron.path.join(
|
|
||||||
context.selectedDirectory.path,
|
|
||||||
oldName
|
|
||||||
)
|
|
||||||
const newPath = window.electron.path.join(
|
|
||||||
context.selectedDirectory.path,
|
|
||||||
name
|
|
||||||
)
|
|
||||||
|
|
||||||
// no-op
|
|
||||||
if (oldPath === newPath) {
|
|
||||||
return {
|
return {
|
||||||
message: `Old is the same as new.`,
|
...input,
|
||||||
|
children: newFiles,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
createAndOpenFile: fromPromise(async ({ input }) => {
|
||||||
|
let createdName = input.name.trim() || DEFAULT_FILE_NAME
|
||||||
|
let createdPath: string
|
||||||
|
|
||||||
|
if (input.makeDir) {
|
||||||
|
let { name, path } = getNextDirName({
|
||||||
|
entryName: createdName,
|
||||||
|
baseDir: input.selectedDirectory.path,
|
||||||
|
})
|
||||||
|
createdName = name
|
||||||
|
createdPath = path
|
||||||
|
await window.electron.mkdir(createdPath)
|
||||||
|
} else {
|
||||||
|
const { name, path } = getNextFileName({
|
||||||
|
entryName: createdName,
|
||||||
|
baseDir: input.selectedDirectory.path,
|
||||||
|
})
|
||||||
|
createdName = name
|
||||||
|
createdPath = path
|
||||||
|
await window.electron.writeFile(createdPath, input.content ?? '')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: `Successfully created "${createdName}"`,
|
||||||
|
path: createdPath,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
createFile: fromPromise(async ({ input }) => {
|
||||||
|
let createdName = input.name.trim() || DEFAULT_FILE_NAME
|
||||||
|
let createdPath: string
|
||||||
|
|
||||||
|
if (input.makeDir) {
|
||||||
|
let { name, path } = getNextDirName({
|
||||||
|
entryName: createdName,
|
||||||
|
baseDir: input.selectedDirectory.path,
|
||||||
|
})
|
||||||
|
createdName = name
|
||||||
|
createdPath = path
|
||||||
|
await window.electron.mkdir(createdPath)
|
||||||
|
} else {
|
||||||
|
const { name, path } = getNextFileName({
|
||||||
|
entryName: createdName,
|
||||||
|
baseDir: input.selectedDirectory.path,
|
||||||
|
})
|
||||||
|
createdName = name
|
||||||
|
createdPath = path
|
||||||
|
await window.electron.writeFile(createdPath, input.content ?? '')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: createdPath,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
renameFile: fromPromise(async ({ input }) => {
|
||||||
|
const { oldName, newName, isDir } = input
|
||||||
|
const name = newName
|
||||||
|
? newName.endsWith(FILE_EXT) || isDir
|
||||||
|
? newName
|
||||||
|
: newName + FILE_EXT
|
||||||
|
: DEFAULT_FILE_NAME
|
||||||
|
const oldPath = window.electron.path.join(
|
||||||
|
input.selectedDirectory.path,
|
||||||
|
oldName
|
||||||
|
)
|
||||||
|
const newPath = window.electron.path.join(
|
||||||
|
input.selectedDirectory.path,
|
||||||
|
name
|
||||||
|
)
|
||||||
|
|
||||||
|
// no-op
|
||||||
|
if (oldPath === newPath) {
|
||||||
|
return {
|
||||||
|
message: `Old is the same as new.`,
|
||||||
|
newPath,
|
||||||
|
oldPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there are any siblings with the same name, report error.
|
||||||
|
const entries = await window.electron.readdir(
|
||||||
|
window.electron.path.dirname(newPath)
|
||||||
|
)
|
||||||
|
for (let entry of entries) {
|
||||||
|
if (entry === newName) {
|
||||||
|
return Promise.reject(new Error('Filename already exists.'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.electron.rename(oldPath, newPath)
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return Promise.reject(new Error('file is not defined'))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldPath === file.path && project?.path) {
|
||||||
|
// If we just renamed the current file, navigate to the new path
|
||||||
|
navigate(`..${PATHS.FILE}/${encodeURIComponent(newPath)}`)
|
||||||
|
} else if (file?.path.includes(oldPath)) {
|
||||||
|
// If we just renamed a directory that the current file is in, navigate to the new path
|
||||||
|
navigate(
|
||||||
|
`..${PATHS.FILE}/${encodeURIComponent(
|
||||||
|
file.path.replace(oldPath, newPath)
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: `Successfully renamed "${oldName}" to "${name}"`,
|
||||||
newPath,
|
newPath,
|
||||||
oldPath,
|
oldPath,
|
||||||
}
|
}
|
||||||
}
|
}),
|
||||||
|
deleteFile: fromPromise(async ({ input }) => {
|
||||||
|
const isDir = !!input.children
|
||||||
|
|
||||||
// if there are any siblings with the same name, report error.
|
if (isDir) {
|
||||||
const entries = await window.electron.readdir(
|
await window.electron
|
||||||
window.electron.path.dirname(newPath)
|
.rm(input.path, {
|
||||||
)
|
recursive: true,
|
||||||
for (let entry of entries) {
|
})
|
||||||
if (entry === newName) {
|
.catch((e) => console.error('Error deleting directory', e))
|
||||||
return Promise.reject(new Error('Filename already exists.'))
|
} else {
|
||||||
|
await window.electron
|
||||||
|
.rm(input.path)
|
||||||
|
.catch((e) => console.error('Error deleting file', e))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
window.electron.rename(oldPath, newPath)
|
// If there are no more files at all in the project, create a main.kcl
|
||||||
|
// for when we navigate to the root.
|
||||||
|
if (!project?.path) {
|
||||||
|
return Promise.reject(new Error('Project path not set.'))
|
||||||
|
}
|
||||||
|
|
||||||
if (!file) {
|
const entries = await window.electron.readdir(project.path)
|
||||||
return Promise.reject(new Error('file is not defined'))
|
const hasKclEntries =
|
||||||
}
|
entries.filter((e: string) => e.endsWith('.kcl')).length !== 0
|
||||||
|
if (!hasKclEntries) {
|
||||||
|
await window.electron.writeFile(
|
||||||
|
window.electron.path.join(project.path, DEFAULT_PROJECT_KCL_FILE),
|
||||||
|
''
|
||||||
|
)
|
||||||
|
// Refresh the route selected above because it's possible we're on
|
||||||
|
// the same path on the navigate, which doesn't cause anything to
|
||||||
|
// refresh, leaving a stale execution state.
|
||||||
|
navigate(0)
|
||||||
|
return {
|
||||||
|
message: 'No more files in project, created main.kcl',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (oldPath === file.path && project?.path) {
|
// If we just deleted the current file or one of its parent directories,
|
||||||
// If we just renamed the current file, navigate to the new path
|
// navigate to the project root
|
||||||
navigate(`..${PATHS.FILE}/${encodeURIComponent(newPath)}`)
|
if (
|
||||||
} else if (file?.path.includes(oldPath)) {
|
(input.path === file?.path || file?.path.includes(input.path)) &&
|
||||||
// If we just renamed a directory that the current file is in, navigate to the new path
|
project?.path
|
||||||
navigate(
|
) {
|
||||||
`..${PATHS.FILE}/${encodeURIComponent(
|
navigate(`../${PATHS.FILE}/${encodeURIComponent(project.path)}`)
|
||||||
file.path.replace(oldPath, newPath)
|
}
|
||||||
)}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: `Successfully renamed "${oldName}" to "${name}"`,
|
message: `Successfully deleted ${isDir ? 'folder' : 'file'} "${
|
||||||
newPath,
|
input.name
|
||||||
oldPath,
|
}"`,
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
deleteFile: async (
|
}),
|
||||||
context: ContextFrom<typeof fileMachine>,
|
{
|
||||||
event: EventFrom<typeof fileMachine, 'Delete file'>
|
input: {
|
||||||
) => {
|
project,
|
||||||
const isDir = !!event.data.children
|
selectedDirectory: project,
|
||||||
|
|
||||||
if (isDir) {
|
|
||||||
await window.electron
|
|
||||||
.rm(event.data.path, {
|
|
||||||
recursive: true,
|
|
||||||
})
|
|
||||||
.catch((e) => console.error('Error deleting directory', e))
|
|
||||||
} else {
|
|
||||||
await window.electron
|
|
||||||
.rm(event.data.path)
|
|
||||||
.catch((e) => console.error('Error deleting file', e))
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are no more files at all in the project, create a main.kcl
|
|
||||||
// for when we navigate to the root.
|
|
||||||
if (!project?.path) {
|
|
||||||
return Promise.reject(new Error('Project path not set.'))
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = await window.electron.readdir(project.path)
|
|
||||||
const hasKclEntries =
|
|
||||||
entries.filter((e: string) => e.endsWith('.kcl')).length !== 0
|
|
||||||
if (!hasKclEntries) {
|
|
||||||
await window.electron.writeFile(
|
|
||||||
window.electron.path.join(project.path, DEFAULT_PROJECT_KCL_FILE),
|
|
||||||
''
|
|
||||||
)
|
|
||||||
// Refresh the route selected above because it's possible we're on
|
|
||||||
// the same path on the navigate, which doesn't cause anything to
|
|
||||||
// refresh, leaving a stale execution state.
|
|
||||||
navigate(0)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we just deleted the current file or one of its parent directories,
|
|
||||||
// navigate to the project root
|
|
||||||
if (
|
|
||||||
(event.data.path === file?.path ||
|
|
||||||
file?.path.includes(event.data.path)) &&
|
|
||||||
project?.path
|
|
||||||
) {
|
|
||||||
navigate(`../${PATHS.FILE}/${encodeURIComponent(project.path)}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return `Successfully deleted ${isDir ? 'folder' : 'file'} "${
|
|
||||||
event.data.name
|
|
||||||
}"`
|
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
guards: {
|
)
|
||||||
'Has at least 1 file': (_, event: EventFrom<typeof fileMachine>) => {
|
|
||||||
if (event.type !== 'done.invoke.read-files') return false
|
|
||||||
return !!event?.data?.children && event.data.children.length > 0
|
|
||||||
},
|
|
||||||
'Is not silent': (_, event) => !event.data?.silent,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FileContext.Provider
|
<FileContext.Provider
|
||||||
|
@ -176,9 +176,11 @@ const FileTreeItem = ({
|
|||||||
`import("${fileOrDir.path.replace(project.path, '.')}")\n` +
|
`import("${fileOrDir.path.replace(project.path, '.')}")\n` +
|
||||||
codeManager.code
|
codeManager.code
|
||||||
)
|
)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
codeManager.writeToFile()
|
codeManager.writeToFile()
|
||||||
|
|
||||||
// Prevent seeing the model built one piece at a time when changing files
|
// Prevent seeing the model built one piece at a time when changing files
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
kclManager.executeCode(true)
|
kclManager.executeCode(true)
|
||||||
} else {
|
} else {
|
||||||
// Let the lsp servers know we closed a file.
|
// Let the lsp servers know we closed a file.
|
||||||
@ -243,13 +245,13 @@ const FileTreeItem = ({
|
|||||||
onClickCapture={(e) =>
|
onClickCapture={(e) =>
|
||||||
fileSend({
|
fileSend({
|
||||||
type: 'Set selected directory',
|
type: 'Set selected directory',
|
||||||
data: fileOrDir,
|
directory: fileOrDir,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
onFocusCapture={(e) =>
|
onFocusCapture={(e) =>
|
||||||
fileSend({
|
fileSend({
|
||||||
type: 'Set selected directory',
|
type: 'Set selected directory',
|
||||||
data: fileOrDir,
|
directory: fileOrDir,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
|
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
|
||||||
@ -296,13 +298,13 @@ const FileTreeItem = ({
|
|||||||
onClickCapture={(e) => {
|
onClickCapture={(e) => {
|
||||||
fileSend({
|
fileSend({
|
||||||
type: 'Set selected directory',
|
type: 'Set selected directory',
|
||||||
data: fileOrDir,
|
directory: fileOrDir,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
onFocusCapture={(e) =>
|
onFocusCapture={(e) =>
|
||||||
fileSend({
|
fileSend({
|
||||||
type: 'Set selected directory',
|
type: 'Set selected directory',
|
||||||
data: fileOrDir,
|
directory: fileOrDir,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -388,14 +390,14 @@ interface FileTreeProps {
|
|||||||
export const FileTreeMenu = () => {
|
export const FileTreeMenu = () => {
|
||||||
const { send } = useFileContext()
|
const { send } = useFileContext()
|
||||||
|
|
||||||
async function createFile() {
|
function createFile() {
|
||||||
send({
|
send({
|
||||||
type: 'Create file',
|
type: 'Create file',
|
||||||
data: { name: '', makeDir: false },
|
data: { name: '', makeDir: false },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createFolder() {
|
function createFolder() {
|
||||||
send({
|
send({
|
||||||
type: 'Create file',
|
type: 'Create file',
|
||||||
data: { name: '', makeDir: true },
|
data: { name: '', makeDir: true },
|
||||||
@ -482,7 +484,7 @@ export const FileTreeInner = ({
|
|||||||
onClickCapture={(e) => {
|
onClickCapture={(e) => {
|
||||||
fileSend({
|
fileSend({
|
||||||
type: 'Set selected directory',
|
type: 'Set selected directory',
|
||||||
data: fileContext.project,
|
directory: fileContext.project,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -27,6 +27,7 @@ import {
|
|||||||
} from './ContextMenu'
|
} from './ContextMenu'
|
||||||
import { Popover } from '@headlessui/react'
|
import { Popover } from '@headlessui/react'
|
||||||
import { CustomIcon } from './CustomIcon'
|
import { CustomIcon } from './CustomIcon'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
const CANVAS_SIZE = 80
|
const CANVAS_SIZE = 80
|
||||||
const FRUSTUM_SIZE = 0.5
|
const FRUSTUM_SIZE = 0.5
|
||||||
@ -67,7 +68,9 @@ export default function Gizmo() {
|
|||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
key={axisName}
|
key={axisName}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
sceneInfra.camControls.updateCameraToAxis(axisName as AxisNames)
|
sceneInfra.camControls
|
||||||
|
.updateCameraToAxis(axisName as AxisNames)
|
||||||
|
.catch(reportRejection)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{axisSemantic} view
|
{axisSemantic} view
|
||||||
@ -75,7 +78,7 @@ export default function Gizmo() {
|
|||||||
)),
|
)),
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
sceneInfra.camControls.resetCameraPosition()
|
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reset view
|
Reset view
|
||||||
@ -299,7 +302,7 @@ const initializeMouseEvents = (
|
|||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (raycasterIntersect.current) {
|
if (raycasterIntersect.current) {
|
||||||
const axisName = raycasterIntersect.current.object.name as AxisNames
|
const axisName = raycasterIntersect.current.object.name as AxisNames
|
||||||
sceneInfra.camControls.updateCameraToAxis(axisName)
|
sceneInfra.camControls.updateCameraToAxis(axisName).catch(reportRejection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import { createAndOpenNewProject } from 'lib/desktopFS'
|
|||||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||||
import { useLspContext } from './LspProvider'
|
import { useLspContext } from './LspProvider'
|
||||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
const HelpMenuDivider = () => (
|
const HelpMenuDivider = () => (
|
||||||
<div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" />
|
<div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" />
|
||||||
@ -115,7 +116,9 @@ export function HelpMenu(props: React.PropsWithChildren) {
|
|||||||
if (isInProject) {
|
if (isInProject) {
|
||||||
navigate(filePath + PATHS.ONBOARDING.INDEX)
|
navigate(filePath + PATHS.ONBOARDING.INDEX)
|
||||||
} else {
|
} else {
|
||||||
createAndOpenNewProject({ onProjectOpen, navigate })
|
createAndOpenNewProject({ onProjectOpen, navigate }).catch(
|
||||||
|
reportRejection
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -12,6 +12,7 @@ import { CoreDumpManager } from 'lib/coredump'
|
|||||||
import openWindow, { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
import openWindow, { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
import { NetworkMachineIndicator } from './NetworkMachineIndicator'
|
import { NetworkMachineIndicator } from './NetworkMachineIndicator'
|
||||||
import { ModelStateIndicator } from './ModelStateIndicator'
|
import { ModelStateIndicator } from './ModelStateIndicator'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
export function LowerRightControls({
|
export function LowerRightControls({
|
||||||
children,
|
children,
|
||||||
@ -25,7 +26,7 @@ export function LowerRightControls({
|
|||||||
const linkOverrideClassName =
|
const linkOverrideClassName =
|
||||||
'!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30'
|
'!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30'
|
||||||
|
|
||||||
async function reportbug(event: {
|
function reportbug(event: {
|
||||||
preventDefault: () => void
|
preventDefault: () => void
|
||||||
stopPropagation: () => void
|
stopPropagation: () => void
|
||||||
}) {
|
}) {
|
||||||
@ -34,7 +35,9 @@ export function LowerRightControls({
|
|||||||
|
|
||||||
if (!coreDumpManager) {
|
if (!coreDumpManager) {
|
||||||
// open default reporting option
|
// open default reporting option
|
||||||
openWindow('https://github.com/KittyCAD/modeling-app/issues/new/choose')
|
openWindow(
|
||||||
|
'https://github.com/KittyCAD/modeling-app/issues/new/choose'
|
||||||
|
).catch(reportRejection)
|
||||||
} else {
|
} else {
|
||||||
toast
|
toast
|
||||||
.promise(
|
.promise(
|
||||||
@ -56,7 +59,7 @@ export function LowerRightControls({
|
|||||||
if (err) {
|
if (err) {
|
||||||
openWindow(
|
openWindow(
|
||||||
'https://github.com/KittyCAD/modeling-app/issues/new/choose'
|
'https://github.com/KittyCAD/modeling-app/issues/new/choose'
|
||||||
)
|
).catch(reportRejection)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -160,7 +160,9 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
// Update the folding ranges, since the AST has changed.
|
// Update the folding ranges, since the AST has changed.
|
||||||
// This is a hack since codemirror does not support async foldService.
|
// This is a hack since codemirror does not support async foldService.
|
||||||
// When they do we can delete this.
|
// When they do we can delete this.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
plugin.updateFoldingRanges()
|
plugin.updateFoldingRanges()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
plugin.requestSemanticTokens()
|
plugin.requestSemanticTokens()
|
||||||
break
|
break
|
||||||
case 'kcl/memoryUpdated':
|
case 'kcl/memoryUpdated':
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -8,6 +8,7 @@ import { editorShortcutMeta } from './KclEditorPane'
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { kclManager } from 'lib/singletons'
|
import { kclManager } from 'lib/singletons'
|
||||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
export const KclEditorMenu = ({ children }: PropsWithChildren) => {
|
export const KclEditorMenu = ({ children }: PropsWithChildren) => {
|
||||||
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
|
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
|
||||||
@ -47,7 +48,9 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
|
|||||||
{convertToVarEnabled && (
|
{convertToVarEnabled && (
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleConvertToVarClick()}
|
onClick={() => {
|
||||||
|
handleConvertToVarClick().catch(reportRejection)
|
||||||
|
}}
|
||||||
className={styles.button}
|
className={styles.button}
|
||||||
>
|
>
|
||||||
<span>Convert to Variable</span>
|
<span>Convert to Variable</span>
|
||||||
|
@ -57,6 +57,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
icon: 'printer3d',
|
icon: 'printer3d',
|
||||||
iconClassName: '!p-0',
|
iconClassName: '!p-0',
|
||||||
keybinding: 'Ctrl + Shift + M',
|
keybinding: 'Ctrl + Shift + M',
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
action: async () => {
|
action: async () => {
|
||||||
commandBarSend({
|
commandBarSend({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
|
@ -4,6 +4,8 @@ import Tooltip from './Tooltip'
|
|||||||
import { ConnectingTypeGroup } from '../lang/std/engineConnection'
|
import { ConnectingTypeGroup } from '../lang/std/engineConnection'
|
||||||
import { useNetworkContext } from '../hooks/useNetworkContext'
|
import { useNetworkContext } from '../hooks/useNetworkContext'
|
||||||
import { NetworkHealthState } from '../hooks/useNetworkStatus'
|
import { NetworkHealthState } from '../hooks/useNetworkStatus'
|
||||||
|
import { toSync } from 'lib/utils'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = {
|
export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = {
|
||||||
[NetworkHealthState.Ok]: 'Connected',
|
[NetworkHealthState.Ok]: 'Connected',
|
||||||
@ -160,13 +162,13 @@ export const NetworkHealthIndicator = () => {
|
|||||||
</div>
|
</div>
|
||||||
{issues[name as ConnectingTypeGroup] && (
|
{issues[name as ConnectingTypeGroup] && (
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={toSync(async () => {
|
||||||
await navigator.clipboard.writeText(
|
await navigator.clipboard.writeText(
|
||||||
JSON.stringify(error, null, 2) || ''
|
JSON.stringify(error, null, 2) || ''
|
||||||
)
|
)
|
||||||
setHasCopied(true)
|
setHasCopied(true)
|
||||||
setTimeout(() => setHasCopied(false), 5000)
|
setTimeout(() => setHasCopied(false), 5000)
|
||||||
}}
|
}, reportRejection)}
|
||||||
className="flex w-fit gap-2 items-center bg-transparent text-sm p-1 py-0 my-0 -mx-1 text-destroy-80 dark:text-destroy-10 hover:bg-transparent border-transparent dark:border-transparent hover:border-destroy-80 dark:hover:border-destroy-80 dark:hover:bg-destroy-80"
|
className="flex w-fit gap-2 items-center bg-transparent text-sm p-1 py-0 my-0 -mx-1 text-destroy-80 dark:text-destroy-10 hover:bg-transparent border-transparent dark:border-transparent hover:border-destroy-80 dark:hover:border-destroy-80 dark:hover:bg-destroy-80"
|
||||||
>
|
>
|
||||||
{hasCopied ? 'Copied' : 'Copy Error'}
|
{hasCopied ? 'Copied' : 'Copy Error'}
|
||||||
|
@ -8,6 +8,8 @@ import Tooltip from '../Tooltip'
|
|||||||
import { DeleteConfirmationDialog } from './DeleteProjectDialog'
|
import { DeleteConfirmationDialog } from './DeleteProjectDialog'
|
||||||
import { ProjectCardRenameForm } from './ProjectCardRenameForm'
|
import { ProjectCardRenameForm } from './ProjectCardRenameForm'
|
||||||
import { Project } from 'lib/project'
|
import { Project } from 'lib/project'
|
||||||
|
import { toSync } from 'lib/utils'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
function ProjectCard({
|
function ProjectCard({
|
||||||
project,
|
project,
|
||||||
@ -165,10 +167,10 @@ function ProjectCard({
|
|||||||
{isConfirmingDelete && (
|
{isConfirmingDelete && (
|
||||||
<DeleteConfirmationDialog
|
<DeleteConfirmationDialog
|
||||||
title="Delete Project"
|
title="Delete Project"
|
||||||
onConfirm={async () => {
|
onConfirm={toSync(async () => {
|
||||||
await handleDeleteProject(project)
|
await handleDeleteProject(project)
|
||||||
setIsConfirmingDelete(false)
|
setIsConfirmingDelete(false)
|
||||||
}}
|
}, reportRejection)}
|
||||||
onDismiss={() => setIsConfirmingDelete(false)}
|
onDismiss={() => setIsConfirmingDelete(false)}
|
||||||
>
|
>
|
||||||
<p className="my-4">
|
<p className="my-4">
|
||||||
|
@ -6,6 +6,8 @@ import React, { useMemo } from 'react'
|
|||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import Tooltip from './Tooltip'
|
import Tooltip from './Tooltip'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
import { toSync } from 'lib/utils'
|
||||||
|
|
||||||
export const RefreshButton = ({ children }: React.PropsWithChildren) => {
|
export const RefreshButton = ({ children }: React.PropsWithChildren) => {
|
||||||
const { auth } = useSettingsAuthContext()
|
const { auth } = useSettingsAuthContext()
|
||||||
@ -50,11 +52,12 @@ export const RefreshButton = ({ children }: React.PropsWithChildren) => {
|
|||||||
// Window may not be available in some environments
|
// Window may not be available in some environments
|
||||||
window?.location.reload()
|
window?.location.reload()
|
||||||
})
|
})
|
||||||
|
.catch(reportRejection)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={refresh}
|
onClick={toSync(refresh, reportRejection)}
|
||||||
className="p-1 m-0 bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 rounded-full border border-solid border-chalkboard-20 dark:border-chalkboard-90"
|
className="p-1 m-0 bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 rounded-full border border-solid border-chalkboard-20 dark:border-chalkboard-90"
|
||||||
>
|
>
|
||||||
<CustomIcon name="exclamationMark" className="w-5 h-5" />
|
<CustomIcon name="exclamationMark" className="w-5 h-5" />
|
||||||
|
@ -20,6 +20,8 @@ import { createAndOpenNewProject, getSettingsFolderPaths } from 'lib/desktopFS'
|
|||||||
import { useDotDotSlash } from 'hooks/useDotDotSlash'
|
import { useDotDotSlash } from 'hooks/useDotDotSlash'
|
||||||
import { ForwardedRef, forwardRef, useEffect } from 'react'
|
import { ForwardedRef, forwardRef, useEffect } from 'react'
|
||||||
import { useLspContext } from 'components/LspProvider'
|
import { useLspContext } from 'components/LspProvider'
|
||||||
|
import { toSync } from 'lib/utils'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
interface AllSettingsFieldsProps {
|
interface AllSettingsFieldsProps {
|
||||||
searchParamTab: SettingsLevel
|
searchParamTab: SettingsLevel
|
||||||
@ -54,7 +56,7 @@ export const AllSettingsFields = forwardRef(
|
|||||||
)
|
)
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
async function restartOnboarding() {
|
function restartOnboarding() {
|
||||||
send({
|
send({
|
||||||
type: `set.app.onboardingStatus`,
|
type: `set.app.onboardingStatus`,
|
||||||
data: { level: 'user', value: '' },
|
data: { level: 'user', value: '' },
|
||||||
@ -82,6 +84,7 @@ export const AllSettingsFields = forwardRef(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
navigateToOnboardingStart()
|
navigateToOnboardingStart()
|
||||||
}, [isFileSettings, navigate, state])
|
}, [isFileSettings, navigate, state])
|
||||||
|
|
||||||
@ -190,7 +193,7 @@ export const AllSettingsFields = forwardRef(
|
|||||||
{isDesktop() && (
|
{isDesktop() && (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={async () => {
|
onClick={toSync(async () => {
|
||||||
const paths = await getSettingsFolderPaths(
|
const paths = await getSettingsFolderPaths(
|
||||||
projectPath ? decodeURIComponent(projectPath) : undefined
|
projectPath ? decodeURIComponent(projectPath) : undefined
|
||||||
)
|
)
|
||||||
@ -199,7 +202,7 @@ export const AllSettingsFields = forwardRef(
|
|||||||
return new Error('finalPath undefined')
|
return new Error('finalPath undefined')
|
||||||
}
|
}
|
||||||
window.electron.showInFolder(finalPath)
|
window.electron.showInFolder(finalPath)
|
||||||
}}
|
}, reportRejection)}
|
||||||
iconStart={{
|
iconStart={{
|
||||||
icon: 'folder',
|
icon: 'folder',
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
@ -211,14 +214,14 @@ export const AllSettingsFields = forwardRef(
|
|||||||
)}
|
)}
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={async () => {
|
onClick={toSync(async () => {
|
||||||
const defaultDirectory = await getInitialDefaultDir()
|
const defaultDirectory = await getInitialDefaultDir()
|
||||||
send({
|
send({
|
||||||
type: 'Reset settings',
|
type: 'Reset settings',
|
||||||
defaultDirectory,
|
defaultDirectory,
|
||||||
})
|
})
|
||||||
toast.success('Settings restored to default')
|
toast.success('Settings restored to default')
|
||||||
}}
|
}, reportRejection)}
|
||||||
iconStart={{
|
iconStart={{
|
||||||
icon: 'refresh',
|
icon: 'refresh',
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
} from 'lib/settings/settingsTypes'
|
} from 'lib/settings/settingsTypes'
|
||||||
import { getSettingInputType } from 'lib/settings/settingsUtils'
|
import { getSettingInputType } from 'lib/settings/settingsUtils'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { Event } from 'xstate'
|
import { EventFrom } from 'xstate'
|
||||||
|
|
||||||
interface SettingsFieldInputProps {
|
interface SettingsFieldInputProps {
|
||||||
// We don't need the fancy types here,
|
// We don't need the fancy types here,
|
||||||
@ -59,7 +59,7 @@ export function SettingsFieldInput({
|
|||||||
level: settingsLevel,
|
level: settingsLevel,
|
||||||
value: newValue,
|
value: newValue,
|
||||||
},
|
},
|
||||||
} as unknown as Event<WildcardSetEvent>)
|
} as unknown as EventFrom<WildcardSetEvent>)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@ -103,7 +103,7 @@ export function SettingsFieldInput({
|
|||||||
level: settingsLevel,
|
level: settingsLevel,
|
||||||
value: e.target.value,
|
value: e.target.value,
|
||||||
},
|
},
|
||||||
} as unknown as Event<WildcardSetEvent>)
|
} as unknown as EventFrom<WildcardSetEvent>)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{options &&
|
{options &&
|
||||||
@ -137,7 +137,7 @@ export function SettingsFieldInput({
|
|||||||
level: settingsLevel,
|
level: settingsLevel,
|
||||||
value: e.target.value,
|
value: e.target.value,
|
||||||
},
|
},
|
||||||
} as unknown as Event<WildcardSetEvent>)
|
} as unknown as EventFrom<WildcardSetEvent>)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -14,13 +14,7 @@ import {
|
|||||||
Themes,
|
Themes,
|
||||||
} from 'lib/theme'
|
} from 'lib/theme'
|
||||||
import decamelize from 'decamelize'
|
import decamelize from 'decamelize'
|
||||||
import {
|
import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate'
|
||||||
AnyStateMachine,
|
|
||||||
ContextFrom,
|
|
||||||
InterpreterFrom,
|
|
||||||
Prop,
|
|
||||||
StateFrom,
|
|
||||||
} from 'xstate'
|
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
|
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
|
||||||
import { kclManager, sceneInfra, engineCommandManager } from 'lib/singletons'
|
import { kclManager, sceneInfra, engineCommandManager } from 'lib/singletons'
|
||||||
@ -39,7 +33,7 @@ import { saveSettings } from 'lib/settings/settingsUtils'
|
|||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
context: ContextFrom<T>
|
context: ContextFrom<T>
|
||||||
send: Prop<InterpreterFrom<T>, 'send'>
|
send: Prop<Actor<T>, 'send'>
|
||||||
}
|
}
|
||||||
|
|
||||||
type SettingsAuthContextType = {
|
type SettingsAuthContextType = {
|
||||||
@ -50,7 +44,7 @@ type SettingsAuthContextType = {
|
|||||||
// a little hacky for sure, open to changing it
|
// a little hacky for sure, open to changing it
|
||||||
// this implies that we should only even have one instance of this provider mounted at any one time
|
// this implies that we should only even have one instance of this provider mounted at any one time
|
||||||
// but I think that's a safe assumption
|
// but I think that's a safe assumption
|
||||||
let settingsStateRef: (typeof settingsMachine)['context'] | undefined
|
let settingsStateRef: ContextFrom<typeof settingsMachine> | undefined
|
||||||
export const getSettingsState = () => settingsStateRef
|
export const getSettingsState = () => settingsStateRef
|
||||||
|
|
||||||
export const SettingsAuthContext = createContext({} as SettingsAuthContextType)
|
export const SettingsAuthContext = createContext({} as SettingsAuthContextType)
|
||||||
@ -101,21 +95,20 @@ export const SettingsAuthProviderBase = ({
|
|||||||
const { commandBarSend } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
|
|
||||||
const [settingsState, settingsSend, settingsActor] = useMachine(
|
const [settingsState, settingsSend, settingsActor] = useMachine(
|
||||||
settingsMachine,
|
settingsMachine.provide({
|
||||||
{
|
|
||||||
context: loadedSettings,
|
|
||||||
actions: {
|
actions: {
|
||||||
//TODO: batch all these and if that's difficult to do from tsx,
|
//TODO: batch all these and if that's difficult to do from tsx,
|
||||||
// make it easy to do
|
// make it easy to do
|
||||||
|
|
||||||
setClientSideSceneUnits: (context, event) => {
|
setClientSideSceneUnits: ({ context, event }) => {
|
||||||
const newBaseUnit =
|
const newBaseUnit =
|
||||||
event.type === 'set.modeling.defaultUnit'
|
event.type === 'set.modeling.defaultUnit'
|
||||||
? (event.data.value as BaseUnit)
|
? (event.data.value as BaseUnit)
|
||||||
: context.modeling.defaultUnit.current
|
: context.modeling.defaultUnit.current
|
||||||
sceneInfra.baseUnit = newBaseUnit
|
sceneInfra.baseUnit = newBaseUnit
|
||||||
},
|
},
|
||||||
setEngineTheme: (context) => {
|
setEngineTheme: ({ context }) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
engineCommandManager.sendSceneCommand({
|
engineCommandManager.sendSceneCommand({
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
@ -126,6 +119,7 @@ export const SettingsAuthProviderBase = ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
engineCommandManager.sendSceneCommand({
|
engineCommandManager.sendSceneCommand({
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
@ -135,16 +129,17 @@ export const SettingsAuthProviderBase = ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
setEngineScaleGridVisibility: (context) => {
|
setEngineScaleGridVisibility: ({ context }) => {
|
||||||
engineCommandManager.setScaleGridVisibility(
|
engineCommandManager.setScaleGridVisibility(
|
||||||
context.modeling.showScaleGrid.current
|
context.modeling.showScaleGrid.current
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
setClientTheme: (context) => {
|
setClientTheme: ({ context }) => {
|
||||||
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
||||||
sceneInfra.theme = opposingTheme
|
sceneInfra.theme = opposingTheme
|
||||||
},
|
},
|
||||||
setEngineEdges: (context) => {
|
setEngineEdges: ({ context }) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
engineCommandManager.sendSceneCommand({
|
engineCommandManager.sendSceneCommand({
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
@ -154,7 +149,8 @@ export const SettingsAuthProviderBase = ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
toastSuccess: (_, event) => {
|
toastSuccess: ({ event }) => {
|
||||||
|
if (!('data' in event)) return
|
||||||
const eventParts = event.type.replace(/^set./, '').split('.') as [
|
const eventParts = event.type.replace(/^set./, '').split('.') as [
|
||||||
keyof typeof settings,
|
keyof typeof settings,
|
||||||
string
|
string
|
||||||
@ -176,7 +172,7 @@ export const SettingsAuthProviderBase = ({
|
|||||||
id: `${event.type}.success`,
|
id: `${event.type}.success`,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
'Execute AST': (context, event) => {
|
'Execute AST': ({ context, event }) => {
|
||||||
try {
|
try {
|
||||||
const allSettingsIncludesUnitChange =
|
const allSettingsIncludesUnitChange =
|
||||||
event.type === 'Set all settings' &&
|
event.type === 'Set all settings' &&
|
||||||
@ -193,6 +189,7 @@ export const SettingsAuthProviderBase = ({
|
|||||||
resetSettingsIncludesUnitChange
|
resetSettingsIncludesUnitChange
|
||||||
) {
|
) {
|
||||||
// Unit changes requires a re-exec of code
|
// Unit changes requires a re-exec of code
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
kclManager.executeCode(true)
|
kclManager.executeCode(true)
|
||||||
} else {
|
} else {
|
||||||
// For any future logging we'd like to do
|
// For any future logging we'd like to do
|
||||||
@ -204,12 +201,13 @@ export const SettingsAuthProviderBase = ({
|
|||||||
console.error('Error executing AST after settings change', e)
|
console.error('Error executing AST after settings change', e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
persistSettings: ({ context }) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
saveSettings(context, loadedProject?.project?.path)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
services: {
|
}),
|
||||||
'Persist settings': (context) =>
|
{ input: loadedSettings }
|
||||||
saveSettings(context, loadedProject?.project?.path),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
settingsStateRef = settingsState.context
|
settingsStateRef = settingsState.context
|
||||||
|
|
||||||
@ -292,19 +290,22 @@ export const SettingsAuthProviderBase = ({
|
|||||||
}, [settingsState.context.textEditor.blinkingCursor.current])
|
}, [settingsState.context.textEditor.blinkingCursor.current])
|
||||||
|
|
||||||
// Auth machine setup
|
// Auth machine setup
|
||||||
const [authState, authSend, authActor] = useMachine(authMachine, {
|
const [authState, authSend, authActor] = useMachine(
|
||||||
actions: {
|
authMachine.provide({
|
||||||
goToSignInPage: () => {
|
actions: {
|
||||||
navigate(PATHS.SIGN_IN)
|
goToSignInPage: () => {
|
||||||
logout()
|
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)
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
goToIndexPage: () => {
|
})
|
||||||
if (location.pathname.includes(PATHS.SIGN_IN)) {
|
)
|
||||||
navigate(PATHS.INDEX)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
useStateMachineCommands({
|
useStateMachineCommands({
|
||||||
machineId: 'auth',
|
machineId: 'auth',
|
||||||
@ -336,13 +337,11 @@ export const SettingsAuthProviderBase = ({
|
|||||||
|
|
||||||
export default SettingsAuthProvider
|
export default SettingsAuthProvider
|
||||||
|
|
||||||
export function logout() {
|
export async function logout() {
|
||||||
localStorage.removeItem(TOKEN_PERSIST_KEY)
|
localStorage.removeItem(TOKEN_PERSIST_KEY)
|
||||||
return (
|
if (isDesktop()) return Promise.resolve(null)
|
||||||
!isDesktop() &&
|
return fetch(withBaseUrl('/logout'), {
|
||||||
fetch(withBaseUrl('/logout'), {
|
method: 'POST',
|
||||||
method: 'POST',
|
credentials: 'include',
|
||||||
credentials: 'include',
|
})
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -53,9 +53,10 @@ export const Stream = () => {
|
|||||||
* executed. If we can find a way to do this from a more
|
* executed. If we can find a way to do this from a more
|
||||||
* central place, we can move this code there.
|
* central place, we can move this code there.
|
||||||
*/
|
*/
|
||||||
async function executeCodeAndPlayStream() {
|
function executeCodeAndPlayStream() {
|
||||||
kclManager.executeCode(true).then(() => {
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
videoRef.current?.play().catch((e) => {
|
kclManager.executeCode(true).then(async () => {
|
||||||
|
await videoRef.current?.play().catch((e) => {
|
||||||
console.warn('Video playing was prevented', e, videoRef.current)
|
console.warn('Video playing was prevented', e, videoRef.current)
|
||||||
})
|
})
|
||||||
setStreamState(StreamState.Playing)
|
setStreamState(StreamState.Playing)
|
||||||
@ -218,12 +219,12 @@ export const Stream = () => {
|
|||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!kclManager.isExecuting) {
|
if (!kclManager.isExecuting) {
|
||||||
setTimeout(() =>
|
setTimeout(() => {
|
||||||
// execute in the next event loop
|
// execute in the next event loop
|
||||||
videoRef.current?.play().catch((e) => {
|
videoRef.current?.play().catch((e) => {
|
||||||
console.warn('Video playing was prevented', e, videoRef.current)
|
console.warn('Video playing was prevented', e, videoRef.current)
|
||||||
})
|
})
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
}, [kclManager.isExecuting])
|
}, [kclManager.isExecuting])
|
||||||
|
|
||||||
@ -287,9 +288,10 @@ export const Stream = () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (state.matches('Sketch')) return
|
if (state.matches('Sketch')) return
|
||||||
if (state.matches('idle.showPlanes')) return
|
if (state.matches({ idle: 'showPlanes' })) return
|
||||||
|
|
||||||
if (!context.store?.didDragInStream && btnName(e).left) {
|
if (!context.store?.didDragInStream && btnName(e).left) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
sendSelectEventToEngine(
|
sendSelectEventToEngine(
|
||||||
e,
|
e,
|
||||||
videoRef.current,
|
videoRef.current,
|
||||||
|
@ -26,8 +26,9 @@ import { sendTelemetry } from 'lib/textToCad'
|
|||||||
import { Themes } from 'lib/theme'
|
import { Themes } from 'lib/theme'
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import { commandBarMachine } from 'machines/commandBarMachine'
|
import { commandBarMachine } from 'machines/commandBarMachine'
|
||||||
import { EventData, EventFrom } from 'xstate'
|
import { EventFrom } from 'xstate'
|
||||||
import { fileMachine } from 'machines/fileMachine'
|
import { fileMachine } from 'machines/fileMachine'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
const CANVAS_SIZE = 128
|
const CANVAS_SIZE = 128
|
||||||
const PROMPT_TRUNCATE_LENGTH = 128
|
const PROMPT_TRUNCATE_LENGTH = 128
|
||||||
@ -45,7 +46,7 @@ export function ToastTextToCadError({
|
|||||||
prompt: string
|
prompt: string
|
||||||
commandBarSend: (
|
commandBarSend: (
|
||||||
event: EventFrom<typeof commandBarMachine>,
|
event: EventFrom<typeof commandBarMachine>,
|
||||||
data?: EventData
|
data?: unknown
|
||||||
) => void
|
) => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@ -112,7 +113,7 @@ export function ToastTextToCadSuccess({
|
|||||||
token?: string
|
token?: string
|
||||||
fileMachineSend: (
|
fileMachineSend: (
|
||||||
event: EventFrom<typeof fileMachine>,
|
event: EventFrom<typeof fileMachine>,
|
||||||
data?: EventData
|
data?: unknown
|
||||||
) => void
|
) => void
|
||||||
settings: {
|
settings: {
|
||||||
theme: Themes
|
theme: Themes
|
||||||
@ -297,7 +298,7 @@ export function ToastTextToCadSuccess({
|
|||||||
name={hasCopied ? 'Close' : 'Reject'}
|
name={hasCopied ? 'Close' : 'Reject'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!hasCopied) {
|
if (!hasCopied) {
|
||||||
sendTelemetry(modelId, 'rejected', token)
|
sendTelemetry(modelId, 'rejected', token).catch(reportRejection)
|
||||||
}
|
}
|
||||||
if (isDesktop()) {
|
if (isDesktop()) {
|
||||||
// Delete the file from the project
|
// Delete the file from the project
|
||||||
@ -323,6 +324,7 @@ export function ToastTextToCadSuccess({
|
|||||||
}}
|
}}
|
||||||
name="Accept"
|
name="Accept"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
sendTelemetry(modelId, 'accepted', token)
|
sendTelemetry(modelId, 'accepted', token)
|
||||||
navigate(
|
navigate(
|
||||||
`${PATHS.FILE}/${encodeURIComponent(
|
`${PATHS.FILE}/${encodeURIComponent(
|
||||||
@ -342,7 +344,9 @@ export function ToastTextToCadSuccess({
|
|||||||
}}
|
}}
|
||||||
name="Copy to clipboard"
|
name="Copy to clipboard"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
sendTelemetry(modelId, 'accepted', token)
|
sendTelemetry(modelId, 'accepted', token)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
navigator.clipboard.writeText(data.code || '// no code found')
|
navigator.clipboard.writeText(data.code || '// no code found')
|
||||||
setShowCopiedUi(true)
|
setShowCopiedUi(true)
|
||||||
setHasCopied(true)
|
setHasCopied(true)
|
||||||
|
@ -133,7 +133,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
|||||||
Element: 'button',
|
Element: 'button',
|
||||||
'data-testid': 'user-sidebar-sign-out',
|
'data-testid': 'user-sidebar-sign-out',
|
||||||
children: 'Sign out',
|
children: 'Sign out',
|
||||||
onClick: () => send('Log out'),
|
onClick: () => send({ type: 'Log out' }),
|
||||||
className: '', // Just making TS's filter type coercion happy 😠
|
className: '', // Just making TS's filter type coercion happy 😠
|
||||||
},
|
},
|
||||||
].filter(
|
].filter(
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { EditorView, ViewUpdate } from '@codemirror/view'
|
import { EditorView, ViewUpdate } from '@codemirror/view'
|
||||||
import { EditorSelection, Annotation, Transaction } from '@codemirror/state'
|
import { EditorSelection, Annotation, Transaction } from '@codemirror/state'
|
||||||
import { engineCommandManager } from 'lib/singletons'
|
import { engineCommandManager } from 'lib/singletons'
|
||||||
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
import { modelingMachine, ModelingMachineEvent } from 'machines/modelingMachine'
|
||||||
import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
|
import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
|
||||||
import { undo, redo } from '@codemirror/commands'
|
import { undo, redo } from '@codemirror/commands'
|
||||||
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
|
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
|
||||||
@ -11,6 +11,7 @@ import {
|
|||||||
forEachDiagnostic,
|
forEachDiagnostic,
|
||||||
setDiagnosticsEffect,
|
setDiagnosticsEffect,
|
||||||
} from '@codemirror/lint'
|
} from '@codemirror/lint'
|
||||||
|
import { StateFrom } from 'xstate'
|
||||||
|
|
||||||
const updateOutsideEditorAnnotation = Annotation.define<boolean>()
|
const updateOutsideEditorAnnotation = Annotation.define<boolean>()
|
||||||
export const updateOutsideEditorEvent = updateOutsideEditorAnnotation.of(true)
|
export const updateOutsideEditorEvent = updateOutsideEditorAnnotation.of(true)
|
||||||
@ -38,7 +39,7 @@ export default class EditorManager {
|
|||||||
private _lastEvent: { event: string; time: number } | null = null
|
private _lastEvent: { event: string; time: number } | null = null
|
||||||
|
|
||||||
private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {}
|
private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {}
|
||||||
private _modelingEvent: ModelingMachineEvent | null = null
|
private _modelingState: StateFrom<typeof modelingMachine> | null = null
|
||||||
|
|
||||||
private _commandBarSend: (eventInfo: CommandBarMachineEvent) => void =
|
private _commandBarSend: (eventInfo: CommandBarMachineEvent) => void =
|
||||||
() => {}
|
() => {}
|
||||||
@ -80,8 +81,8 @@ export default class EditorManager {
|
|||||||
this._modelingSend = send
|
this._modelingSend = send
|
||||||
}
|
}
|
||||||
|
|
||||||
set modelingEvent(event: ModelingMachineEvent) {
|
set modelingState(state: StateFrom<typeof modelingMachine>) {
|
||||||
this._modelingEvent = event
|
this._modelingState = state
|
||||||
}
|
}
|
||||||
|
|
||||||
setCommandBarSend(send: (eventInfo: CommandBarMachineEvent) => void) {
|
setCommandBarSend(send: (eventInfo: CommandBarMachineEvent) => void) {
|
||||||
@ -248,13 +249,11 @@ export default class EditorManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const ignoreEvents: ModelingMachineEvent['type'][] = ['change tool']
|
if (!this._modelingState) {
|
||||||
|
|
||||||
if (!this._modelingEvent) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ignoreEvents.includes(this._modelingEvent.type)) {
|
if (this._modelingState.matches({ Sketch: 'Change Tool' })) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -286,8 +285,9 @@ export default class EditorManager {
|
|||||||
|
|
||||||
this._lastEvent = { event: stringEvent, time: Date.now() }
|
this._lastEvent = { event: stringEvent, time: Date.now() }
|
||||||
this._modelingSend(eventInfo.modelingEvent)
|
this._modelingSend(eventInfo.modelingEvent)
|
||||||
eventInfo.engineEvents.forEach((event) =>
|
eventInfo.engineEvents.forEach((event) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
engineCommandManager.sendSceneCommand(event)
|
engineCommandManager.sendSceneCommand(event)
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,7 @@ import { CopilotCompletionResponse } from 'wasm-lib/kcl/bindings/CopilotCompleti
|
|||||||
import { CopilotAcceptCompletionParams } from 'wasm-lib/kcl/bindings/CopilotAcceptCompletionParams'
|
import { CopilotAcceptCompletionParams } from 'wasm-lib/kcl/bindings/CopilotAcceptCompletionParams'
|
||||||
import { CopilotRejectCompletionParams } from 'wasm-lib/kcl/bindings/CopilotRejectCompletionParams'
|
import { CopilotRejectCompletionParams } from 'wasm-lib/kcl/bindings/CopilotRejectCompletionParams'
|
||||||
import { editorManager } from 'lib/singletons'
|
import { editorManager } from 'lib/singletons'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
const copilotPluginAnnotation = Annotation.define<boolean>()
|
const copilotPluginAnnotation = Annotation.define<boolean>()
|
||||||
export const copilotPluginEvent = copilotPluginAnnotation.of(true)
|
export const copilotPluginEvent = copilotPluginAnnotation.of(true)
|
||||||
@ -266,7 +267,7 @@ export class CompletionRequester implements PluginValue {
|
|||||||
|
|
||||||
if (!this.client.ready) return
|
if (!this.client.ready) return
|
||||||
try {
|
try {
|
||||||
this.requestCompletions()
|
this.requestCompletions().catch(reportRejection)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
@ -462,7 +463,7 @@ export class CompletionRequester implements PluginValue {
|
|||||||
annotations: [copilotPluginEvent, Transaction.addToHistory.of(true)],
|
annotations: [copilotPluginEvent, Transaction.addToHistory.of(true)],
|
||||||
})
|
})
|
||||||
|
|
||||||
this.accept(ghostText.uuid)
|
this.accept(ghostText.uuid).catch(reportRejection)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -490,7 +491,7 @@ export class CompletionRequester implements PluginValue {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
this.reject()
|
this.reject().catch(reportRejection)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,6 +96,7 @@ export class KclPlugin implements PluginValue {
|
|||||||
|
|
||||||
const newCode = viewUpdate.state.doc.toString()
|
const newCode = viewUpdate.state.doc.toString()
|
||||||
codeManager.code = newCode
|
codeManager.code = newCode
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
codeManager.writeToFile()
|
codeManager.writeToFile()
|
||||||
|
|
||||||
this.scheduleUpdateDoc()
|
this.scheduleUpdateDoc()
|
||||||
@ -117,6 +118,7 @@ export class KclPlugin implements PluginValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!this.client.ready) return
|
if (!this.client.ready) return
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
kclManager.executeCode()
|
kclManager.executeCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ import {
|
|||||||
CopilotWorkerOptions,
|
CopilotWorkerOptions,
|
||||||
} from 'editor/plugins/lsp/types'
|
} from 'editor/plugins/lsp/types'
|
||||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||||
import { err } from 'lib/trap'
|
import { err, reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
const intoServer: IntoServer = new IntoServer()
|
const intoServer: IntoServer = new IntoServer()
|
||||||
const fromServer: FromServer | Error = FromServer.create()
|
const fromServer: FromServer | Error = FromServer.create()
|
||||||
@ -60,7 +60,8 @@ export async function kclLspRun(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onmessage = function (event) {
|
// WebWorker message handler.
|
||||||
|
onmessage = function (event: MessageEvent) {
|
||||||
if (err(fromServer)) return
|
if (err(fromServer)) return
|
||||||
const { worker, eventType, eventData }: LspWorkerEvent = event.data
|
const { worker, eventType, eventData }: LspWorkerEvent = event.data
|
||||||
|
|
||||||
@ -70,7 +71,7 @@ onmessage = function (event) {
|
|||||||
| KclWorkerOptions
|
| KclWorkerOptions
|
||||||
| CopilotWorkerOptions
|
| CopilotWorkerOptions
|
||||||
initialise(wasmUrl)
|
initialise(wasmUrl)
|
||||||
.then((instantiatedModule) => {
|
.then(async (instantiatedModule) => {
|
||||||
console.log('Worker: WASM module loaded', worker, instantiatedModule)
|
console.log('Worker: WASM module loaded', worker, instantiatedModule)
|
||||||
const config = new ServerConfig(
|
const config = new ServerConfig(
|
||||||
intoServer,
|
intoServer,
|
||||||
@ -81,7 +82,7 @@ onmessage = function (event) {
|
|||||||
switch (worker) {
|
switch (worker) {
|
||||||
case LspWorker.Kcl:
|
case LspWorker.Kcl:
|
||||||
const kclData = eventData as KclWorkerOptions
|
const kclData = eventData as KclWorkerOptions
|
||||||
kclLspRun(
|
await kclLspRun(
|
||||||
config,
|
config,
|
||||||
null,
|
null,
|
||||||
kclData.token,
|
kclData.token,
|
||||||
@ -91,7 +92,11 @@ onmessage = function (event) {
|
|||||||
break
|
break
|
||||||
case LspWorker.Copilot:
|
case LspWorker.Copilot:
|
||||||
let copilotData = eventData as CopilotWorkerOptions
|
let copilotData = eventData as CopilotWorkerOptions
|
||||||
copilotLspRun(config, copilotData.token, copilotData.apiBaseUrl)
|
await copilotLspRun(
|
||||||
|
config,
|
||||||
|
copilotData.token,
|
||||||
|
copilotData.apiBaseUrl
|
||||||
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -104,7 +109,7 @@ onmessage = function (event) {
|
|||||||
intoServer.enqueue(data)
|
intoServer.enqueue(data)
|
||||||
const json: jsrpc.JSONRPCRequest = Codec.decode(data)
|
const json: jsrpc.JSONRPCRequest = Codec.decode(data)
|
||||||
if (null != json.id) {
|
if (null != json.id) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises, @typescript-eslint/no-non-null-assertion
|
||||||
fromServer.responses.get(json.id)!.then((response) => {
|
fromServer.responses.get(json.id)!.then((response) => {
|
||||||
const encoded = Codec.encode(response as jsrpc.JSONRPCResponse)
|
const encoded = Codec.encode(response as jsrpc.JSONRPCResponse)
|
||||||
postMessage(encoded)
|
postMessage(encoded)
|
||||||
@ -115,19 +120,17 @@ onmessage = function (event) {
|
|||||||
console.error('Worker: Unknown message type', worker, eventType)
|
console.error('Worker: Unknown message type', worker, eventType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
;(async () => {
|
||||||
new Promise<void>(async (resolve) => {
|
|
||||||
if (err(fromServer)) return
|
if (err(fromServer)) return
|
||||||
for await (const requests of fromServer.requests) {
|
for await (const requests of fromServer.requests) {
|
||||||
const encoded = Codec.encode(requests as jsrpc.JSONRPCRequest)
|
const encoded = Codec.encode(requests as jsrpc.JSONRPCRequest)
|
||||||
postMessage(encoded)
|
postMessage(encoded)
|
||||||
}
|
}
|
||||||
})
|
})().catch(reportRejection)
|
||||||
|
;(async () => {
|
||||||
new Promise<void>(async (resolve) => {
|
|
||||||
if (err(fromServer)) return
|
if (err(fromServer)) return
|
||||||
for await (const notification of fromServer.notifications) {
|
for await (const notification of fromServer.notifications) {
|
||||||
const encoded = Codec.encode(notification as jsrpc.JSONRPCRequest)
|
const encoded = Codec.encode(notification as jsrpc.JSONRPCRequest)
|
||||||
postMessage(encoded)
|
postMessage(encoded)
|
||||||
}
|
}
|
||||||
})
|
})().catch(reportRejection)
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { CommandsContext } from 'components/CommandBar/CommandBarProvider'
|
import { CommandsContext } from 'components/CommandBar/CommandBarProvider'
|
||||||
import { useContext } from 'react'
|
|
||||||
|
|
||||||
export const useCommandsContext = () => {
|
export const useCommandsContext = () => {
|
||||||
return useContext(CommandsContext)
|
const commandBarActor = CommandsContext.useActorRef()
|
||||||
|
const commandBarState = CommandsContext.useSelector((state) => state)
|
||||||
|
return {
|
||||||
|
commandBarSend: commandBarActor.send,
|
||||||
|
commandBarState,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ import {
|
|||||||
getSolid2dCodeRef,
|
getSolid2dCodeRef,
|
||||||
getWallCodeRef,
|
getWallCodeRef,
|
||||||
} from 'lang/std/artifactGraph'
|
} from 'lang/std/artifactGraph'
|
||||||
import { err } from 'lib/trap'
|
import { err, reportRejection } from 'lib/trap'
|
||||||
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
|
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
|
||||||
import { getNodePathFromSourceRange } from 'lang/queryAst'
|
import { getNodePathFromSourceRange } from 'lang/queryAst'
|
||||||
|
|
||||||
@ -86,9 +86,11 @@ export function useEngineConnectionSubscriptions() {
|
|||||||
})
|
})
|
||||||
const unSubClick = engineCommandManager.subscribeTo({
|
const unSubClick = engineCommandManager.subscribeTo({
|
||||||
event: 'select_with_point',
|
event: 'select_with_point',
|
||||||
callback: async (engineEvent) => {
|
callback: (engineEvent) => {
|
||||||
const event = await getEventForSelectWithPoint(engineEvent)
|
;(async () => {
|
||||||
event && send(event)
|
const event = await getEventForSelectWithPoint(engineEvent)
|
||||||
|
event && send(event)
|
||||||
|
})().catch(reportRejection)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return () => {
|
return () => {
|
||||||
@ -101,118 +103,120 @@ export function useEngineConnectionSubscriptions() {
|
|||||||
const unSub = engineCommandManager.subscribeTo({
|
const unSub = engineCommandManager.subscribeTo({
|
||||||
event: 'select_with_point',
|
event: 'select_with_point',
|
||||||
callback: state.matches('Sketch no face')
|
callback: state.matches('Sketch no face')
|
||||||
? async ({ data }) => {
|
? ({ data }) => {
|
||||||
let planeOrFaceId = data.entity_id
|
;(async () => {
|
||||||
if (!planeOrFaceId) return
|
let planeOrFaceId = data.entity_id
|
||||||
if (
|
if (!planeOrFaceId) return
|
||||||
engineCommandManager.defaultPlanes?.xy === planeOrFaceId ||
|
if (
|
||||||
engineCommandManager.defaultPlanes?.xz === planeOrFaceId ||
|
engineCommandManager.defaultPlanes?.xy === planeOrFaceId ||
|
||||||
engineCommandManager.defaultPlanes?.yz === planeOrFaceId ||
|
engineCommandManager.defaultPlanes?.xz === planeOrFaceId ||
|
||||||
engineCommandManager.defaultPlanes?.negXy === planeOrFaceId ||
|
engineCommandManager.defaultPlanes?.yz === planeOrFaceId ||
|
||||||
engineCommandManager.defaultPlanes?.negXz === planeOrFaceId ||
|
engineCommandManager.defaultPlanes?.negXy === planeOrFaceId ||
|
||||||
engineCommandManager.defaultPlanes?.negYz === planeOrFaceId
|
engineCommandManager.defaultPlanes?.negXz === planeOrFaceId ||
|
||||||
) {
|
engineCommandManager.defaultPlanes?.negYz === planeOrFaceId
|
||||||
let planeId = planeOrFaceId
|
) {
|
||||||
const defaultPlaneStrMap: Record<string, DefaultPlaneStr> = {
|
let planeId = planeOrFaceId
|
||||||
[engineCommandManager.defaultPlanes.xy]: 'XY',
|
const defaultPlaneStrMap: Record<string, DefaultPlaneStr> = {
|
||||||
[engineCommandManager.defaultPlanes.xz]: 'XZ',
|
[engineCommandManager.defaultPlanes.xy]: 'XY',
|
||||||
[engineCommandManager.defaultPlanes.yz]: 'YZ',
|
[engineCommandManager.defaultPlanes.xz]: 'XZ',
|
||||||
[engineCommandManager.defaultPlanes.negXy]: '-XY',
|
[engineCommandManager.defaultPlanes.yz]: 'YZ',
|
||||||
[engineCommandManager.defaultPlanes.negXz]: '-XZ',
|
[engineCommandManager.defaultPlanes.negXy]: '-XY',
|
||||||
[engineCommandManager.defaultPlanes.negYz]: '-YZ',
|
[engineCommandManager.defaultPlanes.negXz]: '-XZ',
|
||||||
}
|
[engineCommandManager.defaultPlanes.negYz]: '-YZ',
|
||||||
// TODO can we get this information from rust land when it creates the default planes?
|
}
|
||||||
// maybe returned from make_default_planes (src/wasm-lib/src/wasm.rs)
|
// TODO can we get this information from rust land when it creates the default planes?
|
||||||
let zAxis: [number, number, number] = [0, 0, 1]
|
// maybe returned from make_default_planes (src/wasm-lib/src/wasm.rs)
|
||||||
let yAxis: [number, number, number] = [0, 1, 0]
|
let zAxis: [number, number, number] = [0, 0, 1]
|
||||||
|
let yAxis: [number, number, number] = [0, 1, 0]
|
||||||
|
|
||||||
// get unit vector from camera position to target
|
// get unit vector from camera position to target
|
||||||
const camVector = sceneInfra.camControls.camera.position
|
const camVector = sceneInfra.camControls.camera.position
|
||||||
.clone()
|
.clone()
|
||||||
.sub(sceneInfra.camControls.target)
|
.sub(sceneInfra.camControls.target)
|
||||||
|
|
||||||
if (engineCommandManager.defaultPlanes?.xy === planeId) {
|
if (engineCommandManager.defaultPlanes?.xy === planeId) {
|
||||||
zAxis = [0, 0, 1]
|
zAxis = [0, 0, 1]
|
||||||
yAxis = [0, 1, 0]
|
yAxis = [0, 1, 0]
|
||||||
if (camVector.z < 0) {
|
if (camVector.z < 0) {
|
||||||
zAxis = [0, 0, -1]
|
zAxis = [0, 0, -1]
|
||||||
planeId = engineCommandManager.defaultPlanes?.negXy || ''
|
planeId = engineCommandManager.defaultPlanes?.negXy || ''
|
||||||
}
|
}
|
||||||
} else if (engineCommandManager.defaultPlanes?.yz === planeId) {
|
} else if (engineCommandManager.defaultPlanes?.yz === planeId) {
|
||||||
zAxis = [1, 0, 0]
|
zAxis = [1, 0, 0]
|
||||||
yAxis = [0, 0, 1]
|
yAxis = [0, 0, 1]
|
||||||
if (camVector.x < 0) {
|
if (camVector.x < 0) {
|
||||||
zAxis = [-1, 0, 0]
|
zAxis = [-1, 0, 0]
|
||||||
planeId = engineCommandManager.defaultPlanes?.negYz || ''
|
planeId = engineCommandManager.defaultPlanes?.negYz || ''
|
||||||
}
|
}
|
||||||
} else if (engineCommandManager.defaultPlanes?.xz === planeId) {
|
} else if (engineCommandManager.defaultPlanes?.xz === planeId) {
|
||||||
zAxis = [0, 1, 0]
|
zAxis = [0, 1, 0]
|
||||||
yAxis = [0, 0, 1]
|
yAxis = [0, 0, 1]
|
||||||
planeId = engineCommandManager.defaultPlanes?.negXz || ''
|
planeId = engineCommandManager.defaultPlanes?.negXz || ''
|
||||||
if (camVector.y < 0) {
|
if (camVector.y < 0) {
|
||||||
zAxis = [0, -1, 0]
|
zAxis = [0, -1, 0]
|
||||||
planeId = engineCommandManager.defaultPlanes?.xz || ''
|
planeId = engineCommandManager.defaultPlanes?.xz || ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sceneInfra.modelingSend({
|
||||||
|
type: 'Select default plane',
|
||||||
|
data: {
|
||||||
|
type: 'defaultPlane',
|
||||||
|
planeId: planeId,
|
||||||
|
plane: defaultPlaneStrMap[planeId],
|
||||||
|
zAxis,
|
||||||
|
yAxis,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
const faceId = planeOrFaceId
|
||||||
|
const artifact = engineCommandManager.artifactGraph.get(faceId)
|
||||||
|
const extrusion = getExtrusionFromSuspectedExtrudeSurface(
|
||||||
|
faceId,
|
||||||
|
engineCommandManager.artifactGraph
|
||||||
|
)
|
||||||
|
|
||||||
|
if (artifact?.type !== 'cap' && artifact?.type !== 'wall') return
|
||||||
|
|
||||||
|
const codeRef =
|
||||||
|
artifact.type === 'cap'
|
||||||
|
? getCapCodeRef(artifact, engineCommandManager.artifactGraph)
|
||||||
|
: getWallCodeRef(artifact, engineCommandManager.artifactGraph)
|
||||||
|
|
||||||
|
const faceInfo = await getFaceDetails(faceId)
|
||||||
|
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
|
||||||
|
return
|
||||||
|
const { z_axis, y_axis, origin } = faceInfo
|
||||||
|
const sketchPathToNode = getNodePathFromSourceRange(
|
||||||
|
kclManager.ast,
|
||||||
|
err(codeRef) ? [0, 0] : codeRef.range
|
||||||
|
)
|
||||||
|
|
||||||
|
const extrudePathToNode = !err(extrusion)
|
||||||
|
? getNodePathFromSourceRange(
|
||||||
|
kclManager.ast,
|
||||||
|
extrusion.codeRef.range
|
||||||
|
)
|
||||||
|
: []
|
||||||
|
|
||||||
sceneInfra.modelingSend({
|
sceneInfra.modelingSend({
|
||||||
type: 'Select default plane',
|
type: 'Select default plane',
|
||||||
data: {
|
data: {
|
||||||
type: 'defaultPlane',
|
type: 'extrudeFace',
|
||||||
planeId: planeId,
|
zAxis: [z_axis.x, z_axis.y, z_axis.z],
|
||||||
plane: defaultPlaneStrMap[planeId],
|
yAxis: [y_axis.x, y_axis.y, y_axis.z],
|
||||||
zAxis,
|
position: [origin.x, origin.y, origin.z].map(
|
||||||
yAxis,
|
(num) => num / sceneInfra._baseUnitMultiplier
|
||||||
|
) as [number, number, number],
|
||||||
|
sketchPathToNode,
|
||||||
|
extrudePathToNode,
|
||||||
|
cap: artifact.type === 'cap' ? artifact.subType : 'none',
|
||||||
|
faceId: faceId,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
})().catch(reportRejection)
|
||||||
const faceId = planeOrFaceId
|
|
||||||
const artifact = engineCommandManager.artifactGraph.get(faceId)
|
|
||||||
const extrusion = getExtrusionFromSuspectedExtrudeSurface(
|
|
||||||
faceId,
|
|
||||||
engineCommandManager.artifactGraph
|
|
||||||
)
|
|
||||||
|
|
||||||
if (artifact?.type !== 'cap' && artifact?.type !== 'wall') return
|
|
||||||
|
|
||||||
const codeRef =
|
|
||||||
artifact.type === 'cap'
|
|
||||||
? getCapCodeRef(artifact, engineCommandManager.artifactGraph)
|
|
||||||
: getWallCodeRef(artifact, engineCommandManager.artifactGraph)
|
|
||||||
|
|
||||||
const faceInfo = await getFaceDetails(faceId)
|
|
||||||
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
|
|
||||||
return
|
|
||||||
const { z_axis, y_axis, origin } = faceInfo
|
|
||||||
const sketchPathToNode = getNodePathFromSourceRange(
|
|
||||||
kclManager.ast,
|
|
||||||
err(codeRef) ? [0, 0] : codeRef.range
|
|
||||||
)
|
|
||||||
|
|
||||||
const extrudePathToNode = !err(extrusion)
|
|
||||||
? getNodePathFromSourceRange(
|
|
||||||
kclManager.ast,
|
|
||||||
extrusion.codeRef.range
|
|
||||||
)
|
|
||||||
: []
|
|
||||||
|
|
||||||
sceneInfra.modelingSend({
|
|
||||||
type: 'Select default plane',
|
|
||||||
data: {
|
|
||||||
type: 'extrudeFace',
|
|
||||||
zAxis: [z_axis.x, z_axis.y, z_axis.z],
|
|
||||||
yAxis: [y_axis.x, y_axis.y, y_axis.z],
|
|
||||||
position: [origin.x, origin.y, origin.z].map(
|
|
||||||
(num) => num / sceneInfra._baseUnitMultiplier
|
|
||||||
) as [number, number, number],
|
|
||||||
sketchPathToNode,
|
|
||||||
extrudePathToNode,
|
|
||||||
cap: artifact.type === 'cap' ? artifact.subType : 'none',
|
|
||||||
faceId: faceId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
: () => {},
|
: () => {},
|
||||||
})
|
})
|
||||||
|
@ -23,7 +23,8 @@ export function useRefreshSettings(routeId: string = PATHS.INDEX) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
ctx.settings.send('Set all settings', {
|
ctx.settings.send({
|
||||||
|
type: 'Set all settings',
|
||||||
settings: routeData,
|
settings: routeData,
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { AnyStateMachine, InterpreterFrom, StateFrom } from 'xstate'
|
import { AnyStateMachine, Actor, StateFrom } from 'xstate'
|
||||||
import { createMachineCommand } from '../lib/createMachineCommand'
|
import { createMachineCommand } from '../lib/createMachineCommand'
|
||||||
import { useCommandsContext } from './useCommandsContext'
|
import { useCommandsContext } from './useCommandsContext'
|
||||||
import { modelingMachine } from 'machines/modelingMachine'
|
import { modelingMachine } from 'machines/modelingMachine'
|
||||||
@ -15,6 +15,7 @@ import { useKclContext } from 'lang/KclProvider'
|
|||||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||||
import { useAppState } from 'AppState'
|
import { useAppState } from 'AppState'
|
||||||
|
import { getActorNextEvents } from 'lib/utils'
|
||||||
|
|
||||||
// This might not be necessary, AnyStateMachine from xstate is working
|
// This might not be necessary, AnyStateMachine from xstate is working
|
||||||
export type AllMachines =
|
export type AllMachines =
|
||||||
@ -30,7 +31,7 @@ interface UseStateMachineCommandsArgs<
|
|||||||
machineId: T['id']
|
machineId: T['id']
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
send: Function
|
send: Function
|
||||||
actor: InterpreterFrom<T>
|
actor: Actor<T>
|
||||||
commandBarConfig?: StateMachineCommandSetConfig<T, S>
|
commandBarConfig?: StateMachineCommandSetConfig<T, S>
|
||||||
allCommandsRequireNetwork?: boolean
|
allCommandsRequireNetwork?: boolean
|
||||||
onCancel?: () => void
|
onCancel?: () => void
|
||||||
@ -59,7 +60,7 @@ export default function useStateMachineCommands<
|
|||||||
overallState !== NetworkHealthState.Weak) ||
|
overallState !== NetworkHealthState.Weak) ||
|
||||||
isExecuting ||
|
isExecuting ||
|
||||||
!isStreamReady
|
!isStreamReady
|
||||||
const newCommands = state.nextEvents
|
const newCommands = getActorNextEvents(state)
|
||||||
.filter((_) => !allCommandsRequireNetwork || !disableAllButtons)
|
.filter((_) => !allCommandsRequireNetwork || !disableAllButtons)
|
||||||
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
|
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
|
||||||
.flatMap((type) =>
|
.flatMap((type) =>
|
||||||
|
@ -3,13 +3,14 @@ import {
|
|||||||
createSetVarNameModal,
|
createSetVarNameModal,
|
||||||
} from 'components/SetVarNameModal'
|
} from 'components/SetVarNameModal'
|
||||||
import { editorManager, kclManager } from 'lib/singletons'
|
import { editorManager, kclManager } from 'lib/singletons'
|
||||||
import { trap } from 'lib/trap'
|
import { reportRejection, trap } from 'lib/trap'
|
||||||
import { moveValueIntoNewVariable } from 'lang/modifyAst'
|
import { moveValueIntoNewVariable } from 'lang/modifyAst'
|
||||||
import { isNodeSafeToReplace } from 'lang/queryAst'
|
import { isNodeSafeToReplace } from 'lang/queryAst'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useModelingContext } from './useModelingContext'
|
import { useModelingContext } from './useModelingContext'
|
||||||
import { PathToNode, SourceRange } from 'lang/wasm'
|
import { PathToNode, SourceRange } from 'lang/wasm'
|
||||||
import { useKclContext } from 'lang/KclProvider'
|
import { useKclContext } from 'lang/KclProvider'
|
||||||
|
import { toSync } from 'lib/utils'
|
||||||
|
|
||||||
export const getVarNameModal = createSetVarNameModal(SetVarNameModal)
|
export const getVarNameModal = createSetVarNameModal(SetVarNameModal)
|
||||||
|
|
||||||
@ -62,7 +63,7 @@ export function useConvertToVariable(range?: SourceRange) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
editorManager.convertToVariableCallback = handleClick
|
editorManager.convertToVariableCallback = toSync(handleClick, reportRejection)
|
||||||
|
|
||||||
return { enable, handleClick }
|
return { enable, handleClick }
|
||||||
}
|
}
|
||||||
|
@ -50,14 +50,6 @@ body.dark {
|
|||||||
@apply text-chalkboard-10;
|
@apply text-chalkboard-10;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
body,
|
|
||||||
.body-bg,
|
|
||||||
.dark .body-bg {
|
|
||||||
@apply bg-chalkboard-100;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@apply bg-chalkboard-20;
|
@apply bg-chalkboard-20;
|
||||||
}
|
}
|
||||||
|
@ -129,8 +129,8 @@ export class KclManager {
|
|||||||
if (!isExecuting && this.executeIsStale) {
|
if (!isExecuting && this.executeIsStale) {
|
||||||
const args = this.executeIsStale
|
const args = this.executeIsStale
|
||||||
this.executeIsStale = null
|
this.executeIsStale = null
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.executeAst(args)
|
this.executeAst(args)
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
this._isExecutingCallback(isExecuting)
|
this._isExecutingCallback(isExecuting)
|
||||||
}
|
}
|
||||||
@ -154,6 +154,7 @@ export class KclManager {
|
|||||||
constructor(engineCommandManager: EngineCommandManager) {
|
constructor(engineCommandManager: EngineCommandManager) {
|
||||||
this.engineCommandManager = engineCommandManager
|
this.engineCommandManager = engineCommandManager
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.ensureWasmInit().then(() => {
|
this.ensureWasmInit().then(() => {
|
||||||
this.ast = this.safeParse(codeManager.code) || this.ast
|
this.ast = this.safeParse(codeManager.code) || this.ast
|
||||||
})
|
})
|
||||||
@ -400,9 +401,11 @@ export class KclManager {
|
|||||||
// Update the code state and the editor.
|
// Update the code state and the editor.
|
||||||
codeManager.updateCodeStateEditor(code)
|
codeManager.updateCodeStateEditor(code)
|
||||||
// Write back to the file system.
|
// Write back to the file system.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
codeManager.writeToFile()
|
codeManager.writeToFile()
|
||||||
|
|
||||||
// execute the code.
|
// execute the code.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.executeCode()
|
this.executeCode()
|
||||||
}
|
}
|
||||||
// There's overlapping responsibility between updateAst and executeAst.
|
// There's overlapping responsibility between updateAst and executeAst.
|
||||||
@ -541,6 +544,7 @@ function defaultSelectionFilter(
|
|||||||
programMemory: ProgramMemory,
|
programMemory: ProgramMemory,
|
||||||
engineCommandManager: EngineCommandManager
|
engineCommandManager: EngineCommandManager
|
||||||
) {
|
) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
programMemory.hasSketchOrExtrudeGroup() &&
|
programMemory.hasSketchOrExtrudeGroup() &&
|
||||||
engineCommandManager.sendSceneCommand({
|
engineCommandManager.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
|
@ -62,6 +62,7 @@ export async function executeAst({
|
|||||||
try {
|
try {
|
||||||
if (!useFakeExecutor) {
|
if (!useFakeExecutor) {
|
||||||
engineCommandManager.endSession()
|
engineCommandManager.endSession()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
engineCommandManager.startNewSession()
|
engineCommandManager.startNewSession()
|
||||||
}
|
}
|
||||||
const programMemory = await (useFakeExecutor
|
const programMemory = await (useFakeExecutor
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Selection } from 'lib/selections'
|
import { Selection } from 'lib/selections'
|
||||||
import { err, trap } from 'lib/trap'
|
import { err, reportRejection, trap } from 'lib/trap'
|
||||||
import {
|
import {
|
||||||
Program,
|
Program,
|
||||||
CallExpression,
|
CallExpression,
|
||||||
@ -904,115 +904,119 @@ export async function deleteFromSelection(
|
|||||||
const expressionIndex = pathToNode[1][0] as number
|
const expressionIndex = pathToNode[1][0] as number
|
||||||
astClone.body.splice(expressionIndex, 1)
|
astClone.body.splice(expressionIndex, 1)
|
||||||
if (extrudeNameToDelete) {
|
if (extrudeNameToDelete) {
|
||||||
await new Promise(async (resolve) => {
|
await new Promise((resolve) => {
|
||||||
let currentVariableName = ''
|
;(async () => {
|
||||||
const pathsDependingOnExtrude: Array<{
|
let currentVariableName = ''
|
||||||
path: PathToNode
|
const pathsDependingOnExtrude: Array<{
|
||||||
sketchName: string
|
path: PathToNode
|
||||||
}> = []
|
sketchName: string
|
||||||
traverse(astClone, {
|
}> = []
|
||||||
leave: (node) => {
|
traverse(astClone, {
|
||||||
if (node.type === 'VariableDeclaration') {
|
leave: (node) => {
|
||||||
currentVariableName = ''
|
if (node.type === 'VariableDeclaration') {
|
||||||
}
|
currentVariableName = ''
|
||||||
},
|
}
|
||||||
enter: async (node, path) => {
|
},
|
||||||
if (node.type === 'VariableDeclaration') {
|
enter: (node, path) => {
|
||||||
currentVariableName = node.declarations[0].id.name
|
;(async () => {
|
||||||
}
|
if (node.type === 'VariableDeclaration') {
|
||||||
if (
|
currentVariableName = node.declarations[0].id.name
|
||||||
// match startSketchOn(${extrudeNameToDelete})
|
}
|
||||||
node.type === 'CallExpression' &&
|
if (
|
||||||
node.callee.name === 'startSketchOn' &&
|
// match startSketchOn(${extrudeNameToDelete})
|
||||||
node.arguments[0].type === 'Identifier' &&
|
node.type === 'CallExpression' &&
|
||||||
node.arguments[0].name === extrudeNameToDelete
|
node.callee.name === 'startSketchOn' &&
|
||||||
) {
|
node.arguments[0].type === 'Identifier' &&
|
||||||
pathsDependingOnExtrude.push({
|
node.arguments[0].name === extrudeNameToDelete
|
||||||
path,
|
) {
|
||||||
sketchName: currentVariableName,
|
pathsDependingOnExtrude.push({
|
||||||
})
|
path,
|
||||||
}
|
sketchName: currentVariableName,
|
||||||
},
|
})
|
||||||
})
|
}
|
||||||
const roundLiteral = (x: number) => createLiteral(roundOff(x))
|
})().catch(reportRejection)
|
||||||
const modificationDetails: {
|
},
|
||||||
parent: PipeExpression['body']
|
|
||||||
faceDetails: Models['FaceIsPlanar_type']
|
|
||||||
lastKey: number
|
|
||||||
}[] = []
|
|
||||||
for (const { path, sketchName } of pathsDependingOnExtrude) {
|
|
||||||
const parent = getNodeFromPath<PipeExpression['body']>(
|
|
||||||
astClone,
|
|
||||||
path.slice(0, -1)
|
|
||||||
)
|
|
||||||
if (err(parent)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const sketchToPreserve = sketchGroupFromKclValue(
|
|
||||||
programMemory.get(sketchName),
|
|
||||||
sketchName
|
|
||||||
)
|
|
||||||
if (err(sketchToPreserve)) return sketchToPreserve
|
|
||||||
console.log('sketchName', sketchName)
|
|
||||||
// Can't kick off multiple requests at once as getFaceDetails
|
|
||||||
// is three engine calls in one and they conflict
|
|
||||||
const faceDetails = await getFaceDetails(sketchToPreserve.on.id)
|
|
||||||
if (
|
|
||||||
!(
|
|
||||||
faceDetails.origin &&
|
|
||||||
faceDetails.x_axis &&
|
|
||||||
faceDetails.y_axis &&
|
|
||||||
faceDetails.z_axis
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const lastKey = Number(path.slice(-1)[0][0])
|
|
||||||
modificationDetails.push({
|
|
||||||
parent: parent.node,
|
|
||||||
faceDetails,
|
|
||||||
lastKey,
|
|
||||||
})
|
})
|
||||||
}
|
const roundLiteral = (x: number) => createLiteral(roundOff(x))
|
||||||
for (const { parent, faceDetails, lastKey } of modificationDetails) {
|
const modificationDetails: {
|
||||||
if (
|
parent: PipeExpression['body']
|
||||||
!(
|
faceDetails: Models['FaceIsPlanar_type']
|
||||||
faceDetails.origin &&
|
lastKey: number
|
||||||
faceDetails.x_axis &&
|
}[] = []
|
||||||
faceDetails.y_axis &&
|
for (const { path, sketchName } of pathsDependingOnExtrude) {
|
||||||
faceDetails.z_axis
|
const parent = getNodeFromPath<PipeExpression['body']>(
|
||||||
|
astClone,
|
||||||
|
path.slice(0, -1)
|
||||||
)
|
)
|
||||||
) {
|
if (err(parent)) {
|
||||||
continue
|
return
|
||||||
|
}
|
||||||
|
const sketchToPreserve = sketchGroupFromKclValue(
|
||||||
|
programMemory.get(sketchName),
|
||||||
|
sketchName
|
||||||
|
)
|
||||||
|
if (err(sketchToPreserve)) return sketchToPreserve
|
||||||
|
console.log('sketchName', sketchName)
|
||||||
|
// Can't kick off multiple requests at once as getFaceDetails
|
||||||
|
// is three engine calls in one and they conflict
|
||||||
|
const faceDetails = await getFaceDetails(sketchToPreserve.on.id)
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
faceDetails.origin &&
|
||||||
|
faceDetails.x_axis &&
|
||||||
|
faceDetails.y_axis &&
|
||||||
|
faceDetails.z_axis
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const lastKey = Number(path.slice(-1)[0][0])
|
||||||
|
modificationDetails.push({
|
||||||
|
parent: parent.node,
|
||||||
|
faceDetails,
|
||||||
|
lastKey,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
parent[lastKey] = createCallExpressionStdLib('startSketchOn', [
|
for (const { parent, faceDetails, lastKey } of modificationDetails) {
|
||||||
createObjectExpression({
|
if (
|
||||||
plane: createObjectExpression({
|
!(
|
||||||
origin: createObjectExpression({
|
faceDetails.origin &&
|
||||||
x: roundLiteral(faceDetails.origin.x),
|
faceDetails.x_axis &&
|
||||||
y: roundLiteral(faceDetails.origin.y),
|
faceDetails.y_axis &&
|
||||||
z: roundLiteral(faceDetails.origin.z),
|
faceDetails.z_axis
|
||||||
}),
|
)
|
||||||
x_axis: createObjectExpression({
|
) {
|
||||||
x: roundLiteral(faceDetails.x_axis.x),
|
continue
|
||||||
y: roundLiteral(faceDetails.x_axis.y),
|
}
|
||||||
z: roundLiteral(faceDetails.x_axis.z),
|
parent[lastKey] = createCallExpressionStdLib('startSketchOn', [
|
||||||
}),
|
createObjectExpression({
|
||||||
y_axis: createObjectExpression({
|
plane: createObjectExpression({
|
||||||
x: roundLiteral(faceDetails.y_axis.x),
|
origin: createObjectExpression({
|
||||||
y: roundLiteral(faceDetails.y_axis.y),
|
x: roundLiteral(faceDetails.origin.x),
|
||||||
z: roundLiteral(faceDetails.y_axis.z),
|
y: roundLiteral(faceDetails.origin.y),
|
||||||
}),
|
z: roundLiteral(faceDetails.origin.z),
|
||||||
z_axis: createObjectExpression({
|
}),
|
||||||
x: roundLiteral(faceDetails.z_axis.x),
|
x_axis: createObjectExpression({
|
||||||
y: roundLiteral(faceDetails.z_axis.y),
|
x: roundLiteral(faceDetails.x_axis.x),
|
||||||
z: roundLiteral(faceDetails.z_axis.z),
|
y: roundLiteral(faceDetails.x_axis.y),
|
||||||
|
z: roundLiteral(faceDetails.x_axis.z),
|
||||||
|
}),
|
||||||
|
y_axis: createObjectExpression({
|
||||||
|
x: roundLiteral(faceDetails.y_axis.x),
|
||||||
|
y: roundLiteral(faceDetails.y_axis.y),
|
||||||
|
z: roundLiteral(faceDetails.y_axis.z),
|
||||||
|
}),
|
||||||
|
z_axis: createObjectExpression({
|
||||||
|
x: roundLiteral(faceDetails.z_axis.x),
|
||||||
|
y: roundLiteral(faceDetails.z_axis.y),
|
||||||
|
z: roundLiteral(faceDetails.z_axis.z),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
}),
|
])
|
||||||
])
|
}
|
||||||
}
|
resolve(true)
|
||||||
resolve(true)
|
})().catch(reportRejection)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// await prom
|
// await prom
|
||||||
|
@ -36,7 +36,7 @@ beforeAll(async () => {
|
|||||||
setMediaStream: () => {},
|
setMediaStream: () => {},
|
||||||
setIsStreamReady: () => {},
|
setIsStreamReady: () => {},
|
||||||
modifyGrid: async () => {},
|
modifyGrid: async () => {},
|
||||||
callbackOnEngineLiteConnect: async () => {
|
callbackOnEngineLiteConnect: () => {
|
||||||
resolve(true)
|
resolve(true)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -56,6 +56,7 @@ export function applyFilletToSelection(
|
|||||||
const { modifiedAst, pathToFilletNode } = result
|
const { modifiedAst, pathToFilletNode } = result
|
||||||
|
|
||||||
// 3. update ast
|
// 3. update ast
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
updateAstAndFocus(modifiedAst, pathToFilletNode)
|
updateAstAndFocus(modifiedAst, pathToFilletNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,6 +124,7 @@ beforeAll(async () => {
|
|||||||
setMediaStream: () => {},
|
setMediaStream: () => {},
|
||||||
setIsStreamReady: () => {},
|
setIsStreamReady: () => {},
|
||||||
modifyGrid: async () => {},
|
modifyGrid: async () => {},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
callbackOnEngineLiteConnect: async () => {
|
callbackOnEngineLiteConnect: async () => {
|
||||||
const cacheEntries = Object.entries(codeToWriteCacheFor) as [
|
const cacheEntries = Object.entries(codeToWriteCacheFor) as [
|
||||||
CodeKey,
|
CodeKey,
|
||||||
|
@ -18,6 +18,7 @@ import toast from 'react-hot-toast'
|
|||||||
import { SettingsViaQueryString } from 'lib/settings/settingsTypes'
|
import { SettingsViaQueryString } from 'lib/settings/settingsTypes'
|
||||||
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
|
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
|
||||||
import { KclManager } from 'lang/KclSingleton'
|
import { KclManager } from 'lang/KclSingleton'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
// TODO(paultag): This ought to be tweakable.
|
// TODO(paultag): This ought to be tweakable.
|
||||||
const pingIntervalMs = 5_000
|
const pingIntervalMs = 5_000
|
||||||
@ -388,11 +389,12 @@ class EngineConnection extends EventTarget {
|
|||||||
default:
|
default:
|
||||||
if (this.isConnecting()) break
|
if (this.isConnecting()) break
|
||||||
// Means we never could do an initial connection. Reconnect everything.
|
// Means we never could do an initial connection. Reconnect everything.
|
||||||
if (!this.pingPongSpan.ping) this.connect()
|
if (!this.pingPongSpan.ping) this.connect().catch(reportRejection)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}, pingIntervalMs)
|
}, pingIntervalMs)
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.connect()
|
this.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1464,6 +1466,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
this.onEngineConnectionOpened = async () => {
|
this.onEngineConnectionOpened = async () => {
|
||||||
// Set the stream background color
|
// Set the stream background color
|
||||||
// This takes RGBA values from 0-1
|
// This takes RGBA values from 0-1
|
||||||
@ -1480,6 +1483,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
|
|
||||||
// Sets the default line colors
|
// Sets the default line colors
|
||||||
const opposingTheme = getOppositeTheme(this.settings.theme)
|
const opposingTheme = getOppositeTheme(this.settings.theme)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.sendSceneCommand({
|
this.sendSceneCommand({
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
@ -1490,6 +1494,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Set the edge lines visibility
|
// Set the edge lines visibility
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.sendSceneCommand({
|
this.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
@ -1500,6 +1505,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this._camControlsCameraChange()
|
this._camControlsCameraChange()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.sendSceneCommand({
|
this.sendSceneCommand({
|
||||||
// CameraControls subscribes to default_camera_get_settings response events
|
// CameraControls subscribes to default_camera_get_settings response events
|
||||||
// firing this at connection ensure the camera's are synced initially
|
// firing this at connection ensure the camera's are synced initially
|
||||||
@ -1512,6 +1518,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
// We want modify the grid first because we don't want it to flash.
|
// We want modify the grid first because we don't want it to flash.
|
||||||
// Ideally these would already be default hidden in engine (TODO do
|
// Ideally these would already be default hidden in engine (TODO do
|
||||||
// that) https://github.com/KittyCAD/engine/issues/2282
|
// that) https://github.com/KittyCAD/engine/issues/2282
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.modifyGrid(!this.settings.showScaleGrid)?.then(async () => {
|
this.modifyGrid(!this.settings.showScaleGrid)?.then(async () => {
|
||||||
await this.initPlanes()
|
await this.initPlanes()
|
||||||
setIsStreamReady(true)
|
setIsStreamReady(true)
|
||||||
@ -1715,6 +1722,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
this.onEngineConnectionNewTrack as EventListener
|
this.onEngineConnectionNewTrack as EventListener
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.engineConnection?.connect()
|
this.engineConnection?.connect()
|
||||||
}
|
}
|
||||||
this.engineConnection.addEventListener(
|
this.engineConnection.addEventListener(
|
||||||
@ -2125,6 +2133,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
* @param visible - whether to show or hide the scale grid
|
* @param visible - whether to show or hide the scale grid
|
||||||
*/
|
*/
|
||||||
setScaleGridVisibility(visible: boolean) {
|
setScaleGridVisibility(visible: boolean) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.modifyGrid(!visible)
|
this.modifyGrid(!visible)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -360,6 +360,7 @@ export const executor = async (
|
|||||||
): Promise<ProgramMemory> => {
|
): Promise<ProgramMemory> => {
|
||||||
if (err(programMemory)) return Promise.reject(programMemory)
|
if (err(programMemory)) return Promise.reject(programMemory)
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
engineCommandManager.startNewSession()
|
engineCommandManager.startNewSession()
|
||||||
const _programMemory = await _executor(
|
const _programMemory = await _executor(
|
||||||
node,
|
node,
|
||||||
@ -569,6 +570,7 @@ export async function coreDump(
|
|||||||
a new GitHub issue for the user.
|
a new GitHub issue for the user.
|
||||||
*/
|
*/
|
||||||
if (openGithubIssue && dump.github_issue_url) {
|
if (openGithubIssue && dump.github_issue_url) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
openWindow(dump.github_issue_url)
|
openWindow(dump.github_issue_url)
|
||||||
} else {
|
} else {
|
||||||
console.error(
|
console.error(
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
} from 'lib/settings/settingsTypes'
|
} from 'lib/settings/settingsTypes'
|
||||||
import { settingsMachine } from 'machines/settingsMachine'
|
import { settingsMachine } from 'machines/settingsMachine'
|
||||||
import { PathValue } from 'lib/types'
|
import { PathValue } from 'lib/types'
|
||||||
import { AnyStateMachine, ContextFrom, InterpreterFrom } from 'xstate'
|
import { Actor, AnyStateMachine, ContextFrom } from 'xstate'
|
||||||
import { getPropertyByPath } from 'lib/objectPropertyByPath'
|
import { getPropertyByPath } from 'lib/objectPropertyByPath'
|
||||||
import { buildCommandArgument } from 'lib/createMachineCommand'
|
import { buildCommandArgument } from 'lib/createMachineCommand'
|
||||||
import decamelize from 'decamelize'
|
import decamelize from 'decamelize'
|
||||||
@ -28,7 +28,7 @@ export const settingsWithCommandConfigs = (
|
|||||||
) as SettingsPaths[]
|
) as SettingsPaths[]
|
||||||
|
|
||||||
const levelArgConfig = <T extends AnyStateMachine = AnyStateMachine>(
|
const levelArgConfig = <T extends AnyStateMachine = AnyStateMachine>(
|
||||||
actor: InterpreterFrom<T>,
|
actor: Actor<T>,
|
||||||
isProjectAvailable: boolean,
|
isProjectAvailable: boolean,
|
||||||
hideOnLevel?: SettingsLevel
|
hideOnLevel?: SettingsLevel
|
||||||
): CommandArgument<SettingsLevel, T> => ({
|
): CommandArgument<SettingsLevel, T> => ({
|
||||||
@ -55,7 +55,7 @@ interface CreateSettingsArgs {
|
|||||||
type: SettingsPaths
|
type: SettingsPaths
|
||||||
send: Function
|
send: Function
|
||||||
context: ContextFrom<typeof settingsMachine>
|
context: ContextFrom<typeof settingsMachine>
|
||||||
actor: InterpreterFrom<typeof settingsMachine>
|
actor: Actor<typeof settingsMachine>
|
||||||
isProjectAvailable: boolean
|
isProjectAvailable: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,7 +132,7 @@ export function createSettingsCommand({
|
|||||||
if (data !== undefined && data !== null) {
|
if (data !== undefined && data !== null) {
|
||||||
send({ type: `set.${type}`, data })
|
send({ type: `set.${type}`, data })
|
||||||
} else {
|
} else {
|
||||||
send(type)
|
send({ type })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
import { CustomIconName } from 'components/CustomIcon'
|
import { CustomIconName } from 'components/CustomIcon'
|
||||||
import { AllMachines } from 'hooks/useStateMachineCommands'
|
import { AllMachines } from 'hooks/useStateMachineCommands'
|
||||||
import {
|
import { Actor, AnyStateMachine, ContextFrom, EventFrom } from 'xstate'
|
||||||
AnyStateMachine,
|
|
||||||
ContextFrom,
|
|
||||||
EventFrom,
|
|
||||||
InterpreterFrom,
|
|
||||||
} from 'xstate'
|
|
||||||
import { Selection } from './selections'
|
import { Selection } from './selections'
|
||||||
import { Identifier, Expr, VariableDeclaration } from 'lang/wasm'
|
import { Identifier, Expr, VariableDeclaration } from 'lang/wasm'
|
||||||
import { commandBarMachine } from 'machines/commandBarMachine'
|
import { commandBarMachine } from 'machines/commandBarMachine'
|
||||||
@ -186,7 +181,7 @@ export type CommandArgument<
|
|||||||
machineContext?: ContextFrom<T>
|
machineContext?: ContextFrom<T>
|
||||||
) => boolean)
|
) => boolean)
|
||||||
skip?: boolean
|
skip?: boolean
|
||||||
machineActor: InterpreterFrom<T>
|
machineActor: Actor<T>
|
||||||
/** For showing a summary display of the current value, such as in
|
/** For showing a summary display of the current value, such as in
|
||||||
* the command bar's header
|
* the command bar's header
|
||||||
*/
|
*/
|
||||||
|
@ -2,7 +2,7 @@ import {
|
|||||||
AnyStateMachine,
|
AnyStateMachine,
|
||||||
ContextFrom,
|
ContextFrom,
|
||||||
EventFrom,
|
EventFrom,
|
||||||
InterpreterFrom,
|
Actor,
|
||||||
StateFrom,
|
StateFrom,
|
||||||
} from 'xstate'
|
} from 'xstate'
|
||||||
import { isDesktop } from './isDesktop'
|
import { isDesktop } from './isDesktop'
|
||||||
@ -23,7 +23,7 @@ interface CreateMachineCommandProps<
|
|||||||
groupId: T['id']
|
groupId: T['id']
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
send: Function
|
send: Function
|
||||||
actor: InterpreterFrom<T>
|
actor: Actor<T>
|
||||||
commandBarConfig?: StateMachineCommandSetConfig<T, S>
|
commandBarConfig?: StateMachineCommandSetConfig<T, S>
|
||||||
onCancel?: () => void
|
onCancel?: () => void
|
||||||
}
|
}
|
||||||
@ -90,9 +90,9 @@ export function createMachineCommand<
|
|||||||
needsReview: commandConfig.needsReview || false,
|
needsReview: commandConfig.needsReview || false,
|
||||||
onSubmit: (data?: S[typeof type]) => {
|
onSubmit: (data?: S[typeof type]) => {
|
||||||
if (data !== undefined && data !== null) {
|
if (data !== undefined && data !== null) {
|
||||||
send(type, { data })
|
send({ type, data })
|
||||||
} else {
|
} else {
|
||||||
send(type)
|
send({ type })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -124,7 +124,7 @@ function buildCommandArguments<
|
|||||||
>(
|
>(
|
||||||
state: StateFrom<T>,
|
state: StateFrom<T>,
|
||||||
args: CommandConfig<T, CommandName, S>['args'],
|
args: CommandConfig<T, CommandName, S>['args'],
|
||||||
machineActor: InterpreterFrom<T>
|
machineActor: Actor<T>
|
||||||
): NonNullable<Command<T, CommandName, S>['args']> {
|
): NonNullable<Command<T, CommandName, S>['args']> {
|
||||||
const newArgs = {} as NonNullable<Command<T, CommandName, S>['args']>
|
const newArgs = {} as NonNullable<Command<T, CommandName, S>['args']>
|
||||||
|
|
||||||
@ -143,7 +143,7 @@ export function buildCommandArgument<
|
|||||||
>(
|
>(
|
||||||
arg: CommandArgumentConfig<O, T>,
|
arg: CommandArgumentConfig<O, T>,
|
||||||
context: ContextFrom<T>,
|
context: ContextFrom<T>,
|
||||||
machineActor: InterpreterFrom<T>
|
machineActor: Actor<T>
|
||||||
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
|
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
|
||||||
const baseCommandArgument = {
|
const baseCommandArgument = {
|
||||||
description: arg.description,
|
description: arg.description,
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { isDesktop } from './isDesktop'
|
import { isDesktop } from './isDesktop'
|
||||||
import { components } from './machine-api'
|
import { components } from './machine-api'
|
||||||
|
import { reportRejection } from './trap'
|
||||||
|
import { toSync } from './utils'
|
||||||
|
|
||||||
export type MachinesListing = Array<
|
export type MachinesListing = Array<
|
||||||
components['schemas']['MachineInfoResponse']
|
components['schemas']['MachineInfoResponse']
|
||||||
@ -17,7 +19,7 @@ export class MachineManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateMachines()
|
this.updateMachines().catch(reportRejection)
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
@ -31,11 +33,14 @@ export class MachineManager {
|
|||||||
let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined
|
let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined
|
||||||
const timeoutLoop = () => {
|
const timeoutLoop = () => {
|
||||||
clearTimeout(timeoutId)
|
clearTimeout(timeoutId)
|
||||||
timeoutId = setTimeout(async () => {
|
timeoutId = setTimeout(
|
||||||
await this.updateMachineApiIp()
|
toSync(async () => {
|
||||||
await this.updateMachines()
|
await this.updateMachineApiIp()
|
||||||
timeoutLoop()
|
await this.updateMachines()
|
||||||
}, 10000)
|
timeoutLoop()
|
||||||
|
}, reportRejection),
|
||||||
|
10000
|
||||||
|
)
|
||||||
}
|
}
|
||||||
timeoutLoop()
|
timeoutLoop()
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import { MouseEventHandler } from 'react'
|
import { MouseEventHandler } from 'react'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
|
import { reportRejection } from './trap'
|
||||||
|
|
||||||
export const openExternalBrowserIfDesktop = (to?: string) =>
|
export const openExternalBrowserIfDesktop = (to?: string) =>
|
||||||
function (e) {
|
function (e) {
|
||||||
if (isDesktop()) {
|
if (isDesktop()) {
|
||||||
// Ignoring because currentTarget could be a few different things
|
// Ignoring because currentTarget could be a few different things
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.electron.openExternal(to || e.currentTarget?.href)
|
window.electron
|
||||||
|
.openExternal(to || e.currentTarget?.href)
|
||||||
|
.catch(reportRejection)
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
return false
|
return false
|
||||||
|
@ -16,6 +16,8 @@ import { isDesktop } from 'lib/isDesktop'
|
|||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
import { CustomIcon } from 'components/CustomIcon'
|
import { CustomIcon } from 'components/CustomIcon'
|
||||||
import Tooltip from 'components/Tooltip'
|
import Tooltip from 'components/Tooltip'
|
||||||
|
import { toSync } from 'lib/utils'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A setting that can be set at the user or project level
|
* A setting that can be set at the user or project level
|
||||||
@ -206,7 +208,7 @@ export function createSettings() {
|
|||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={toSync(async () => {
|
||||||
// In desktop end-to-end tests we can't control the file picker,
|
// In desktop end-to-end tests we can't control the file picker,
|
||||||
// so we seed the new directory value in the element's dataset
|
// so we seed the new directory value in the element's dataset
|
||||||
const inputRefVal = inputRef.current?.dataset.testValue
|
const inputRefVal = inputRef.current?.dataset.testValue
|
||||||
@ -225,7 +227,7 @@ export function createSettings() {
|
|||||||
if (newPath.canceled) return
|
if (newPath.canceled) return
|
||||||
updateValue(newPath.filePaths[0])
|
updateValue(newPath.filePaths[0])
|
||||||
}
|
}
|
||||||
}}
|
}, reportRejection)}
|
||||||
className="p-0 m-0 border-none hover:bg-primary/10 focus:bg-primary/10 dark:hover:bg-primary/20 dark:focus::bg-primary/20"
|
className="p-0 m-0 border-none hover:bg-primary/10 focus:bg-primary/10 dark:hover:bg-primary/20 dark:focus::bg-primary/20"
|
||||||
data-testid="project-directory-button"
|
data-testid="project-directory-button"
|
||||||
>
|
>
|
||||||
|
@ -7,7 +7,8 @@ import { EngineCommand } from 'lang/std/artifactGraph'
|
|||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||||
import { err } from 'lib/trap'
|
import { err, reportRejection } from 'lib/trap'
|
||||||
|
import { toSync } from './utils'
|
||||||
|
|
||||||
type WebSocketResponse = Models['WebSocketResponse_type']
|
type WebSocketResponse = Models['WebSocketResponse_type']
|
||||||
|
|
||||||
@ -85,6 +86,7 @@ export async function enginelessExecutor(
|
|||||||
setIsStreamReady: () => {},
|
setIsStreamReady: () => {},
|
||||||
setMediaStream: () => {},
|
setMediaStream: () => {},
|
||||||
}) as any as EngineCommandManager
|
}) as any as EngineCommandManager
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
mockEngineCommandManager.startNewSession()
|
mockEngineCommandManager.startNewSession()
|
||||||
const programMemory = await _executor(ast, pm, mockEngineCommandManager, true)
|
const programMemory = await _executor(ast, pm, mockEngineCommandManager, true)
|
||||||
await mockEngineCommandManager.waitForAllCommands()
|
await mockEngineCommandManager.waitForAllCommands()
|
||||||
@ -112,7 +114,8 @@ export async function executor(
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
engineCommandManager.addEventListener(
|
engineCommandManager.addEventListener(
|
||||||
EngineCommandManagerEvents.SceneReady,
|
EngineCommandManagerEvents.SceneReady,
|
||||||
async () => {
|
toSync(async () => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
engineCommandManager.startNewSession()
|
engineCommandManager.startNewSession()
|
||||||
const programMemory = await _executor(
|
const programMemory = await _executor(
|
||||||
ast,
|
ast,
|
||||||
@ -121,8 +124,8 @@ export async function executor(
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
await engineCommandManager.waitForAllCommands()
|
await engineCommandManager.waitForAllCommands()
|
||||||
Promise.resolve(programMemory)
|
resolve(programMemory)
|
||||||
}
|
}, reportRejection)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
import { VITE_KC_API_BASE_URL } from 'env'
|
import { VITE_KC_API_BASE_URL } from 'env'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { FILE_EXT } from './constants'
|
import { FILE_EXT } from './constants'
|
||||||
import { ContextFrom, EventData, EventFrom } from 'xstate'
|
import { ContextFrom, EventFrom } from 'xstate'
|
||||||
import { fileMachine } from 'machines/fileMachine'
|
import { fileMachine } from 'machines/fileMachine'
|
||||||
import { NavigateFunction } from 'react-router-dom'
|
import { NavigateFunction } from 'react-router-dom'
|
||||||
import crossPlatformFetch from './crossPlatformFetch'
|
import crossPlatformFetch from './crossPlatformFetch'
|
||||||
@ -14,6 +14,8 @@ import { isDesktop } from 'lib/isDesktop'
|
|||||||
import { Themes } from './theme'
|
import { Themes } from './theme'
|
||||||
import { commandBarMachine } from 'machines/commandBarMachine'
|
import { commandBarMachine } from 'machines/commandBarMachine'
|
||||||
import { getNextFileName } from './desktopFS'
|
import { getNextFileName } from './desktopFS'
|
||||||
|
import { reportRejection } from './trap'
|
||||||
|
import { toSync } from './utils'
|
||||||
|
|
||||||
export async function submitTextToCadPrompt(
|
export async function submitTextToCadPrompt(
|
||||||
prompt: string,
|
prompt: string,
|
||||||
@ -63,12 +65,12 @@ interface TextToKclProps {
|
|||||||
trimmedPrompt: string
|
trimmedPrompt: string
|
||||||
fileMachineSend: (
|
fileMachineSend: (
|
||||||
type: EventFrom<typeof fileMachine>,
|
type: EventFrom<typeof fileMachine>,
|
||||||
data?: EventData
|
data?: unknown
|
||||||
) => unknown
|
) => unknown
|
||||||
navigate: NavigateFunction
|
navigate: NavigateFunction
|
||||||
commandBarSend: (
|
commandBarSend: (
|
||||||
type: EventFrom<typeof commandBarMachine>,
|
type: EventFrom<typeof commandBarMachine>,
|
||||||
data?: EventData
|
data?: unknown
|
||||||
) => unknown
|
) => unknown
|
||||||
context: ContextFrom<typeof fileMachine>
|
context: ContextFrom<typeof fileMachine>
|
||||||
token?: string
|
token?: string
|
||||||
@ -128,37 +130,42 @@ export async function submitAndAwaitTextToKcl({
|
|||||||
// Check the status of the text-to-cad API job
|
// Check the status of the text-to-cad API job
|
||||||
// until it is completed
|
// until it is completed
|
||||||
const textToCadComplete = new Promise<Models['TextToCad_type']>(
|
const textToCadComplete = new Promise<Models['TextToCad_type']>(
|
||||||
async (resolve, reject) => {
|
(resolve, reject) => {
|
||||||
const value = await textToCadQueued
|
;(async () => {
|
||||||
if (value instanceof Error) {
|
const value = await textToCadQueued
|
||||||
reject(value)
|
if (value instanceof Error) {
|
||||||
}
|
reject(value)
|
||||||
|
|
||||||
const MAX_CHECK_TIMEOUT = 3 * 60_000
|
|
||||||
const CHECK_INTERVAL = 3000
|
|
||||||
|
|
||||||
let timeElapsed = 0
|
|
||||||
const interval = setInterval(async () => {
|
|
||||||
timeElapsed += CHECK_INTERVAL
|
|
||||||
if (timeElapsed >= MAX_CHECK_TIMEOUT) {
|
|
||||||
clearInterval(interval)
|
|
||||||
reject(new Error('Text-to-CAD API timed out'))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const check = await getTextToCadResult(value.id, token)
|
const MAX_CHECK_TIMEOUT = 3 * 60_000
|
||||||
if (check instanceof Error) {
|
const CHECK_INTERVAL = 3000
|
||||||
clearInterval(interval)
|
|
||||||
reject(check)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (check instanceof Error || check.status === 'failed') {
|
let timeElapsed = 0
|
||||||
clearInterval(interval)
|
const interval = setInterval(
|
||||||
reject(check)
|
toSync(async () => {
|
||||||
} else if (check.status === 'completed') {
|
timeElapsed += CHECK_INTERVAL
|
||||||
clearInterval(interval)
|
if (timeElapsed >= MAX_CHECK_TIMEOUT) {
|
||||||
resolve(check)
|
clearInterval(interval)
|
||||||
}
|
reject(new Error('Text-to-CAD API timed out'))
|
||||||
}, CHECK_INTERVAL)
|
}
|
||||||
|
|
||||||
|
const check = await getTextToCadResult(value.id, token)
|
||||||
|
if (check instanceof Error) {
|
||||||
|
clearInterval(interval)
|
||||||
|
reject(check)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (check instanceof Error || check.status === 'failed') {
|
||||||
|
clearInterval(interval)
|
||||||
|
reject(check)
|
||||||
|
} else if (check.status === 'completed') {
|
||||||
|
clearInterval(interval)
|
||||||
|
resolve(check)
|
||||||
|
}
|
||||||
|
}, reportRejection),
|
||||||
|
CHECK_INTERVAL
|
||||||
|
)
|
||||||
|
})().catch(reportRejection)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ type ToolbarMode = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ToolbarItemCallbackProps {
|
export interface ToolbarItemCallbackProps {
|
||||||
modelingStateMatches: StateFrom<typeof modelingMachine>['matches']
|
modelingState: StateFrom<typeof modelingMachine>
|
||||||
modelingSend: (event: EventFrom<typeof modelingMachine>) => void
|
modelingSend: (event: EventFrom<typeof modelingMachine>) => void
|
||||||
commandBarSend: (event: EventFrom<typeof commandBarMachine>) => void
|
commandBarSend: (event: EventFrom<typeof commandBarMachine>) => void
|
||||||
sketchPathId: string | false
|
sketchPathId: string | false
|
||||||
@ -85,7 +85,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Extrude', groupId: 'modeling' },
|
data: { name: 'Extrude', groupId: 'modeling' },
|
||||||
}),
|
}),
|
||||||
disabled: (state) => !state.can('Extrude'),
|
disabled: (state) => !state.can({ type: 'Extrude' }),
|
||||||
icon: 'extrude',
|
icon: 'extrude',
|
||||||
status: 'available',
|
status: 'available',
|
||||||
title: 'Extrude',
|
title: 'Extrude',
|
||||||
@ -156,7 +156,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
}),
|
}),
|
||||||
icon: 'fillet3d',
|
icon: 'fillet3d',
|
||||||
status: DEV ? 'available' : 'kcl-only',
|
status: DEV ? 'available' : 'kcl-only',
|
||||||
disabled: (state) => !state.can('Fillet'),
|
disabled: (state) => !state.can({ type: 'Fillet' }),
|
||||||
title: 'Fillet',
|
title: 'Fillet',
|
||||||
hotkey: 'F',
|
hotkey: 'F',
|
||||||
description: 'Round the edges of a 3D solid.',
|
description: 'Round the edges of a 3D solid.',
|
||||||
@ -273,7 +273,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
}),
|
}),
|
||||||
disableHotkey: (state) =>
|
disableHotkey: (state) =>
|
||||||
!(
|
!(
|
||||||
state.matches('Sketch.SketchIdle') ||
|
state.matches({ Sketch: 'SketchIdle' }) ||
|
||||||
state.matches('Sketch no face')
|
state.matches('Sketch no face')
|
||||||
),
|
),
|
||||||
icon: 'arrowLeft',
|
icon: 'arrowLeft',
|
||||||
@ -287,35 +287,41 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
'break',
|
'break',
|
||||||
{
|
{
|
||||||
id: 'line',
|
id: 'line',
|
||||||
onClick: ({ modelingStateMatches: matches, modelingSend }) =>
|
onClick: ({ modelingState, modelingSend }) =>
|
||||||
modelingSend({
|
modelingSend({
|
||||||
type: 'change tool',
|
type: 'change tool',
|
||||||
data: {
|
data: {
|
||||||
tool: !matches('Sketch.Line tool') ? 'line' : 'none',
|
tool: !modelingState.matches({ Sketch: 'Line tool' })
|
||||||
|
? 'line'
|
||||||
|
: 'none',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
icon: 'line',
|
icon: 'line',
|
||||||
status: 'available',
|
status: 'available',
|
||||||
disabled: (state) =>
|
disabled: (state) =>
|
||||||
state.matches('Sketch no face') ||
|
state.matches('Sketch no face') ||
|
||||||
state.matches('Sketch.Rectangle tool.Awaiting second corner') ||
|
state.matches({
|
||||||
state.matches('Sketch.Circle tool.Awaiting Radius') ||
|
Sketch: { 'Rectangle tool': 'Awaiting second corner' },
|
||||||
|
}) ||
|
||||||
|
state.matches({
|
||||||
|
Sketch: { 'Circle tool': 'Awaiting Radius' },
|
||||||
|
}) ||
|
||||||
isClosedSketch(state.context),
|
isClosedSketch(state.context),
|
||||||
title: 'Line',
|
title: 'Line',
|
||||||
hotkey: (state) =>
|
hotkey: (state) =>
|
||||||
state.matches('Sketch.Line tool') ? ['Esc', 'L'] : 'L',
|
state.matches({ Sketch: 'Line tool' }) ? ['Esc', 'L'] : 'L',
|
||||||
description: 'Start drawing straight lines',
|
description: 'Start drawing straight lines',
|
||||||
links: [],
|
links: [],
|
||||||
isActive: (state) => state.matches('Sketch.Line tool'),
|
isActive: (state) => state.matches({ Sketch: 'Line tool' }),
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
id: 'tangential-arc',
|
id: 'tangential-arc',
|
||||||
onClick: ({ modelingStateMatches, modelingSend }) =>
|
onClick: ({ modelingState, modelingSend }) =>
|
||||||
modelingSend({
|
modelingSend({
|
||||||
type: 'change tool',
|
type: 'change tool',
|
||||||
data: {
|
data: {
|
||||||
tool: !modelingStateMatches('Sketch.Tangential arc to')
|
tool: !modelingState.matches({ Sketch: 'Tangential arc to' })
|
||||||
? 'tangentialArc'
|
? 'tangentialArc'
|
||||||
: 'none',
|
: 'none',
|
||||||
},
|
},
|
||||||
@ -324,16 +330,20 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
status: 'available',
|
status: 'available',
|
||||||
disabled: (state) =>
|
disabled: (state) =>
|
||||||
(!isEditingExistingSketch(state.context) &&
|
(!isEditingExistingSketch(state.context) &&
|
||||||
!state.matches('Sketch.Tangential arc to')) ||
|
!state.matches({ Sketch: 'Tangential arc to' })) ||
|
||||||
state.matches('Sketch.Rectangle tool.Awaiting second corner') ||
|
state.matches({
|
||||||
state.matches('Sketch.Circle tool.Awaiting Radius') ||
|
Sketch: { 'Rectangle tool': 'Awaiting second corner' },
|
||||||
|
}) ||
|
||||||
|
state.matches({
|
||||||
|
Sketch: { 'Circle tool': 'Awaiting Radius' },
|
||||||
|
}) ||
|
||||||
isClosedSketch(state.context),
|
isClosedSketch(state.context),
|
||||||
title: 'Tangential Arc',
|
title: 'Tangential Arc',
|
||||||
hotkey: (state) =>
|
hotkey: (state) =>
|
||||||
state.matches('Sketch.Tangential arc to') ? ['Esc', 'A'] : 'A',
|
state.matches({ Sketch: 'Tangential arc to' }) ? ['Esc', 'A'] : 'A',
|
||||||
description: 'Start drawing an arc tangent to the current segment',
|
description: 'Start drawing an arc tangent to the current segment',
|
||||||
links: [],
|
links: [],
|
||||||
isActive: (state) => state.matches('Sketch.Tangential arc to'),
|
isActive: (state) => state.matches({ Sketch: 'Tangential arc to' }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'three-point-arc',
|
id: 'three-point-arc',
|
||||||
@ -405,11 +415,11 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
id: 'corner-rectangle',
|
id: 'corner-rectangle',
|
||||||
onClick: ({ modelingStateMatches, modelingSend }) =>
|
onClick: ({ modelingState, modelingSend }) =>
|
||||||
modelingSend({
|
modelingSend({
|
||||||
type: 'change tool',
|
type: 'change tool',
|
||||||
data: {
|
data: {
|
||||||
tool: !modelingStateMatches('Sketch.Rectangle tool')
|
tool: !modelingState.matches({ Sketch: 'Rectangle tool' })
|
||||||
? 'rectangle'
|
? 'rectangle'
|
||||||
: 'none',
|
: 'none',
|
||||||
},
|
},
|
||||||
@ -418,13 +428,13 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
status: 'available',
|
status: 'available',
|
||||||
disabled: (state) =>
|
disabled: (state) =>
|
||||||
!canRectangleOrCircleTool(state.context) &&
|
!canRectangleOrCircleTool(state.context) &&
|
||||||
!state.matches('Sketch.Rectangle tool'),
|
!state.matches({ Sketch: 'Rectangle tool' }),
|
||||||
title: 'Corner rectangle',
|
title: 'Corner rectangle',
|
||||||
hotkey: (state) =>
|
hotkey: (state) =>
|
||||||
state.matches('Sketch.Rectangle tool') ? ['Esc', 'R'] : 'R',
|
state.matches({ Sketch: 'Rectangle tool' }) ? ['Esc', 'R'] : 'R',
|
||||||
description: 'Start drawing a rectangle',
|
description: 'Start drawing a rectangle',
|
||||||
links: [],
|
links: [],
|
||||||
isActive: (state) => state.matches('Sketch.Rectangle tool'),
|
isActive: (state) => state.matches({ Sketch: 'Rectangle tool' }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'center-rectangle',
|
id: 'center-rectangle',
|
||||||
@ -473,9 +483,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
id: 'constraint-length',
|
id: 'constraint-length',
|
||||||
disabled: (state) =>
|
disabled: (state) =>
|
||||||
!(
|
!(
|
||||||
state.matches('Sketch.SketchIdle') &&
|
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||||
state.nextEvents.includes('Constrain length') &&
|
state.can({ type: 'Constrain length' })
|
||||||
state.can('Constrain length')
|
|
||||||
),
|
),
|
||||||
onClick: ({ modelingSend }) =>
|
onClick: ({ modelingSend }) =>
|
||||||
modelingSend({ type: 'Constrain length' }),
|
modelingSend({ type: 'Constrain length' }),
|
||||||
@ -490,9 +499,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
id: 'constraint-angle',
|
id: 'constraint-angle',
|
||||||
disabled: (state) =>
|
disabled: (state) =>
|
||||||
!(
|
!(
|
||||||
state.matches('Sketch.SketchIdle') &&
|
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||||
state.nextEvents.includes('Constrain angle') &&
|
state.can({ type: 'Constrain angle' })
|
||||||
state.can('Constrain angle')
|
|
||||||
),
|
),
|
||||||
onClick: ({ modelingSend }) =>
|
onClick: ({ modelingSend }) =>
|
||||||
modelingSend({ type: 'Constrain angle' }),
|
modelingSend({ type: 'Constrain angle' }),
|
||||||
@ -506,9 +514,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
id: 'constraint-vertical',
|
id: 'constraint-vertical',
|
||||||
disabled: (state) =>
|
disabled: (state) =>
|
||||||
!(
|
!(
|
||||||
state.matches('Sketch.SketchIdle') &&
|
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||||
state.nextEvents.includes('Make segment vertical') &&
|
state.can({ type: 'Make segment vertical' })
|
||||||
state.can('Make segment vertical')
|
|
||||||
),
|
),
|
||||||
onClick: ({ modelingSend }) =>
|
onClick: ({ modelingSend }) =>
|
||||||
modelingSend({ type: 'Make segment vertical' }),
|
modelingSend({ type: 'Make segment vertical' }),
|
||||||
@ -523,9 +530,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
id: 'constraint-horizontal',
|
id: 'constraint-horizontal',
|
||||||
disabled: (state) =>
|
disabled: (state) =>
|
||||||
!(
|
!(
|
||||||
state.matches('Sketch.SketchIdle') &&
|
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||||
state.nextEvents.includes('Make segment horizontal') &&
|
state.can({ type: 'Make segment horizontal' })
|
||||||
state.can('Make segment horizontal')
|
|
||||||
),
|
),
|
||||||
onClick: ({ modelingSend }) =>
|
onClick: ({ modelingSend }) =>
|
||||||
modelingSend({ type: 'Make segment horizontal' }),
|
modelingSend({ type: 'Make segment horizontal' }),
|
||||||
@ -540,9 +546,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
id: 'constraint-parallel',
|
id: 'constraint-parallel',
|
||||||
disabled: (state) =>
|
disabled: (state) =>
|
||||||
!(
|
!(
|
||||||
state.matches('Sketch.SketchIdle') &&
|
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||||
state.nextEvents.includes('Constrain parallel') &&
|
state.can({ type: 'Constrain parallel' })
|
||||||
state.can('Constrain parallel')
|
|
||||||
),
|
),
|
||||||
onClick: ({ modelingSend }) =>
|
onClick: ({ modelingSend }) =>
|
||||||
modelingSend({ type: 'Constrain parallel' }),
|
modelingSend({ type: 'Constrain parallel' }),
|
||||||
@ -556,9 +561,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
id: 'constraint-equal-length',
|
id: 'constraint-equal-length',
|
||||||
disabled: (state) =>
|
disabled: (state) =>
|
||||||
!(
|
!(
|
||||||
state.matches('Sketch.SketchIdle') &&
|
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||||
state.nextEvents.includes('Constrain equal length') &&
|
state.can({ type: 'Constrain equal length' })
|
||||||
state.can('Constrain equal length')
|
|
||||||
),
|
),
|
||||||
onClick: ({ modelingSend }) =>
|
onClick: ({ modelingSend }) =>
|
||||||
modelingSend({ type: 'Constrain equal length' }),
|
modelingSend({ type: 'Constrain equal length' }),
|
||||||
@ -572,9 +576,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
id: 'constraint-horizontal-distance',
|
id: 'constraint-horizontal-distance',
|
||||||
disabled: (state) =>
|
disabled: (state) =>
|
||||||
!(
|
!(
|
||||||
state.matches('Sketch.SketchIdle') &&
|
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||||
state.nextEvents.includes('Constrain horizontal distance') &&
|
state.can({ type: 'Constrain horizontal distance' })
|
||||||
state.can('Constrain horizontal distance')
|
|
||||||
),
|
),
|
||||||
onClick: ({ modelingSend }) =>
|
onClick: ({ modelingSend }) =>
|
||||||
modelingSend({ type: 'Constrain horizontal distance' }),
|
modelingSend({ type: 'Constrain horizontal distance' }),
|
||||||
@ -588,9 +591,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
id: 'constraint-vertical-distance',
|
id: 'constraint-vertical-distance',
|
||||||
disabled: (state) =>
|
disabled: (state) =>
|
||||||
!(
|
!(
|
||||||
state.matches('Sketch.SketchIdle') &&
|
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||||
state.nextEvents.includes('Constrain vertical distance') &&
|
state.can({ type: 'Constrain vertical distance' })
|
||||||
state.can('Constrain vertical distance')
|
|
||||||
),
|
),
|
||||||
onClick: ({ modelingSend }) =>
|
onClick: ({ modelingSend }) =>
|
||||||
modelingSend({ type: 'Constrain vertical distance' }),
|
modelingSend({ type: 'Constrain vertical distance' }),
|
||||||
@ -604,9 +606,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
id: 'constraint-absolute-x',
|
id: 'constraint-absolute-x',
|
||||||
disabled: (state) =>
|
disabled: (state) =>
|
||||||
!(
|
!(
|
||||||
state.matches('Sketch.SketchIdle') &&
|
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||||
state.nextEvents.includes('Constrain ABS X') &&
|
state.can({ type: 'Constrain ABS X' })
|
||||||
state.can('Constrain ABS X')
|
|
||||||
),
|
),
|
||||||
onClick: ({ modelingSend }) =>
|
onClick: ({ modelingSend }) =>
|
||||||
modelingSend({ type: 'Constrain ABS X' }),
|
modelingSend({ type: 'Constrain ABS X' }),
|
||||||
@ -620,9 +621,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
id: 'constraint-absolute-y',
|
id: 'constraint-absolute-y',
|
||||||
disabled: (state) =>
|
disabled: (state) =>
|
||||||
!(
|
!(
|
||||||
state.matches('Sketch.SketchIdle') &&
|
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||||
state.nextEvents.includes('Constrain ABS Y') &&
|
state.can({ type: 'Constrain ABS Y' })
|
||||||
state.can('Constrain ABS Y')
|
|
||||||
),
|
),
|
||||||
onClick: ({ modelingSend }) =>
|
onClick: ({ modelingSend }) =>
|
||||||
modelingSend({ type: 'Constrain ABS Y' }),
|
modelingSend({ type: 'Constrain ABS Y' }),
|
||||||
@ -636,9 +636,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
id: 'constraint-perpendicular-distance',
|
id: 'constraint-perpendicular-distance',
|
||||||
disabled: (state) =>
|
disabled: (state) =>
|
||||||
!(
|
!(
|
||||||
state.matches('Sketch.SketchIdle') &&
|
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||||
state.nextEvents.includes('Constrain perpendicular distance') &&
|
state.can({ type: 'Constrain perpendicular distance' })
|
||||||
state.can('Constrain perpendicular distance')
|
|
||||||
),
|
),
|
||||||
onClick: ({ modelingSend }) =>
|
onClick: ({ modelingSend }) =>
|
||||||
modelingSend({ type: 'Constrain perpendicular distance' }),
|
modelingSend({ type: 'Constrain perpendicular distance' }),
|
||||||
@ -653,9 +652,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
id: 'constraint-align-horizontal',
|
id: 'constraint-align-horizontal',
|
||||||
disabled: (state) =>
|
disabled: (state) =>
|
||||||
!(
|
!(
|
||||||
state.matches('Sketch.SketchIdle') &&
|
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||||
state.nextEvents.includes('Constrain horizontally align') &&
|
state.can({ type: 'Constrain horizontally align' })
|
||||||
state.can('Constrain horizontally align')
|
|
||||||
),
|
),
|
||||||
onClick: ({ modelingSend }) =>
|
onClick: ({ modelingSend }) =>
|
||||||
modelingSend({ type: 'Constrain horizontally align' }),
|
modelingSend({ type: 'Constrain horizontally align' }),
|
||||||
@ -669,9 +667,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
id: 'constraint-align-vertical',
|
id: 'constraint-align-vertical',
|
||||||
disabled: (state) =>
|
disabled: (state) =>
|
||||||
!(
|
!(
|
||||||
state.matches('Sketch.SketchIdle') &&
|
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||||
state.nextEvents.includes('Constrain vertically align') &&
|
state.can({ type: 'Constrain vertically align' })
|
||||||
state.can('Constrain vertically align')
|
|
||||||
),
|
),
|
||||||
onClick: ({ modelingSend }) =>
|
onClick: ({ modelingSend }) =>
|
||||||
modelingSend({ type: 'Constrain vertically align' }),
|
modelingSend({ type: 'Constrain vertically align' }),
|
||||||
@ -685,9 +682,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
id: 'snap-to-x',
|
id: 'snap-to-x',
|
||||||
disabled: (state) =>
|
disabled: (state) =>
|
||||||
!(
|
!(
|
||||||
state.matches('Sketch.SketchIdle') &&
|
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||||
state.nextEvents.includes('Constrain snap to X') &&
|
state.can({ type: 'Constrain snap to X' })
|
||||||
state.can('Constrain snap to X')
|
|
||||||
),
|
),
|
||||||
onClick: ({ modelingSend }) =>
|
onClick: ({ modelingSend }) =>
|
||||||
modelingSend({ type: 'Constrain snap to X' }),
|
modelingSend({ type: 'Constrain snap to X' }),
|
||||||
@ -701,9 +697,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
id: 'snap-to-y',
|
id: 'snap-to-y',
|
||||||
disabled: (state) =>
|
disabled: (state) =>
|
||||||
!(
|
!(
|
||||||
state.matches('Sketch.SketchIdle') &&
|
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||||
state.nextEvents.includes('Constrain snap to Y') &&
|
state.can({ type: 'Constrain snap to Y' })
|
||||||
state.can('Constrain snap to Y')
|
|
||||||
),
|
),
|
||||||
onClick: ({ modelingSend }) =>
|
onClick: ({ modelingSend }) =>
|
||||||
modelingSend({ type: 'Constrain snap to Y' }),
|
modelingSend({ type: 'Constrain snap to Y' }),
|
||||||
@ -717,9 +712,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
id: 'constraint-remove',
|
id: 'constraint-remove',
|
||||||
disabled: (state) =>
|
disabled: (state) =>
|
||||||
!(
|
!(
|
||||||
state.matches('Sketch.SketchIdle') &&
|
state.matches({ Sketch: 'SketchIdle' }) &&
|
||||||
state.nextEvents.includes('Constrain remove constraints') &&
|
state.can({ type: 'Constrain remove constraints' })
|
||||||
state.can('Constrain remove constraints')
|
|
||||||
),
|
),
|
||||||
onClick: ({ modelingSend }) =>
|
onClick: ({ modelingSend }) =>
|
||||||
modelingSend({ type: 'Constrain remove constraints' }),
|
modelingSend({ type: 'Constrain remove constraints' }),
|
||||||
|
@ -2,13 +2,22 @@ import toast from 'react-hot-toast'
|
|||||||
|
|
||||||
type ExcludeErr<T> = Exclude<T, Error>
|
type ExcludeErr<T> = Exclude<T, Error>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is intentionally *not* exported due to misuse. We'd like to add a lint.
|
||||||
|
*/
|
||||||
|
function isErr<T>(value: ExcludeErr<T> | Error): value is Error {
|
||||||
|
return value instanceof Error
|
||||||
|
}
|
||||||
|
|
||||||
// Used to bubble errors up
|
// Used to bubble errors up
|
||||||
export function err<T>(value: ExcludeErr<T> | Error): value is Error {
|
export function err<T>(value: ExcludeErr<T> | Error): value is Error {
|
||||||
if (!(value instanceof Error)) {
|
if (!isErr(value)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Remove this once we have a lint to prevent misuse of this function.
|
||||||
console.error(value)
|
console.error(value)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,7 +30,7 @@ export function cleanErrs<T>(
|
|||||||
const argsWOutErr: Array<ExcludeErr<T>> = []
|
const argsWOutErr: Array<ExcludeErr<T>> = []
|
||||||
const argsWErr: Array<Error> = []
|
const argsWErr: Array<Error> = []
|
||||||
for (const v of value) {
|
for (const v of value) {
|
||||||
if (err(v)) {
|
if (isErr(v)) {
|
||||||
argsWErr.push(v)
|
argsWErr.push(v)
|
||||||
} else {
|
} else {
|
||||||
argsWOutErr.push(v)
|
argsWOutErr.push(v)
|
||||||
@ -30,9 +39,28 @@ export function cleanErrs<T>(
|
|||||||
return [argsWOutErr.length !== value.length, argsWOutErr, argsWErr]
|
return [argsWOutErr.length !== value.length, argsWOutErr, argsWErr]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function report(
|
||||||
|
message: string,
|
||||||
|
{ showToast }: { showToast: boolean } = { showToast: false }
|
||||||
|
) {
|
||||||
|
console.error(message)
|
||||||
|
if (showToast) {
|
||||||
|
toast.error(message, { id: 'error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to report errors to user at a certain point in execution
|
* Report a promise rejection. The type of reason is `any` so that it matches
|
||||||
* @returns boolean
|
* Promise.prototype.catch.
|
||||||
|
*/
|
||||||
|
export function reportRejection(reason: any) {
|
||||||
|
report((reason ?? 'Unknown promise rejection').toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report an error to the user. Trapping is the opposite of propagating an
|
||||||
|
* error. We should propagate errors in low-level functions and trap at the top
|
||||||
|
* level.
|
||||||
*/
|
*/
|
||||||
export function trap<T>(
|
export function trap<T>(
|
||||||
value: ExcludeErr<T> | Error,
|
value: ExcludeErr<T> | Error,
|
||||||
@ -41,7 +69,7 @@ export function trap<T>(
|
|||||||
suppress?: boolean
|
suppress?: boolean
|
||||||
}
|
}
|
||||||
): value is Error {
|
): value is Error {
|
||||||
if (!err(value)) {
|
if (!isErr(value)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,3 +98,18 @@ export function isEnumMember<T extends Record<string, unknown>>(
|
|||||||
export type DeepPartial<T> = {
|
export type DeepPartial<T> = {
|
||||||
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
|
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace a function's return type with another type.
|
||||||
|
*/
|
||||||
|
export type WithReturnType<F extends (...args: any[]) => any, NewReturn> = (
|
||||||
|
...args: Parameters<F>
|
||||||
|
) => NewReturn
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that a function type is async, preserving its parameter types.
|
||||||
|
*/
|
||||||
|
export type AsyncFn<F extends (...args: any[]) => any> = WithReturnType<
|
||||||
|
F,
|
||||||
|
Promise<unknown>
|
||||||
|
>
|
||||||
|
@ -2,6 +2,8 @@ import { SourceRange } from '../lang/wasm'
|
|||||||
|
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
import { isDesktop } from './isDesktop'
|
import { isDesktop } from './isDesktop'
|
||||||
|
import { AnyMachineSnapshot } from 'xstate'
|
||||||
|
import { AsyncFn } from './types'
|
||||||
|
|
||||||
export const uuidv4 = v4
|
export const uuidv4 = v4
|
||||||
|
|
||||||
@ -105,6 +107,28 @@ export function deferExecution<T>(func: (args: T) => any, wait: number) {
|
|||||||
return deferred
|
return deferred
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap an async function so that it can be called in a sync context, catching
|
||||||
|
* rejections.
|
||||||
|
*
|
||||||
|
* It's common to want to run an async function in a sync context, like an event
|
||||||
|
* handler or callback. But we want to catch errors.
|
||||||
|
*
|
||||||
|
* Note: The returned function doesn't block. This isn't magic.
|
||||||
|
*
|
||||||
|
* @param onReject This callback type is from Promise.prototype.catch.
|
||||||
|
*/
|
||||||
|
export function toSync<F extends AsyncFn<F>>(
|
||||||
|
fn: F,
|
||||||
|
onReject: (
|
||||||
|
reason: any
|
||||||
|
) => void | PromiseLike<void | null | undefined> | null | undefined
|
||||||
|
): (...args: Parameters<F>) => void {
|
||||||
|
return (...args: Parameters<F>) => {
|
||||||
|
fn(...args).catch(onReject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function getNormalisedCoordinates({
|
export function getNormalisedCoordinates({
|
||||||
clientX,
|
clientX,
|
||||||
clientY,
|
clientY,
|
||||||
@ -208,3 +232,7 @@ export function isReducedMotion(): boolean {
|
|||||||
export function XOR(bool1: boolean, bool2: boolean): boolean {
|
export function XOR(bool1: boolean, bool2: boolean): boolean {
|
||||||
return (bool1 || bool2) && !(bool1 && bool2)
|
return (bool1 || bool2) && !(bool1 && bool2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getActorNextEvents(snapshot: AnyMachineSnapshot) {
|
||||||
|
return [...new Set([...snapshot._nodes.flatMap((sn) => sn.ownEvents)])]
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createMachine, assign } from 'xstate'
|
import { assign, setup, fromPromise } from 'xstate'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import withBaseURL from '../lib/withBaseURL'
|
import withBaseURL from '../lib/withBaseURL'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
@ -55,79 +55,93 @@ const persistedToken =
|
|||||||
localStorage?.getItem(TOKEN_PERSIST_KEY) ||
|
localStorage?.getItem(TOKEN_PERSIST_KEY) ||
|
||||||
''
|
''
|
||||||
|
|
||||||
export const authMachine = createMachine<UserContext, Events>(
|
export const authMachine = setup({
|
||||||
{
|
types: {} as {
|
||||||
/** @xstate-layout N4IgpgJg5mDOIC5QEECuAXAFgOgMabFwGsBJAMwBkB7KGCEgOwGIIqGxsBLBgNyqI75CRALQAbGnRHcA2gAYAuolAAHKrE7pObZSAAeiAIwBmAEzYA7ABYAbAFZTcgBzGbN44adWANCACeiKbGdthypk4AnBFyVs6uQXYAvom+aFh4BMTk1LSQjExgAE6FVIXYKmIAhuhkpQC2GcLikpDSDPJKSCBqGlo6XQYIrk7YETYWctYRxmMWFk6+AUPj2I5OdjZyrnZOFmbJqRg4Ern0zDkABFQYHbo9mtoMuoOGFhHYxlZOhvbOsUGGRaIL4WbBONzWQxWYwWOx2H4HEBpY4tCAAeQwTEuskUd3UD36oEGIlMNlCuzk8Js0TcVisgP8iG2lmcGysb0mW3ByRSIAYVAgcF0yLxvUez0QIms5ImVJpNjpDKWxmw9PGdLh4Te00+iORjSylFRjFFBKeA0QThGQWcexMwWhniBCGiqrepisUVMdlszgieqO2BOdBNXXufXNRKMHtGVuphlJkXs4Wdriso2CCasdgipOidID6WDkAx6FNEYlCAT5jmcjrckMdj2b3GzpsjbBMVMWezDbGPMSQA */
|
context: UserContext
|
||||||
id: 'Auth',
|
events:
|
||||||
initial: 'checkIfLoggedIn',
|
| Events
|
||||||
states: {
|
| {
|
||||||
checkIfLoggedIn: {
|
type: 'xstate.done.actor.check-logged-in'
|
||||||
id: 'check-if-logged-in',
|
output: {
|
||||||
invoke: {
|
user: Models['User_type']
|
||||||
src: 'getUser',
|
token: string
|
||||||
id: 'check-logged-in',
|
}
|
||||||
onDone: [
|
}
|
||||||
{
|
},
|
||||||
target: 'loggedIn',
|
actions: {
|
||||||
actions: assign((context, event) => ({
|
goToIndexPage: () => {},
|
||||||
user: event.data.user,
|
goToSignInPage: () => {},
|
||||||
token: event.data.token || context.token,
|
},
|
||||||
})),
|
actors: {
|
||||||
},
|
getUser: fromPromise(({ input }: { input: { token?: string } }) =>
|
||||||
],
|
getUser(input)
|
||||||
onError: [
|
),
|
||||||
{
|
},
|
||||||
target: 'loggedOut',
|
}).createMachine({
|
||||||
actions: assign({
|
/** @xstate-layout N4IgpgJg5mDOIC5QEECuAXAFgOgMabFwGsBJAMwBkB7KGCEgOwGIIqGxsBLBgNyqI75CRALQAbGnRHcA2gAYAuolAAHKrE7pObZSAAeiAIwBmAEzYA7ABYAbAFZTcgBzGbN44adWANCACeiKbGdthypk4AnBFyVs6uQXYAvom+aFh4BMTk1LSQjExgAE6FVIXYKmIAhuhkpQC2GcLikpDSDPJKSCBqGlo6XQYIrk7YETYWctYRxmMWFk6+AUPj2I5OdjZyrnZOFmbJqRg4Ern0zDkABFQYHbo9mtoMuoOGFhHYxlZOhvbOsUGGRaIL4WbBONzWQxWYwWOx2H4HEBpY4tCAAeQwTEuskUd3UD36oEGIlMNlCuzk8Js0TcVisgP8iG2lmcGysb0mW3ByRSIAYVAgcF0yLxvUez0QIms5ImVJpNjpDKWxmw9PGdLh4Te00+iORjSylFRjFFBKeA0QThGQWcexMwWhniBCGiqrepisUVMdlszgieqO2BOdBNXXufXNRKMHtGVuphlJkXs4Wdriso2CCasdgipOidID6WDkAx6FNEYlCAT5jmcjrckMdj2b3GzpsjbBMVMWezDbGPMSQA */
|
||||||
user: () => undefined,
|
id: 'Auth',
|
||||||
}),
|
initial: 'checkIfLoggedIn',
|
||||||
},
|
context: {
|
||||||
],
|
token: persistedToken,
|
||||||
},
|
},
|
||||||
},
|
states: {
|
||||||
loggedIn: {
|
checkIfLoggedIn: {
|
||||||
entry: ['goToIndexPage'],
|
id: 'check-if-logged-in',
|
||||||
on: {
|
invoke: {
|
||||||
'Log out': {
|
src: 'getUser',
|
||||||
target: 'loggedOut',
|
input: ({ context }) => ({ token: context.token }),
|
||||||
actions: () => {
|
id: 'check-logged-in',
|
||||||
if (isDesktop()) writeTokenFile('')
|
onDone: [
|
||||||
},
|
{
|
||||||
|
target: 'loggedIn',
|
||||||
|
actions: assign(({ context, event }) => ({
|
||||||
|
user: event.output.user,
|
||||||
|
token: event.output.token || context.token,
|
||||||
|
})),
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
onError: [
|
||||||
loggedOut: {
|
{
|
||||||
entry: ['goToSignInPage'],
|
target: 'loggedOut',
|
||||||
on: {
|
|
||||||
'Log in': {
|
|
||||||
target: 'checkIfLoggedIn',
|
|
||||||
actions: assign({
|
actions: assign({
|
||||||
token: (_, event) => {
|
user: () => undefined,
|
||||||
const token = event.token || ''
|
|
||||||
return token
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
loggedIn: {
|
||||||
|
entry: ['goToIndexPage'],
|
||||||
|
on: {
|
||||||
|
'Log out': {
|
||||||
|
target: 'loggedOut',
|
||||||
|
actions: () => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
if (isDesktop()) writeTokenFile('')
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
schema: { events: {} as { type: 'Log out' } | { type: 'Log in' } },
|
loggedOut: {
|
||||||
predictableActionArguments: true,
|
entry: ['goToSignInPage'],
|
||||||
preserveActionOrder: true,
|
on: {
|
||||||
context: {
|
'Log in': {
|
||||||
token: persistedToken,
|
target: 'checkIfLoggedIn',
|
||||||
|
actions: assign({
|
||||||
|
token: ({ event }) => {
|
||||||
|
const token = event.token || ''
|
||||||
|
return token
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
schema: { events: {} as { type: 'Log out' } | { type: 'Log in' } },
|
||||||
actions: {},
|
})
|
||||||
services: { getUser },
|
|
||||||
guards: {},
|
|
||||||
delays: {},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
async function getUser(context: UserContext) {
|
async function getUser(input: { token?: string }) {
|
||||||
const token = await getAndSyncStoredToken(context)
|
const token = await getAndSyncStoredToken(input)
|
||||||
const url = withBaseURL('/user')
|
const url = withBaseURL('/user')
|
||||||
const headers: { [key: string]: string } = {
|
const headers: { [key: string]: string } = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -156,7 +170,7 @@ async function getUser(context: UserContext) {
|
|||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.catch((err) => console.error('error from Browser getUser', err))
|
.catch((err) => console.error('error from Browser getUser', err))
|
||||||
: getUserDesktop(context.token ?? '', VITE_KC_API_BASE_URL)
|
: getUserDesktop(input.token ?? '', VITE_KC_API_BASE_URL)
|
||||||
|
|
||||||
const user = await userPromise
|
const user = await userPromise
|
||||||
|
|
||||||
@ -193,17 +207,20 @@ function getCookie(cname: string): string | null {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAndSyncStoredToken(context: UserContext): Promise<string> {
|
async function getAndSyncStoredToken(input: {
|
||||||
|
token?: string
|
||||||
|
}): Promise<string> {
|
||||||
// dev mode
|
// dev mode
|
||||||
if (VITE_KC_DEV_TOKEN) return VITE_KC_DEV_TOKEN
|
if (VITE_KC_DEV_TOKEN) return VITE_KC_DEV_TOKEN
|
||||||
|
|
||||||
const token =
|
const token =
|
||||||
context.token && context.token !== ''
|
input.token && input.token !== ''
|
||||||
? context.token
|
? input.token
|
||||||
: getCookie(COOKIE_NAME) || localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
|
: getCookie(COOKIE_NAME) || localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
|
||||||
if (token) {
|
if (token) {
|
||||||
// has just logged in, update storage
|
// has just logged in, update storage
|
||||||
localStorage.setItem(TOKEN_PERSIST_KEY, token)
|
localStorage.setItem(TOKEN_PERSIST_KEY, token)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
isDesktop() && writeTokenFile(token)
|
isDesktop() && writeTokenFile(token)
|
||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { assign, createMachine } from 'xstate'
|
import { assign, fromPromise, setup } from 'xstate'
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandArgument,
|
CommandArgument,
|
||||||
@ -25,7 +25,7 @@ export type CommandBarMachineEvent =
|
|||||||
data: { command: Command; argDefaultValues?: { [x: string]: unknown } }
|
data: { command: Command; argDefaultValues?: { [x: string]: unknown } }
|
||||||
}
|
}
|
||||||
| { type: 'Deselect command' }
|
| { type: 'Deselect command' }
|
||||||
| { type: 'Submit command'; data: { [x: string]: unknown } }
|
| { type: 'Submit command'; output: { [x: string]: unknown } }
|
||||||
| {
|
| {
|
||||||
type: 'Add argument'
|
type: 'Add argument'
|
||||||
data: { argument: CommandArgumentWithName<unknown> }
|
data: { argument: CommandArgumentWithName<unknown> }
|
||||||
@ -48,12 +48,16 @@ export type CommandBarMachineEvent =
|
|||||||
}
|
}
|
||||||
| { type: 'Submit argument'; data: { [x: string]: unknown } }
|
| { type: 'Submit argument'; data: { [x: string]: unknown } }
|
||||||
| {
|
| {
|
||||||
type: 'done.invoke.validateArguments'
|
type: 'xstate.done.actor.validateSingleArgument'
|
||||||
data: { [x: string]: unknown }
|
output: { [x: string]: unknown }
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'error.platform.validateArguments'
|
type: 'xstate.done.actor.validateArguments'
|
||||||
data: { message: string; arg: CommandArgumentWithName<unknown> }
|
output: { [x: string]: unknown }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'xstate.error.actor.validateArguments'
|
||||||
|
error: { message: string; arg: CommandArgumentWithName<unknown> }
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'Find and select command'
|
type: 'Find and select command'
|
||||||
@ -68,382 +72,199 @@ export type CommandBarMachineEvent =
|
|||||||
data: { [x: string]: CommandArgumentWithName<unknown> }
|
data: { [x: string]: CommandArgumentWithName<unknown> }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const commandBarMachine = createMachine(
|
export const commandBarMachine = setup({
|
||||||
{
|
types: {
|
||||||
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswZJbPltM0U3eXLZAsRFeQcrerUHRbTFvfkmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUilkskqdiUWjWCAcDC0kjUqnElkc6lkoK0JzOT0CnWo1zIAEEIARvn48LA-uwuIC4qAEqIHJjJPJcYtxFoYdJFFjVsZEG5pqCquJxJoGvJxOUCT82sSrj0AEpgdCoABuYC+yog9OYIyZsQmYmhWzMmTqLnU0kyikRGVRbj5lg2SmUam5StpFxIkgAymBXh8HlADQGyKHw58aecGZEzUDWYgchY7CisWoSlpQQ5EVDLJJxKkdIpyypUop-ed2kHCW0Xu9uD1kwDzcCEJCtlUNgWhZZ1OlnaKEBpZJItHVzDZ5xlpPWiZdDc8w22O7JwozosyLUj3KiZZY85YtMUNgx5Ii81JuS5xBtPVCCyuVR0AOJYbgACzAEhI3wUgoAAV3QQZuFgCg-1wGAvjAkgSCgkCSHAyCcG4TtUxZYRED2TQZGSOw80qaQ7ARCc9mmZYSjPPFpDSQUP0DSQf3-QDgNAiCoJggAROBNw+aMkxNf5cMPHJSjPWRdhvZ9LGcF0b0kVQ8ycDRNkvNRWMbdjfwAoCcCjHjMOgyRyQAdywGITPwB42DA7hYzAgAjdAeDQjCoJw-du3TJE6mnWwoT0NIUTUAs71sMtdGsRYCyUtI9OJDijO49DeKw2BJAANSwShOAgX9IzICB+DASQHh1VAAGsqp1Qrit-MBySy8y-LGPCElSeoy2lZJcwcNRfXHQpBWmWQsRsPYdDPZJUu-QyuPssy+Py5qSt4EyyEAkhUCDNhKF-AAzQ70EkJqiu2tqOt88S926w9UhhLlykWXFHXcTJixsd60hHGxNj2Jag01HVODAKzXI8rzE1+R6U38tN8IQJSuR0OZ0gvHQXHGxBPWmUdppUWwFV0MHJAhqGYcpAh1qwrqDx7ZFajUmVpulEbqlqREsfBTQ0jcPM5P5KmaehshNW1PVvOy7Cka7VHeoYDkyjSPQtAvPMqkRQU1BnX1+UydFkQvCWwEhqWAFEIC8xnFd3ZHntZuTDZG4ob1nGxPTUfXrBnS8nDmEpT2ObxTnXVUALeOrgPanycvKyrqpwWqGqurbWsThXjWd5WesQZYtmBkplCceplIncK0SrXYqnccxxcj5s2OQWP4-s3PzJgiqcCqmr6sa7P2x7vi6AcAvJJ7XEy0dUFMWsFxlnKfn+S5CL3E9Jiuapjv3i7qNx+T-bDskY6zourObpz+6cuZgK0Y5Ua0TqEvXDkJR-YnR0s1xjkIMnDln3uuVsHwOxT1NCjIuR5nCSBUExJi1huRqyooUTYpYnzeyUFkKsy4Tg4FQBAOAgg26Nmga7QKohshSBtNYXGDonQumtPYaQfsSjLBHDefepJICUJZtQ4oMx8wXj6jebGt4JwbC2OiVwig9hh2hLpVu0cOhxjbMBBGeABFPwSA3ERRMlhKWCn9WRVQzZQirMo0BAYNzxn4RJGBh5MRbGXsHawGQFBmLrpUasOQorOAjs0OxaUVrGVMvfaCuiVYEV2KWTYg1yxyA0i6ZYaIPSL3lOkS8wSo6hOWpxCJ8te6WRsnZKMjlnIxNgSNIiiTF6vVSdI9JM1taXlxLzTwqiClBnSqtSJScLIFVvjtKANSXquENqoJwtgyZzRFIUQ4MhqgNACdNTQCpLbWyshMnsjpSwjXRJYHecgcmImsLIz0odNAaGqPiHpDYY6HwTlE+ATiqHPwohYewWQshmGhHrCcJz5Bck5toZwGhMheC8EAA */
|
context: {} as CommandBarContext,
|
||||||
predictableActionArguments: true,
|
events: {} as CommandBarMachineEvent,
|
||||||
tsTypes: {} as import('./commandBarMachine.typegen').Typegen0,
|
|
||||||
context: {
|
|
||||||
commands: [],
|
|
||||||
selectedCommand: undefined,
|
|
||||||
currentArgument: undefined,
|
|
||||||
selectionRanges: {
|
|
||||||
otherSelections: [],
|
|
||||||
codeBasedSelections: [],
|
|
||||||
},
|
|
||||||
argumentsToSubmit: {},
|
|
||||||
} as CommandBarContext,
|
|
||||||
id: 'Command Bar',
|
|
||||||
initial: 'Closed',
|
|
||||||
states: {
|
|
||||||
Closed: {
|
|
||||||
on: {
|
|
||||||
Open: {
|
|
||||||
target: 'Selecting command',
|
|
||||||
},
|
|
||||||
|
|
||||||
'Find and select command': {
|
|
||||||
target: 'Command selected',
|
|
||||||
actions: [
|
|
||||||
'Find and select command',
|
|
||||||
'Initialize arguments to submit',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
'Add commands': {
|
|
||||||
target: 'Closed',
|
|
||||||
|
|
||||||
actions: [
|
|
||||||
assign({
|
|
||||||
commands: (context, event) =>
|
|
||||||
[...context.commands, ...event.data.commands].sort(
|
|
||||||
sortCommands
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
|
|
||||||
internal: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
'Remove commands': {
|
|
||||||
target: 'Closed',
|
|
||||||
|
|
||||||
actions: [
|
|
||||||
assign({
|
|
||||||
commands: (context, event) =>
|
|
||||||
context.commands.filter(
|
|
||||||
(c) =>
|
|
||||||
!event.data.commands.some(
|
|
||||||
(c2) => c2.name === c.name && c2.groupId === c.groupId
|
|
||||||
)
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
|
|
||||||
internal: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'Selecting command': {
|
|
||||||
on: {
|
|
||||||
'Select command': {
|
|
||||||
target: 'Command selected',
|
|
||||||
actions: ['Set selected command', 'Initialize arguments to submit'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'Command selected': {
|
|
||||||
always: [
|
|
||||||
{
|
|
||||||
target: 'Closed',
|
|
||||||
cond: 'Command has no arguments',
|
|
||||||
actions: ['Execute command'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: 'Checking Arguments',
|
|
||||||
cond: 'All arguments are skippable',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: 'Gathering arguments',
|
|
||||||
actions: ['Set current argument to first non-skippable'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
'Gathering arguments': {
|
|
||||||
states: {
|
|
||||||
'Awaiting input': {
|
|
||||||
on: {
|
|
||||||
'Submit argument': {
|
|
||||||
target: 'Validating',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
Validating: {
|
|
||||||
invoke: {
|
|
||||||
src: 'Validate argument',
|
|
||||||
id: 'validateArgument',
|
|
||||||
onDone: {
|
|
||||||
target: '#Command Bar.Checking Arguments',
|
|
||||||
actions: [
|
|
||||||
assign({
|
|
||||||
argumentsToSubmit: (context, event) => {
|
|
||||||
const [argName, argData] = Object.entries(event.data)[0]
|
|
||||||
const { currentArgument } = context
|
|
||||||
if (!currentArgument) return {}
|
|
||||||
return {
|
|
||||||
...context.argumentsToSubmit,
|
|
||||||
[argName]: argData,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
onError: [
|
|
||||||
{
|
|
||||||
target: 'Awaiting input',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
initial: 'Awaiting input',
|
|
||||||
|
|
||||||
on: {
|
|
||||||
'Change current argument': {
|
|
||||||
target: 'Gathering arguments',
|
|
||||||
internal: true,
|
|
||||||
actions: ['Set current argument'],
|
|
||||||
},
|
|
||||||
|
|
||||||
'Deselect command': {
|
|
||||||
target: 'Selecting command',
|
|
||||||
actions: [
|
|
||||||
assign({
|
|
||||||
selectedCommand: (_c, _e) => undefined,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
Review: {
|
|
||||||
entry: ['Clear current argument'],
|
|
||||||
on: {
|
|
||||||
'Submit command': {
|
|
||||||
target: 'Closed',
|
|
||||||
actions: ['Execute command'],
|
|
||||||
},
|
|
||||||
|
|
||||||
'Add argument': {
|
|
||||||
target: 'Gathering arguments',
|
|
||||||
actions: ['Set current argument'],
|
|
||||||
},
|
|
||||||
|
|
||||||
'Remove argument': {
|
|
||||||
target: 'Review',
|
|
||||||
actions: ['Remove argument'],
|
|
||||||
},
|
|
||||||
|
|
||||||
'Edit argument': {
|
|
||||||
target: 'Gathering arguments',
|
|
||||||
actions: ['Set current argument'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'Checking Arguments': {
|
|
||||||
invoke: {
|
|
||||||
src: 'Validate all arguments',
|
|
||||||
id: 'validateArguments',
|
|
||||||
onDone: [
|
|
||||||
{
|
|
||||||
target: 'Review',
|
|
||||||
cond: 'Command needs review',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: 'Closed',
|
|
||||||
actions: 'Execute command',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
onError: [
|
|
||||||
{
|
|
||||||
target: 'Gathering arguments',
|
|
||||||
actions: ['Set current argument to first non-skippable'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
on: {
|
|
||||||
Close: {
|
|
||||||
target: '.Closed',
|
|
||||||
},
|
|
||||||
|
|
||||||
Clear: {
|
|
||||||
target: '#Command Bar',
|
|
||||||
internal: true,
|
|
||||||
actions: ['Clear argument data'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
schema: {
|
|
||||||
events: {} as CommandBarMachineEvent,
|
|
||||||
},
|
|
||||||
preserveActionOrder: true,
|
|
||||||
},
|
},
|
||||||
{
|
actions: {
|
||||||
actions: {
|
enqueueValidArgsToSubmit: assign({
|
||||||
'Execute command': (context, event) => {
|
argumentsToSubmit: ({ context, event }) => {
|
||||||
const { selectedCommand } = context
|
if (event.type !== 'xstate.done.actor.validateSingleArgument') return {}
|
||||||
if (!selectedCommand) return
|
const [argName, argData] = Object.entries(event.output)[0]
|
||||||
if (
|
const { currentArgument } = context
|
||||||
(selectedCommand?.args && event.type === 'Submit command') ||
|
if (!currentArgument) return {}
|
||||||
event.type === 'done.invoke.validateArguments'
|
return {
|
||||||
) {
|
...context.argumentsToSubmit,
|
||||||
const resolvedArgs = {} as { [x: string]: unknown }
|
[argName]: argData,
|
||||||
for (const [argName, argValue] of Object.entries(
|
|
||||||
getCommandArgumentKclValuesOnly(event.data)
|
|
||||||
)) {
|
|
||||||
resolvedArgs[argName] =
|
|
||||||
typeof argValue === 'function' ? argValue(context) : argValue
|
|
||||||
}
|
|
||||||
selectedCommand?.onSubmit(resolvedArgs)
|
|
||||||
} else {
|
|
||||||
selectedCommand?.onSubmit()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'Set current argument to first non-skippable': assign({
|
}),
|
||||||
currentArgument: (context, event) => {
|
'Execute command': ({ context, event }) => {
|
||||||
const { selectedCommand } = context
|
const { selectedCommand } = context
|
||||||
if (!(selectedCommand && selectedCommand.args)) return undefined
|
if (!selectedCommand) return
|
||||||
const rejectedArg = 'data' in event && event.data.arg
|
if (
|
||||||
|
(selectedCommand?.args && event.type === 'Submit command') ||
|
||||||
|
event.type === 'xstate.done.actor.validateArguments'
|
||||||
|
) {
|
||||||
|
const resolvedArgs = {} as { [x: string]: unknown }
|
||||||
|
for (const [argName, argValue] of Object.entries(
|
||||||
|
getCommandArgumentKclValuesOnly(event.output)
|
||||||
|
)) {
|
||||||
|
resolvedArgs[argName] =
|
||||||
|
typeof argValue === 'function' ? argValue(context) : argValue
|
||||||
|
}
|
||||||
|
selectedCommand?.onSubmit(resolvedArgs)
|
||||||
|
} else {
|
||||||
|
selectedCommand?.onSubmit()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Set current argument to first non-skippable': assign({
|
||||||
|
currentArgument: ({ context, event }) => {
|
||||||
|
const { selectedCommand } = context
|
||||||
|
if (!(selectedCommand && selectedCommand.args)) return undefined
|
||||||
|
const rejectedArg =
|
||||||
|
'data' in event && 'arg' in event.data && event.data.arg
|
||||||
|
|
||||||
// Find the first argument that is not to be skipped:
|
// Find the first argument that is not to be skipped:
|
||||||
// that is, the first argument that is not already in the argumentsToSubmit
|
// that is, the first argument that is not already in the argumentsToSubmit
|
||||||
// or that is not undefined, or that is not marked as "skippable".
|
// or that is not undefined, or that is not marked as "skippable".
|
||||||
// TODO validate the type of the existing arguments
|
// TODO validate the type of the existing arguments
|
||||||
let argIndex = 0
|
let argIndex = 0
|
||||||
|
|
||||||
while (argIndex < Object.keys(selectedCommand.args).length) {
|
while (argIndex < Object.keys(selectedCommand.args).length) {
|
||||||
const [argName, argConfig] = Object.entries(selectedCommand.args)[
|
const [argName, argConfig] = Object.entries(selectedCommand.args)[
|
||||||
argIndex
|
argIndex
|
||||||
]
|
]
|
||||||
const argIsRequired =
|
const argIsRequired =
|
||||||
typeof argConfig.required === 'function'
|
typeof argConfig.required === 'function'
|
||||||
? argConfig.required(context)
|
? argConfig.required(context)
|
||||||
: argConfig.required
|
: argConfig.required
|
||||||
const mustNotSkipArg =
|
const mustNotSkipArg =
|
||||||
argIsRequired &&
|
argIsRequired &&
|
||||||
(!context.argumentsToSubmit.hasOwnProperty(argName) ||
|
(!context.argumentsToSubmit.hasOwnProperty(argName) ||
|
||||||
context.argumentsToSubmit[argName] === undefined ||
|
context.argumentsToSubmit[argName] === undefined ||
|
||||||
(rejectedArg && rejectedArg.name === argName))
|
(rejectedArg &&
|
||||||
|
typeof rejectedArg === 'object' &&
|
||||||
|
'name' in rejectedArg &&
|
||||||
|
rejectedArg.name === argName))
|
||||||
|
|
||||||
if (
|
if (
|
||||||
mustNotSkipArg === true ||
|
mustNotSkipArg === true ||
|
||||||
argIndex + 1 === Object.keys(selectedCommand.args).length
|
argIndex + 1 === Object.keys(selectedCommand.args).length
|
||||||
) {
|
) {
|
||||||
// If we have reached the end of the arguments and none are skippable,
|
// If we have reached the end of the arguments and none are skippable,
|
||||||
// return the last argument.
|
// return the last argument.
|
||||||
return {
|
return {
|
||||||
...selectedCommand.args[argName],
|
...selectedCommand.args[argName],
|
||||||
name: argName,
|
name: argName,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
argIndex++
|
|
||||||
}
|
}
|
||||||
|
argIndex++
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: use an XState service to continue onto review step
|
// TODO: use an XState service to continue onto review step
|
||||||
// if all arguments are skippable and contain values.
|
// if all arguments are skippable and contain values.
|
||||||
return undefined
|
return undefined
|
||||||
},
|
|
||||||
}),
|
|
||||||
'Clear current argument': assign({
|
|
||||||
currentArgument: undefined,
|
|
||||||
}),
|
|
||||||
'Remove argument': assign({
|
|
||||||
argumentsToSubmit: (context, event) => {
|
|
||||||
if (event.type !== 'Remove argument') return context.argumentsToSubmit
|
|
||||||
const argToRemove = Object.values(event.data)[0]
|
|
||||||
// Extract all but the argument to remove and return it
|
|
||||||
const { [argToRemove.name]: _, ...rest } = context.argumentsToSubmit
|
|
||||||
return rest
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
'Set current argument': assign({
|
|
||||||
currentArgument: (context, event) => {
|
|
||||||
switch (event.type) {
|
|
||||||
case 'Edit argument':
|
|
||||||
return event.data.arg
|
|
||||||
case 'Change current argument':
|
|
||||||
return Object.values(event.data)[0]
|
|
||||||
default:
|
|
||||||
return context.currentArgument
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
'Clear argument data': assign({
|
|
||||||
selectedCommand: undefined,
|
|
||||||
currentArgument: undefined,
|
|
||||||
argumentsToSubmit: {},
|
|
||||||
}),
|
|
||||||
'Set selected command': assign({
|
|
||||||
selectedCommand: (c, e) =>
|
|
||||||
e.type === 'Select command' ? e.data.command : c.selectedCommand,
|
|
||||||
}),
|
|
||||||
'Find and select command': assign({
|
|
||||||
selectedCommand: (c, e) => {
|
|
||||||
if (e.type !== 'Find and select command') return c.selectedCommand
|
|
||||||
const found = c.commands.find(
|
|
||||||
(cmd) => cmd.name === e.data.name && cmd.groupId === e.data.groupId
|
|
||||||
)
|
|
||||||
|
|
||||||
return !!found ? found : c.selectedCommand
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
'Initialize arguments to submit': assign({
|
|
||||||
argumentsToSubmit: (c, e) => {
|
|
||||||
const command =
|
|
||||||
'command' in e.data ? e.data.command : c.selectedCommand
|
|
||||||
if (!command?.args) return {}
|
|
||||||
const args: { [x: string]: unknown } = {}
|
|
||||||
for (const [argName, arg] of Object.entries(command.args)) {
|
|
||||||
args[argName] =
|
|
||||||
e.data.argDefaultValues && argName in e.data.argDefaultValues
|
|
||||||
? e.data.argDefaultValues[argName]
|
|
||||||
: arg.skip && 'defaultValue' in arg
|
|
||||||
? arg.defaultValue
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
return args
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
guards: {
|
|
||||||
'Command needs review': (context, _) =>
|
|
||||||
context.selectedCommand?.needsReview || false,
|
|
||||||
},
|
|
||||||
services: {
|
|
||||||
'Validate argument': (context, event) => {
|
|
||||||
if (event.type !== 'Submit argument') return Promise.reject()
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// TODO: figure out if we should validate argument data here or in the form itself,
|
|
||||||
// and if we should support people configuring a argument's validation function
|
|
||||||
|
|
||||||
resolve(event.data)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
'Validate all arguments': (context, _) => {
|
}),
|
||||||
|
'Clear current argument': assign({
|
||||||
|
currentArgument: undefined,
|
||||||
|
}),
|
||||||
|
'Remove argument': assign({
|
||||||
|
argumentsToSubmit: ({ context, event }) => {
|
||||||
|
if (event.type !== 'Remove argument') return context.argumentsToSubmit
|
||||||
|
const argToRemove = Object.values(event.data)[0]
|
||||||
|
// Extract all but the argument to remove and return it
|
||||||
|
const { [argToRemove.name]: _, ...rest } = context.argumentsToSubmit
|
||||||
|
return rest
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'Set current argument': assign({
|
||||||
|
currentArgument: ({ context, event }) => {
|
||||||
|
switch (event.type) {
|
||||||
|
case 'Edit argument':
|
||||||
|
return event.data.arg
|
||||||
|
case 'Change current argument':
|
||||||
|
return Object.values(event.data)[0]
|
||||||
|
default:
|
||||||
|
return context.currentArgument
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'Clear argument data': assign({
|
||||||
|
selectedCommand: undefined,
|
||||||
|
currentArgument: undefined,
|
||||||
|
argumentsToSubmit: {},
|
||||||
|
}),
|
||||||
|
'Set selected command': assign({
|
||||||
|
selectedCommand: ({ context, event }) =>
|
||||||
|
event.type === 'Select command'
|
||||||
|
? event.data.command
|
||||||
|
: context.selectedCommand,
|
||||||
|
}),
|
||||||
|
'Find and select command': assign({
|
||||||
|
selectedCommand: ({ context, event }) => {
|
||||||
|
if (event.type !== 'Find and select command')
|
||||||
|
return context.selectedCommand
|
||||||
|
const found = context.commands.find(
|
||||||
|
(cmd) =>
|
||||||
|
cmd.name === event.data.name && cmd.groupId === event.data.groupId
|
||||||
|
)
|
||||||
|
|
||||||
|
return !!found ? found : context.selectedCommand
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'Initialize arguments to submit': assign({
|
||||||
|
argumentsToSubmit: ({ context, event }) => {
|
||||||
|
if (
|
||||||
|
event.type !== 'Select command' &&
|
||||||
|
event.type !== 'Find and select command'
|
||||||
|
)
|
||||||
|
return {}
|
||||||
|
const command =
|
||||||
|
'data' in event && 'command' in event.data
|
||||||
|
? event.data.command
|
||||||
|
: context.selectedCommand
|
||||||
|
if (!command?.args) return {}
|
||||||
|
const args: { [x: string]: unknown } = {}
|
||||||
|
for (const [argName, arg] of Object.entries(command.args)) {
|
||||||
|
args[argName] =
|
||||||
|
event.data.argDefaultValues &&
|
||||||
|
argName in event.data.argDefaultValues
|
||||||
|
? event.data.argDefaultValues[argName]
|
||||||
|
: arg.skip && 'defaultValue' in arg
|
||||||
|
? arg.defaultValue
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
guards: {
|
||||||
|
'Command needs review': ({ context }) =>
|
||||||
|
context.selectedCommand?.needsReview || false,
|
||||||
|
'Command has no arguments': () => false,
|
||||||
|
'All arguments are skippable': () => false,
|
||||||
|
},
|
||||||
|
actors: {
|
||||||
|
'Validate argument': fromPromise(({ input }) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// TODO: figure out if we should validate argument data here or in the form itself,
|
||||||
|
// and if we should support people configuring a argument's validation function
|
||||||
|
|
||||||
|
resolve(input)
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
'Validate all arguments': fromPromise(
|
||||||
|
({ input }: { input: CommandBarContext }) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
for (const [argName, argConfig] of Object.entries(
|
for (const [argName, argConfig] of Object.entries(
|
||||||
context.selectedCommand!.args!
|
input.selectedCommand!.args!
|
||||||
)) {
|
)) {
|
||||||
let arg = context.argumentsToSubmit[argName]
|
let arg = input.argumentsToSubmit[argName]
|
||||||
let argValue = typeof arg === 'function' ? arg(context) : arg
|
let argValue = typeof arg === 'function' ? arg(input) : arg
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const isRequired =
|
const isRequired =
|
||||||
typeof argConfig.required === 'function'
|
typeof argConfig.required === 'function'
|
||||||
? argConfig.required(context)
|
? argConfig.required(input)
|
||||||
: argConfig.required
|
: argConfig.required
|
||||||
|
|
||||||
const resolvedDefaultValue =
|
const resolvedDefaultValue =
|
||||||
'defaultValue' in argConfig
|
'defaultValue' in argConfig
|
||||||
? typeof argConfig.defaultValue === 'function'
|
? typeof argConfig.defaultValue === 'function'
|
||||||
? argConfig.defaultValue(context)
|
? argConfig.defaultValue(input)
|
||||||
: argConfig.defaultValue
|
: argConfig.defaultValue
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
@ -461,7 +282,7 @@ export const commandBarMachine = createMachine(
|
|||||||
!(
|
!(
|
||||||
typeof argConfig.options === 'function'
|
typeof argConfig.options === 'function'
|
||||||
? argConfig.options(
|
? argConfig.options(
|
||||||
context,
|
input,
|
||||||
argConfig.machineActor.getSnapshot().context
|
argConfig.machineActor.getSnapshot().context
|
||||||
)
|
)
|
||||||
: argConfig.options
|
: argConfig.options
|
||||||
@ -502,13 +323,214 @@ export const commandBarMachine = createMachine(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolve(context.argumentsToSubmit)
|
return resolve(input.argumentsToSubmit)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswZJbPltM0U3eXLZAsRFeQcrerUHRbTFvfkmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUilkskqdiUWjWCAcDC0kjUqnElkc6lkoK0JzOT0CnWo1zIAEEIARvn48LA-uwuIC4qAEqIHJjJPJcYtxFoYdJFFjVsZEG5pqCquJxJoGvJxOUCT82sSrj0AEpgdCoABuYC+yog9OYIyZsQmYmhWzMmTqLnU0kyikRGVRbj5lg2SmUam5StpFxIkgAymBXh8HlADQGyKHw58aecGZEzUDWYgchY7CisWoSlpQQ5EVDLJJxKkdIpyypUop-ed2kHCW0Xu9uD1kwDzcCEJCtlUNgWhZZ1OlnaKEBpZJItHVzDZ5xlpPWiZdDc8w22O7JwozosyLUj3KiZZY85YtMUNgx5Ii81JuS5xBtPVCCyuVR0AOJYbgACzAEhI3wUgoAAV3QQZuFgCg-1wGAvjAkgSCgkCSHAyCcG4TtUxZYRED2TQZGSOw80qaQ7ARCc9mmZYSjPPFpDSQUP0DSQf3-QDgNAiCoJggAROBNw+aMkxNf5cMPHJSjPWRdhvZ9LGcF0b0kVQ8ycDRNkvNRWMbdjfwAoCcCjHjMOgyRyQAdywGITPwB42DA7hYzAgAjdAeDQjCoJw-du3TJE6mnWwoT0NIUTUAs71sMtdGsRYCyUtI9OJDijO49DeKw2BJAANSwShOAgX9IzICB+DASQHh1VAAGsqp1Qrit-MBySy8y-LGPCElSeoy2lZJcwcNRfXHQpBWmWQsRsPYdDPZJUu-QyuPssy+Py5qSt4EyyEAkhUCDNhKF-AAzQ70EkJqiu2tqOt88S926w9UhhLlykWXFHXcTJixsd60hHGxNj2Jag01HVODAKzXI8rzE1+R6U38tN8IQJSuR0OZ0gvHQXHGxBPWmUdppUWwFV0MHJAhqGYcpAh1qwrqDx7ZFajUmVpulEbqlqREsfBTQ0jcPM5P5KmaehshNW1PVvOy7Cka7VHeoYDkyjSPQtAvPMqkRQU1BnX1+UydFkQvCWwEhqWAFEIC8xnFd3ZHntZuTDZG4ob1nGxPTUfXrBnS8nDmEpT2ObxTnXVUALeOrgPanycvKyrqpwWqGqurbWsThXjWd5WesQZYtmBkplCceplIncK0SrXYqnccxxcj5s2OQWP4-s3PzJgiqcCqmr6sa7P2x7vi6AcAvJJ7XEy0dUFMWsFxlnKfn+S5CL3E9Jiuapjv3i7qNx+T-bDskY6zourObpz+6cuZgK0Y5Ua0TqEvXDkJR-YnR0s1xjkIMnDln3uuVsHwOxT1NCjIuR5nCSBUExJi1huRqyooUTYpYnzeyUFkKsy4Tg4FQBAOAgg26Nmga7QKohshSBtNYXGDonQumtPYaQfsSjLBHDefepJICUJZtQ4oMx8wXj6jebGt4JwbC2OiVwig9hh2hLpVu0cOhxjbMBBGeABFPwSA3ERRMlhKWCn9WRVQzZQirMo0BAYNzxn4RJGBh5MRbGXsHawGQFBmLrpUasOQorOAjs0OxaUVrGVMvfaCuiVYEV2KWTYg1yxyA0i6ZYaIPSL3lOkS8wSo6hOWpxCJ8te6WRsnZKMjlnIxNgSNIiiTF6vVSdI9JM1taXlxLzTwqiClBnSqtSJScLIFVvjtKANSXquENqoJwtgyZzRFIUQ4MhqgNACdNTQCpLbWyshMnsjpSwjXRJYHecgcmImsLIz0odNAaGqPiHpDYY6HwTlE+ATiqHPwohYewWQshmGhHrCcJz5Bck5toZwGhMheC8EAA */
|
||||||
|
context: {
|
||||||
|
commands: [],
|
||||||
|
selectedCommand: undefined,
|
||||||
|
currentArgument: undefined,
|
||||||
|
selectionRanges: {
|
||||||
|
otherSelections: [],
|
||||||
|
codeBasedSelections: [],
|
||||||
|
},
|
||||||
|
argumentsToSubmit: {},
|
||||||
|
},
|
||||||
|
id: 'Command Bar',
|
||||||
|
initial: 'Closed',
|
||||||
|
states: {
|
||||||
|
Closed: {
|
||||||
|
on: {
|
||||||
|
Open: {
|
||||||
|
target: 'Selecting command',
|
||||||
|
},
|
||||||
|
|
||||||
|
'Find and select command': {
|
||||||
|
target: 'Command selected',
|
||||||
|
actions: [
|
||||||
|
'Find and select command',
|
||||||
|
'Initialize arguments to submit',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
'Add commands': {
|
||||||
|
target: 'Closed',
|
||||||
|
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
commands: ({ context, event }) =>
|
||||||
|
[...context.commands, ...event.data.commands].sort(
|
||||||
|
sortCommands
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
reenter: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
'Remove commands': {
|
||||||
|
target: 'Closed',
|
||||||
|
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
commands: ({ context, event }) =>
|
||||||
|
context.commands.filter(
|
||||||
|
(c) =>
|
||||||
|
!event.data.commands.some(
|
||||||
|
(c2) => c2.name === c.name && c2.groupId === c.groupId
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
reenter: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
delays: {},
|
|
||||||
}
|
'Selecting command': {
|
||||||
)
|
on: {
|
||||||
|
'Select command': {
|
||||||
|
target: 'Command selected',
|
||||||
|
actions: ['Set selected command', 'Initialize arguments to submit'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Command selected': {
|
||||||
|
always: [
|
||||||
|
{
|
||||||
|
target: 'Closed',
|
||||||
|
guard: 'Command has no arguments',
|
||||||
|
actions: ['Execute command'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'Checking Arguments',
|
||||||
|
guard: 'All arguments are skippable',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'Gathering arguments',
|
||||||
|
actions: ['Set current argument to first non-skippable'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
'Gathering arguments': {
|
||||||
|
states: {
|
||||||
|
'Awaiting input': {
|
||||||
|
on: {
|
||||||
|
'Submit argument': {
|
||||||
|
target: 'Validating',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Validating: {
|
||||||
|
invoke: {
|
||||||
|
src: 'Validate argument',
|
||||||
|
id: 'validateSingleArgument',
|
||||||
|
input: ({ event }) => {
|
||||||
|
if (event.type !== 'Submit argument') return {}
|
||||||
|
return event.data
|
||||||
|
},
|
||||||
|
onDone: {
|
||||||
|
target: '#Command Bar.Checking Arguments',
|
||||||
|
actions: ['enqueueValidArgsToSubmit'],
|
||||||
|
},
|
||||||
|
onError: [
|
||||||
|
{
|
||||||
|
target: 'Awaiting input',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
initial: 'Awaiting input',
|
||||||
|
|
||||||
|
on: {
|
||||||
|
'Change current argument': {
|
||||||
|
target: 'Gathering arguments',
|
||||||
|
internal: true,
|
||||||
|
actions: ['Set current argument'],
|
||||||
|
},
|
||||||
|
|
||||||
|
'Deselect command': {
|
||||||
|
target: 'Selecting command',
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
selectedCommand: (_c, _e) => undefined,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Review: {
|
||||||
|
entry: ['Clear current argument'],
|
||||||
|
on: {
|
||||||
|
'Submit command': {
|
||||||
|
target: 'Closed',
|
||||||
|
actions: ['Execute command'],
|
||||||
|
},
|
||||||
|
|
||||||
|
'Add argument': {
|
||||||
|
target: 'Gathering arguments',
|
||||||
|
actions: ['Set current argument'],
|
||||||
|
},
|
||||||
|
|
||||||
|
'Remove argument': {
|
||||||
|
target: 'Review',
|
||||||
|
actions: ['Remove argument'],
|
||||||
|
},
|
||||||
|
|
||||||
|
'Edit argument': {
|
||||||
|
target: 'Gathering arguments',
|
||||||
|
actions: ['Set current argument'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Checking Arguments': {
|
||||||
|
invoke: {
|
||||||
|
src: 'Validate all arguments',
|
||||||
|
id: 'validateArguments',
|
||||||
|
input: ({ context }) => context,
|
||||||
|
onDone: [
|
||||||
|
{
|
||||||
|
target: 'Review',
|
||||||
|
guard: 'Command needs review',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'Closed',
|
||||||
|
actions: 'Execute command',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onError: [
|
||||||
|
{
|
||||||
|
target: 'Gathering arguments',
|
||||||
|
actions: ['Set current argument to first non-skippable'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
Close: {
|
||||||
|
target: '.Closed',
|
||||||
|
},
|
||||||
|
|
||||||
|
Clear: {
|
||||||
|
target: '#Command Bar',
|
||||||
|
reenter: false,
|
||||||
|
actions: ['Clear argument data'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
function sortCommands(a: Command, b: Command) {
|
function sortCommands(a: Command, b: Command) {
|
||||||
if (b.groupId === 'auth' && !(a.groupId === 'auth')) return -2
|
if (b.groupId === 'auth' && !(a.groupId === 'auth')) return -2
|
||||||
|
@ -1,233 +1,393 @@
|
|||||||
import { assign, createMachine } from 'xstate'
|
import { assign, fromPromise, setup } from 'xstate'
|
||||||
import { Project, FileEntry } from 'lib/project'
|
import { Project, FileEntry } from 'lib/project'
|
||||||
|
|
||||||
export const fileMachine = createMachine(
|
type FileMachineContext = {
|
||||||
{
|
project: Project
|
||||||
/** @xstate-layout N4IgpgJg5mDOIC5QDECWAbMACAtgQwGMALVAOzAGI9ZZUpSBtABgF1FQAHAe1oBdUupdiAAeiACwAmADQgAnogAcANgCsAOnHiAjOICcAZh3K9TRQHYAvpdlpMuQiXIUASmABmAJzhFmbJCDcfAJCAWIIUrIKCHpq6nraipJJKorahqrWthjY+MRkYOoAEtRYpFxY7jmwFADC3ni82FWYfsJBqPyCwuG6hurKTAYG5mlSyeJRiHqS2prKg6Mj2pKz4lkgdrmOBcWlLXCuYKR4OM05bQEdXaGgvdpD6qPiioqqieJM2gaqUxELT3MQz0eleBlMhmUGy2Dny5D2sEq1TqDSaSNarHaPE6IR6iD6BgGQxGY1Wikm8gkQM0n2UknERm0qmUw2hOVhTkKJURBxq9TAjXOrW0-k42JueIQBKJw1GujJFOiqkkTE0X3M5mUuiYgxmbPseU5CPRhwAImBMGiDpcxcFumF8dptMp1GZRlqNYYdXo-ml1IlzMkHuZVAZJAY1PrtnCuftkQB5DjHE02wLi3EOqX6QmDWWkiZ-HSKf3gph6ZWqcwJIZRjm7bkmmoAZTAvCwsAtYAITQgWAgqG83a4njkqeuGbu+MkLPU7x+iiGszJfwj4ldTvB6UURh0klrht2-MaZCgWDwpF7XCTpBPJooEEEhTIADcuABrQoEVFgAC054gP5XscP7WpiVzpvak5SnO6hJD8RYfJ8ir4kw06zqoTDiMyTA4WGPz7js8JHvwpCnv+WBATepF3mAnieMO6gcOgjTuMOODqF+ApNH+F6AdeIEXGBto4pBoj4u8GjOiMqjiKMqhJFhfyVqqEbJIoCTkmk3wETG6huCcOC3gc96PuoL7voU3gGb+oGimmdq3GJCARuY8RMroEk6MMihKeWrpYepEZMKMSw6Ua+mnEZOQULR9GeIxzG8KxnjsVZpw2YJdnjqJ4QjK59KaoGLKhh6fyBpIsFgtqKjKuCYW7OalpRZgJnwuZH7qBAnbcbZWIOZKeXqAVyhFT8EbaOYK44f6kjlhYG6FVYNibOyB7wo1rbNZQsUMUxLFsZ13UZRiWUQY5uUakNskjdOY2lZSCAqhV24LlhHpMl89Xwm4eD9tRvKtU+pCvh1DQAbyY5nZKMwqZWwxqMorzltoZUrK6YbOlJoazIoX2FD9f2ngDD5tcDFnqGDAmYLADAin1InndMKrqD85jw8ySPvH8pgulqoYWEjc16HjekCoTjYxXRu2JclqVi1TcCQ-1mYwyzcMRhz6lcw9C56Cz4Yatd05ISLxFbYDZlkx1nGCgrSsM5KTJVgMMmjKkEYmAYfwrOkQ30i8WFSF8mTLTCa2FGb-3RTt8V7UlB02z1mX0xKmZMgu8R6C8YahqYwUow9TqBkNxXiLmUgGEyIsRYZUctSTQMg5ZxzpXbdPgcrUEuW57xYUyXkGD5D2Bhog9aKsLyzQywsbOUXXwAEYeEWAKcTk5P7KH8G+ujhuHDDJTJZ0t2QGsvxrlI2q85fiBhlgMZcQq8+iqDJ3OzAML2qCCqxDEkIsNryK+jMpSV1clIck3xB6ViLIWEwmhXiJF0EYIqptUS3nIpRLaQDHajAqvKCwqxEZTxkIXVChJNTqUDCkB4L9q4t1rkTHI2DMyRAeosIawxFxDESMoLCIsNokUYZgZhUF1IDGdK7LyWgX6TULqCIagYcKSHMAya6VdQ6rTPgTLaC9hKpygnSOY8FA7kj0J6WR0QISzn0J8IYN0tIi0TMcLBHcHZp1wf6cB5UiFZxIdEcEhJKyvQ9BqGSqCuIuL0WvXoHj8HeKSL472E0KrBRfrVRGL9cbWEsEAA */
|
selectedDirectory: FileEntry
|
||||||
id: 'File machine',
|
itemsBeingRenamed: (FileEntry | string)[]
|
||||||
|
}
|
||||||
|
|
||||||
initial: 'Reading files',
|
type FileMachineEvents =
|
||||||
|
| { type: 'Open file'; data: { name: string } }
|
||||||
|
| {
|
||||||
|
type: 'Rename file'
|
||||||
|
data: { oldName: string; newName: string; isDir: boolean }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'Create file'
|
||||||
|
data: {
|
||||||
|
name: string
|
||||||
|
makeDir: boolean
|
||||||
|
content?: string
|
||||||
|
silent?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| { type: 'Delete file'; data: FileEntry }
|
||||||
|
| { type: 'Set selected directory'; directory: FileEntry }
|
||||||
|
| { type: 'navigate'; data: { name: string } }
|
||||||
|
| {
|
||||||
|
type: 'xstate.done.actor.read-files'
|
||||||
|
output: Project
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'xstate.done.actor.rename-file'
|
||||||
|
output: {
|
||||||
|
message: string
|
||||||
|
oldPath: string
|
||||||
|
newPath: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'xstate.done.actor.create-and-open-file'
|
||||||
|
output: {
|
||||||
|
message: string
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'xstate.done.actor.create-file'
|
||||||
|
output: {
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'xstate.done.actor.delete-file'
|
||||||
|
output: {
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| { type: 'assign'; data: { [key: string]: any } }
|
||||||
|
| { type: 'Refresh' }
|
||||||
|
|
||||||
context: {
|
export const fileMachine = setup({
|
||||||
project: {} as Project,
|
types: {} as {
|
||||||
selectedDirectory: {} as FileEntry,
|
context: FileMachineContext
|
||||||
itemsBeingRenamed: [] as string[],
|
events: FileMachineEvents
|
||||||
},
|
input: Partial<Pick<FileMachineContext, 'project' | 'selectedDirectory'>>
|
||||||
|
|
||||||
on: {
|
|
||||||
assign: {
|
|
||||||
actions: assign((_, event) => ({
|
|
||||||
...event.data,
|
|
||||||
})),
|
|
||||||
target: '.Reading files',
|
|
||||||
},
|
|
||||||
|
|
||||||
Refresh: '.Reading files',
|
|
||||||
},
|
|
||||||
states: {
|
|
||||||
'Has no files': {
|
|
||||||
on: {
|
|
||||||
'Create file': {
|
|
||||||
target: 'Creating and opening file',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'Has files': {
|
|
||||||
on: {
|
|
||||||
'Rename file': {
|
|
||||||
target: 'Renaming file',
|
|
||||||
},
|
|
||||||
|
|
||||||
'Create file': [
|
|
||||||
{
|
|
||||||
target: 'Creating and opening file',
|
|
||||||
cond: 'Is not silent',
|
|
||||||
},
|
|
||||||
'Creating file',
|
|
||||||
],
|
|
||||||
|
|
||||||
'Delete file': {
|
|
||||||
target: 'Deleting file',
|
|
||||||
},
|
|
||||||
|
|
||||||
'Open file': {
|
|
||||||
target: 'Opening file',
|
|
||||||
},
|
|
||||||
|
|
||||||
'Set selected directory': {
|
|
||||||
target: 'Has files',
|
|
||||||
actions: ['setSelectedDirectory'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'Creating and opening file': {
|
|
||||||
invoke: {
|
|
||||||
id: 'create-and-open-file',
|
|
||||||
src: 'createAndOpenFile',
|
|
||||||
onDone: [
|
|
||||||
{
|
|
||||||
target: 'Reading files',
|
|
||||||
actions: [
|
|
||||||
'createToastSuccess',
|
|
||||||
'addFileToRenamingQueue',
|
|
||||||
'navigateToFile',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
onError: [
|
|
||||||
{
|
|
||||||
target: 'Reading files',
|
|
||||||
actions: ['toastError'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'Renaming file': {
|
|
||||||
invoke: {
|
|
||||||
id: 'rename-file',
|
|
||||||
src: 'renameFile',
|
|
||||||
onDone: [
|
|
||||||
{
|
|
||||||
target: '#File machine.Reading files',
|
|
||||||
actions: ['renameToastSuccess'],
|
|
||||||
cond: 'Name has been changed',
|
|
||||||
},
|
|
||||||
'Reading files',
|
|
||||||
],
|
|
||||||
onError: [
|
|
||||||
{
|
|
||||||
target: '#File machine.Reading files',
|
|
||||||
actions: ['toastError'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
exit: 'removeFileFromRenamingQueue',
|
|
||||||
},
|
|
||||||
|
|
||||||
'Deleting file': {
|
|
||||||
invoke: {
|
|
||||||
id: 'delete-file',
|
|
||||||
src: 'deleteFile',
|
|
||||||
onDone: [
|
|
||||||
{
|
|
||||||
actions: ['toastSuccess'],
|
|
||||||
target: '#File machine.Reading files',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
onError: {
|
|
||||||
actions: ['toastError'],
|
|
||||||
target: '#File machine.Has files',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'Reading files': {
|
|
||||||
invoke: {
|
|
||||||
id: 'read-files',
|
|
||||||
src: 'readFiles',
|
|
||||||
onDone: [
|
|
||||||
{
|
|
||||||
cond: 'Has at least 1 file',
|
|
||||||
target: 'Has files',
|
|
||||||
actions: ['setFiles'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: 'Has no files',
|
|
||||||
actions: ['setFiles'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
onError: [
|
|
||||||
{
|
|
||||||
target: 'Has no files',
|
|
||||||
actions: ['toastError'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'Opening file': {
|
|
||||||
entry: ['navigateToFile'],
|
|
||||||
},
|
|
||||||
|
|
||||||
'Creating file': {
|
|
||||||
invoke: {
|
|
||||||
src: 'createFile',
|
|
||||||
id: 'create-file',
|
|
||||||
onDone: 'Reading files',
|
|
||||||
onError: 'Reading files',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
schema: {
|
|
||||||
events: {} as
|
|
||||||
| { type: 'Open file'; data: { name: string } }
|
|
||||||
| {
|
|
||||||
type: 'Rename file'
|
|
||||||
data: { oldName: string; newName: string; isDir: boolean }
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'Create file'
|
|
||||||
data: {
|
|
||||||
name: string
|
|
||||||
makeDir: boolean
|
|
||||||
content?: string
|
|
||||||
silent?: boolean
|
|
||||||
}
|
|
||||||
}
|
|
||||||
| { type: 'Delete file'; data: FileEntry }
|
|
||||||
| { type: 'Set selected directory'; data: FileEntry }
|
|
||||||
| { type: 'navigate'; data: { name: string } }
|
|
||||||
| {
|
|
||||||
type: 'done.invoke.read-files'
|
|
||||||
data: Project
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'done.invoke.rename-file'
|
|
||||||
data: {
|
|
||||||
message: string
|
|
||||||
oldPath: string
|
|
||||||
newPath: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'done.invoke.create-and-open-file'
|
|
||||||
data: {
|
|
||||||
message: string
|
|
||||||
path: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'done.invoke.create-file'
|
|
||||||
data: {
|
|
||||||
path: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
| { type: 'assign'; data: { [key: string]: any } }
|
|
||||||
| { type: 'Refresh' },
|
|
||||||
},
|
|
||||||
|
|
||||||
predictableActionArguments: true,
|
|
||||||
preserveActionOrder: true,
|
|
||||||
tsTypes: {} as import('./fileMachine.typegen').Typegen0,
|
|
||||||
},
|
},
|
||||||
{
|
actions: {
|
||||||
actions: {
|
setFiles: assign(({ event }) => {
|
||||||
setFiles: assign((_, event) => {
|
if (event.type !== 'xstate.done.actor.read-files') return {}
|
||||||
return { project: event.data }
|
return { project: event.output }
|
||||||
}),
|
}),
|
||||||
setSelectedDirectory: assign((_, event) => {
|
setSelectedDirectory: assign(({ event }) => {
|
||||||
return { selectedDirectory: event.data }
|
if (event.type !== 'Set selected directory') return {}
|
||||||
}),
|
return { selectedDirectory: event.directory }
|
||||||
|
}),
|
||||||
|
addFileToRenamingQueue: assign({
|
||||||
|
itemsBeingRenamed: ({ context, event }) => {
|
||||||
|
if (event.type !== 'xstate.done.actor.create-and-open-file')
|
||||||
|
return context.itemsBeingRenamed
|
||||||
|
return [...context.itemsBeingRenamed, event.output.path]
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
removeFileFromRenamingQueue: assign({
|
||||||
|
itemsBeingRenamed: ({ context, event }) => {
|
||||||
|
if (event.type !== 'xstate.done.actor.rename-file')
|
||||||
|
return context.itemsBeingRenamed
|
||||||
|
return context.itemsBeingRenamed.filter(
|
||||||
|
(path) => path !== event.output.oldPath
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
navigateToFile: () => {},
|
||||||
|
renameToastSuccess: () => {},
|
||||||
|
createToastSuccess: () => {},
|
||||||
|
toastSuccess: () => {},
|
||||||
|
toastError: () => {},
|
||||||
|
},
|
||||||
|
guards: {
|
||||||
|
'Name has been changed': ({ event }) => {
|
||||||
|
if (event.type !== 'xstate.done.actor.rename-file') return false
|
||||||
|
return event.output.newPath !== event.output.oldPath
|
||||||
},
|
},
|
||||||
guards: {
|
'Has at least 1 file': ({ event }) => {
|
||||||
'Name has been changed': (_, event) => {
|
if (event.type !== 'xstate.done.actor.read-files') return false
|
||||||
return event.data.newPath !== event.data.oldPath
|
return !!event?.output?.children && event.output.children.length > 0
|
||||||
|
},
|
||||||
|
'Is not silent': ({ event }) =>
|
||||||
|
event.type === 'Create file' ? !event.data.silent : false,
|
||||||
|
},
|
||||||
|
actors: {
|
||||||
|
readFiles: fromPromise(({ input }: { input: Project }) =>
|
||||||
|
Promise.resolve(input)
|
||||||
|
),
|
||||||
|
createAndOpenFile: fromPromise(
|
||||||
|
(_: {
|
||||||
|
input: {
|
||||||
|
name: string
|
||||||
|
makeDir: boolean
|
||||||
|
selectedDirectory: FileEntry
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
}) => Promise.resolve({ message: '', path: '' })
|
||||||
|
),
|
||||||
|
renameFile: fromPromise(
|
||||||
|
(_: {
|
||||||
|
input: {
|
||||||
|
oldName: string
|
||||||
|
newName: string
|
||||||
|
isDir: boolean
|
||||||
|
selectedDirectory: FileEntry
|
||||||
|
}
|
||||||
|
}) => Promise.resolve({ message: '', newPath: '', oldPath: '' })
|
||||||
|
),
|
||||||
|
deleteFile: fromPromise(
|
||||||
|
(_: {
|
||||||
|
input: { path: string; children: FileEntry[] | null; name: string }
|
||||||
|
}) => Promise.resolve({ message: '' } as { message: string } | undefined)
|
||||||
|
),
|
||||||
|
createFile: fromPromise(
|
||||||
|
(_: {
|
||||||
|
input: {
|
||||||
|
name: string
|
||||||
|
makeDir: boolean
|
||||||
|
selectedDirectory: FileEntry
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
}) => Promise.resolve({ path: '' })
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
/** @xstate-layout N4IgpgJg5mDOIC5QDECWAbMACAtgQwGMALVAOzAGI9ZZUpSBtABgF1FQAHAe1oBdUupdiAAeiACwAmADQgAnogAcANgCsAOnHiAjOICcAZh3K9TRQHYAvpdlpMuQiXIUASmABmAJzhFmbJCDcfAJCAWIIUrIKCHpq6nraipJJKorahqrWthjY+MRkYOoAEtRYpFxY7jmwFADC3ni82FWYfsJBqPyCwuG6hurKTAYG5mlSyeJRiHqS2prKg6Mj2pKz4lkgdrmOBcWlLXCuYKR4OM05bQEdXaGgvdpD6qPiioqqieJM2gaqUxELT3MQz0eleBlMhmUGy2Dny5D2sEq1TqDSaSNarHaPE6IR6iD6BgGQxGY1Wikm8gkQM0n2UknERm0qmUw2hOVhTkKJURBxq9TAjXOrW0-k42JueIQBKJw1GujJFOiqkkTE0X3M5mUuiYgxmbPseU5CPRhwAImBMGiDpcxcFumF8dptMp1GZRlqNYYdXo-ml1IlzMkHuZVAZJAY1PrtnCuftkQB5DjHE02wLi3EOqX6QmDWWkiZ-HSKf3gph6ZWqcwJIZRjm7bkmmoAZTAvCwsAtYAITQgWAgqG83a4njkqeuGbu+MkLPU7x+iiGszJfwj4ldTvB6UURh0klrht2-MaZCgWDwpF7XCTpBPJooEEEhTIADcuABrQoEVFgAC054gP5XscP7WpiVzpvak5SnO6hJD8RYfJ8ir4kw06zqoTDiMyTA4WGPz7js8JHvwpCnv+WBATepF3mAnieMO6gcOgjTuMOODqF+ApNH+F6AdeIEXGBto4pBoj4u8GjOiMqjiKMqhJFhfyVqqEbJIoCTkmk3wETG6huCcOC3gc96PuoL7voU3gGb+oGimmdq3GJCARuY8RMroEk6MMihKeWrpYepEZMKMSw6Ua+mnEZOQULR9GeIxzG8KxnjsVZpw2YJdnjqJ4QjK59KaoGLKhh6fyBpIsFgtqKjKuCYW7OalpRZgJnwuZH7qBAnbcbZWIOZKeXqAVyhFT8EbaOYK44f6kjlhYG6FVYNibOyB7wo1rbNZQsUMUxLFsZ13UZRiWUQY5uUakNskjdOY2lZSCAqhV24LlhHpMl89Xwm4eD9tRvKtU+pCvh1DQAbyY5nZKMwqZWwxqMorzltoZUrK6YbOlJoazIoX2FD9f2ngDD5tcDFnqGDAmYLADAin1InndMKrqD85jw8ySPvH8pgulqoYWEjc16HjekCoTjYxXRu2JclqVi1TcCQ-1mYwyzcMRhz6lcw9C56Cz4Yatd05ISLxFbYDZlkx1nGCgrSsM5KTJVgMMmjKkEYmAYfwrOkQ30i8WFSF8mTLTCa2FGb-3RTt8V7UlB02z1mX0xKmZMgu8R6C8YahqYwUow9TqBkNxXiLmUgGEyIsRYZUctSTQMg5ZxzpXbdPgcrUEuW57xYUyXkGD5D2Bhog9aKsLyzQywsbOUXXwAEYeEWAKcTk5P7KH8G+ujhuHDDJTJZ0t2QGsvxrlI2q85fiBhlgMZcQq8+iqDJ3OzAML2qCCqxDEkIsNryK+jMpSV1clIck3xB6ViLIWEwmhXiJF0EYIqptUS3nIpRLaQDHajAqvKCwqxEZTxkIXVChJNTqUDCkB4L9q4t1rkTHI2DMyRAeosIawxFxDESMoLCIsNokUYZgZhUF1IDGdK7LyWgX6TULqCIagYcKSHMAya6VdQ6rTPgTLaC9hKpygnSOY8FA7kj0J6WR0QISzn0J8IYN0tIi0TMcLBHcHZp1wf6cB5UiFZxIdEcEhJKyvQ9BqGSqCuIuL0WvXoHj8HeKSL472E0KrBRfrVRGL9cbWEsEAA */
|
||||||
|
id: 'File machine',
|
||||||
|
|
||||||
|
initial: 'Reading files',
|
||||||
|
|
||||||
|
context: ({ input }) => {
|
||||||
|
return {
|
||||||
|
project: input.project ?? ({} as Project), // TODO: Either make this a flexible type or type this property to allow empty object
|
||||||
|
selectedDirectory: input.selectedDirectory ?? ({} as FileEntry), // TODO: Either make this a flexible type or type this property to allow empty object
|
||||||
|
itemsBeingRenamed: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
on: {
|
||||||
|
assign: {
|
||||||
|
actions: assign(({ event }) => ({
|
||||||
|
...event.data,
|
||||||
|
})),
|
||||||
|
target: '.Reading files',
|
||||||
|
},
|
||||||
|
|
||||||
|
Refresh: '.Reading files',
|
||||||
|
},
|
||||||
|
states: {
|
||||||
|
'Has no files': {
|
||||||
|
on: {
|
||||||
|
'Create file': {
|
||||||
|
target: 'Creating and opening file',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
|
||||||
)
|
'Has files': {
|
||||||
|
on: {
|
||||||
|
'Rename file': {
|
||||||
|
target: 'Renaming file',
|
||||||
|
},
|
||||||
|
|
||||||
|
'Create file': [
|
||||||
|
{
|
||||||
|
target: 'Creating and opening file',
|
||||||
|
guard: 'Is not silent',
|
||||||
|
},
|
||||||
|
'Creating file',
|
||||||
|
],
|
||||||
|
|
||||||
|
'Delete file': {
|
||||||
|
target: 'Deleting file',
|
||||||
|
},
|
||||||
|
|
||||||
|
'Open file': {
|
||||||
|
target: 'Opening file',
|
||||||
|
},
|
||||||
|
|
||||||
|
'Set selected directory': {
|
||||||
|
target: 'Has files',
|
||||||
|
actions: ['setSelectedDirectory'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Creating and opening file': {
|
||||||
|
invoke: {
|
||||||
|
id: 'create-and-open-file',
|
||||||
|
src: 'createAndOpenFile',
|
||||||
|
input: ({ event, context }) => {
|
||||||
|
if (event.type !== 'Create file')
|
||||||
|
// This is just to make TS happy
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
makeDir: false,
|
||||||
|
selectedDirectory: context.selectedDirectory,
|
||||||
|
content: '',
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: event.data.name,
|
||||||
|
makeDir: event.data.makeDir,
|
||||||
|
selectedDirectory: context.selectedDirectory,
|
||||||
|
content: event.data.content ?? '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDone: [
|
||||||
|
{
|
||||||
|
target: 'Reading files',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: 'createToastSuccess',
|
||||||
|
params: ({
|
||||||
|
event,
|
||||||
|
}: {
|
||||||
|
// TODO: rely on type inference
|
||||||
|
event: Extract<
|
||||||
|
FileMachineEvents,
|
||||||
|
{ type: 'xstate.done.actor.create-and-open-file' }
|
||||||
|
>
|
||||||
|
}) => {
|
||||||
|
return { message: event.output.message }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'addFileToRenamingQueue',
|
||||||
|
'navigateToFile',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onError: [
|
||||||
|
{
|
||||||
|
target: 'Reading files',
|
||||||
|
actions: ['toastError'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Renaming file': {
|
||||||
|
invoke: {
|
||||||
|
id: 'rename-file',
|
||||||
|
src: 'renameFile',
|
||||||
|
input: ({ event, context }) => {
|
||||||
|
if (event.type !== 'Rename file') {
|
||||||
|
// This is just to make TS happy
|
||||||
|
return {
|
||||||
|
oldName: '',
|
||||||
|
newName: '',
|
||||||
|
isDir: false,
|
||||||
|
selectedDirectory: {} as FileEntry,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
oldName: event.data.oldName,
|
||||||
|
newName: event.data.newName,
|
||||||
|
isDir: event.data.isDir,
|
||||||
|
selectedDirectory: context.selectedDirectory,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onDone: [
|
||||||
|
{
|
||||||
|
target: '#File machine.Reading files',
|
||||||
|
actions: ['renameToastSuccess'],
|
||||||
|
guard: 'Name has been changed',
|
||||||
|
},
|
||||||
|
'Reading files',
|
||||||
|
],
|
||||||
|
onError: [
|
||||||
|
{
|
||||||
|
target: '#File machine.Reading files',
|
||||||
|
actions: ['toastError'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
exit: 'removeFileFromRenamingQueue',
|
||||||
|
},
|
||||||
|
|
||||||
|
'Deleting file': {
|
||||||
|
invoke: {
|
||||||
|
id: 'delete-file',
|
||||||
|
src: 'deleteFile',
|
||||||
|
input: ({ event }) => {
|
||||||
|
if (event.type !== 'Delete file') {
|
||||||
|
// This is just to make TS happy
|
||||||
|
return {
|
||||||
|
path: '',
|
||||||
|
children: [],
|
||||||
|
name: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
path: event.data.path,
|
||||||
|
children: event.data.children,
|
||||||
|
name: event.data.name,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDone: [
|
||||||
|
{
|
||||||
|
actions: ['toastSuccess'],
|
||||||
|
target: '#File machine.Reading files',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onError: {
|
||||||
|
actions: ['toastError'],
|
||||||
|
target: '#File machine.Has files',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Reading files': {
|
||||||
|
invoke: {
|
||||||
|
id: 'read-files',
|
||||||
|
src: 'readFiles',
|
||||||
|
input: ({ context }) => context.project,
|
||||||
|
onDone: [
|
||||||
|
{
|
||||||
|
guard: 'Has at least 1 file',
|
||||||
|
target: 'Has files',
|
||||||
|
actions: ['setFiles'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'Has no files',
|
||||||
|
actions: ['setFiles'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onError: [
|
||||||
|
{
|
||||||
|
target: 'Has no files',
|
||||||
|
actions: ['toastError'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Opening file': {
|
||||||
|
entry: ['navigateToFile'],
|
||||||
|
},
|
||||||
|
|
||||||
|
'Creating file': {
|
||||||
|
invoke: {
|
||||||
|
src: 'createFile',
|
||||||
|
id: 'create-file',
|
||||||
|
input: ({ event, context }) => {
|
||||||
|
if (event.type !== 'Create file') {
|
||||||
|
// This is just to make TS happy
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
makeDir: false,
|
||||||
|
selectedDirectory: {} as FileEntry,
|
||||||
|
content: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: event.data.name,
|
||||||
|
makeDir: event.data.makeDir,
|
||||||
|
selectedDirectory: context.selectedDirectory,
|
||||||
|
content: event.data.content ?? '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDone: 'Reading files',
|
||||||
|
onError: 'Reading files',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ -1,164 +1,233 @@
|
|||||||
import { assign, createMachine } from 'xstate'
|
import { assign, fromPromise, setup } from 'xstate'
|
||||||
import { HomeCommandSchema } from 'lib/commandBarConfigs/homeCommandConfig'
|
import { HomeCommandSchema } from 'lib/commandBarConfigs/homeCommandConfig'
|
||||||
import { Project } from 'lib/project'
|
import { Project } from 'lib/project'
|
||||||
|
|
||||||
export const homeMachine = createMachine(
|
export const homeMachine = setup({
|
||||||
{
|
types: {
|
||||||
/** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAHTK6xampYAOATqgFZj4AusAxAMLMwuLthbtOXANoAGALqJQjVLGJdiqUopAAPRAHYAbPooAWABwBGUwE5zAJgeGArM-MAaEAE9EN0wGYKGX97GX1nGVNDS0MbfwBfeM80TBwCEnIqGiZWDm4+ACUwUlxU8TzpeW1lVXVNbT0EcJNg02d-fzt7fU77Tx8EQ0iKCPtnfUsjGRtLGXtE5IxsPCIySmpacsk+QWFRHIluWQUkEBq1DS1TxqN7ChjzOxtXf0t7a37EcwsRibH-ZzRezA8wLEApZbpNZZTa5ba8AAiYAANmB9lsjlVTuc6ldQDdDOYKP5bm0os5TDJDJ8mlEzPpzIZHA4bO9umCIWlVpkNgcKnwAPKMYp8yTHaoqC71a6IEmBUz6BkWZzWDq2Uw0qzOIJAwz+PXWfSmeZJcFLLkZSi7ERkKCi7i8CCaShkABuqAA1pR8EIRGAALQYyonJSS3ENRDA2wUeyvd6dPVhGw0-RhGOp8IA8xGFkc80rS0Ua3qUh2oO8MDMVjMCiMZEiABmqGY6AoPr2AaD4uxYcuEYQoQpQWNNjsMnMgLGKbT3TC7TcOfsNjzqQL0KKJXQtvtXEdzoobs9lCEm87cMxIbOvel+MQqtMQRmS5ks31sZpAUsZkcIX+cQZJIrpC3KUBupTbuWlbVrW9ZcE2LYUCepRnocwYSrUfYyggbzvBQ+jMq49imLYwTUt4iCft+5i-u0-7UfoQEWtCSKoiWZbnruTqZIeXoUBAKJoihFTdqGGE3rod7UdqsQTI8hiGAqrIauRA7RvYeoqhO1jtAqjFrpkLFohBHEVlWzYwY2zatvxrFCWKWKiVKeISdh4yBJE-jGs4fhhA4zg0kRNgxhplhaW0nn4XpUKZEUuAQMZqF8FxLqkO6vG+hAgYcbAIlXmJzmNERdy0RYNiKgpthxDSEU6q8MSTJYjWGFFIEULF8WljuSX7jxx7CJlQY5ZYl44pht4IP61gyPc8njt0lIuH51UKrVVITEyMy2C1hbtQl-KmdBdaWQhGVZYluWjeJjSTf402shMEyuEyljPAFL0UNmMiuN86lWHMiSmvQ-HwKcnL6WA6FOf2k3mESMRDA4RpUm4U4qf6gSEt0QIvvqfjOCaiyrtF6zZPQXWQ+GWFlUEsbmNMf1TV9NLeXDcqRIySnNaaYPEzC5M9vl-b+IyFCjupryPF9jKWP5Kks-cbMWLERHRNt0LFntkgU2NLk4dqsz43YsTK++Kk2C+MbTOOcxzOMrhqzFxTgZ1Qba1dd6BUE1jGsLMxxK9KlDNqm3tMLUQvqYlgO5QhlsTubsFXesTTUuPTfHExshDS0RftRftGgEnTZtHbX9Zr+QJ-2S4Y3qnmTC+4tMyp1EfeOnmeQqdOhyXQrFOXXCV1hCkmLDOnBJYvRRDSsyRzGjiKj0lKdAkANAA */
|
context: {} as {
|
||||||
id: 'Home machine',
|
projects: Project[]
|
||||||
|
defaultProjectName: string
|
||||||
initial: 'Reading projects',
|
defaultDirectory: string
|
||||||
|
|
||||||
context: {
|
|
||||||
projects: [] as Project[],
|
|
||||||
defaultProjectName: '',
|
|
||||||
defaultDirectory: '',
|
|
||||||
},
|
},
|
||||||
|
events: {} as
|
||||||
on: {
|
| { type: 'Open project'; data: HomeCommandSchema['Open project'] }
|
||||||
assign: {
|
| { type: 'Rename project'; data: HomeCommandSchema['Rename project'] }
|
||||||
actions: assign((_, event) => ({
|
| { type: 'Create project'; data: HomeCommandSchema['Create project'] }
|
||||||
...event.data,
|
| { type: 'Delete project'; data: HomeCommandSchema['Delete project'] }
|
||||||
})),
|
| { type: 'navigate'; data: { name: string } }
|
||||||
target: '.Reading projects',
|
| {
|
||||||
},
|
type: 'xstate.done.actor.read-projects'
|
||||||
|
output: Project[]
|
||||||
|
}
|
||||||
|
| { type: 'assign'; data: { [key: string]: any } },
|
||||||
|
input: {} as {
|
||||||
|
projects: Project[]
|
||||||
|
defaultProjectName: string
|
||||||
|
defaultDirectory: string
|
||||||
},
|
},
|
||||||
states: {
|
|
||||||
'Has no projects': {
|
|
||||||
on: {
|
|
||||||
'Create project': {
|
|
||||||
target: 'Creating project',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'Has projects': {
|
|
||||||
on: {
|
|
||||||
'Rename project': {
|
|
||||||
target: 'Renaming project',
|
|
||||||
},
|
|
||||||
|
|
||||||
'Create project': {
|
|
||||||
target: 'Creating project',
|
|
||||||
},
|
|
||||||
|
|
||||||
'Delete project': {
|
|
||||||
target: 'Deleting project',
|
|
||||||
},
|
|
||||||
|
|
||||||
'Open project': {
|
|
||||||
target: 'Opening project',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'Creating project': {
|
|
||||||
invoke: {
|
|
||||||
id: 'create-project',
|
|
||||||
src: 'createProject',
|
|
||||||
onDone: [
|
|
||||||
{
|
|
||||||
target: 'Reading projects',
|
|
||||||
actions: ['toastSuccess'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
onError: [
|
|
||||||
{
|
|
||||||
target: 'Reading projects',
|
|
||||||
actions: ['toastError'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'Renaming project': {
|
|
||||||
invoke: {
|
|
||||||
id: 'rename-project',
|
|
||||||
src: 'renameProject',
|
|
||||||
onDone: [
|
|
||||||
{
|
|
||||||
target: '#Home machine.Reading projects',
|
|
||||||
actions: ['toastSuccess'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
onError: [
|
|
||||||
{
|
|
||||||
target: '#Home machine.Reading projects',
|
|
||||||
actions: ['toastError'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'Deleting project': {
|
|
||||||
invoke: {
|
|
||||||
id: 'delete-project',
|
|
||||||
src: 'deleteProject',
|
|
||||||
onDone: [
|
|
||||||
{
|
|
||||||
actions: ['toastSuccess'],
|
|
||||||
target: '#Home machine.Reading projects',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
onError: {
|
|
||||||
actions: ['toastError'],
|
|
||||||
target: '#Home machine.Has projects',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'Reading projects': {
|
|
||||||
invoke: {
|
|
||||||
id: 'read-projects',
|
|
||||||
src: 'readProjects',
|
|
||||||
onDone: [
|
|
||||||
{
|
|
||||||
cond: 'Has at least 1 project',
|
|
||||||
target: 'Has projects',
|
|
||||||
actions: ['setProjects'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: 'Has no projects',
|
|
||||||
actions: ['setProjects'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
onError: [
|
|
||||||
{
|
|
||||||
target: 'Has no projects',
|
|
||||||
actions: ['toastError'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'Opening project': {
|
|
||||||
entry: ['navigateToProject'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
schema: {
|
|
||||||
events: {} as
|
|
||||||
| { type: 'Open project'; data: HomeCommandSchema['Open project'] }
|
|
||||||
| { type: 'Rename project'; data: HomeCommandSchema['Rename project'] }
|
|
||||||
| { type: 'Create project'; data: HomeCommandSchema['Create project'] }
|
|
||||||
| { type: 'Delete project'; data: HomeCommandSchema['Delete project'] }
|
|
||||||
| { type: 'navigate'; data: { name: string } }
|
|
||||||
| {
|
|
||||||
type: 'done.invoke.read-projects'
|
|
||||||
data: Project[]
|
|
||||||
}
|
|
||||||
| { type: 'assign'; data: { [key: string]: any } },
|
|
||||||
},
|
|
||||||
|
|
||||||
predictableActionArguments: true,
|
|
||||||
preserveActionOrder: true,
|
|
||||||
tsTypes: {} as import('./homeMachine.typegen').Typegen0,
|
|
||||||
},
|
},
|
||||||
{
|
actions: {
|
||||||
actions: {
|
setProjects: assign({
|
||||||
setProjects: assign((_, event) => {
|
projects: ({ context, event }) =>
|
||||||
return { projects: event.data as Project[] }
|
'output' in event ? event.output : context.projects,
|
||||||
}),
|
}),
|
||||||
|
toastSuccess: () => {},
|
||||||
|
toastError: () => {},
|
||||||
|
navigateToProject: () => {},
|
||||||
|
},
|
||||||
|
actors: {
|
||||||
|
readProjects: fromPromise(() => Promise.resolve([] as Project[])),
|
||||||
|
createProject: fromPromise((_: { input: { name: string } }) =>
|
||||||
|
Promise.resolve('')
|
||||||
|
),
|
||||||
|
renameProject: fromPromise(
|
||||||
|
(_: {
|
||||||
|
input: {
|
||||||
|
oldName: string
|
||||||
|
newName: string
|
||||||
|
defaultProjectName: string
|
||||||
|
defaultDirectory: string
|
||||||
|
}
|
||||||
|
}) => Promise.resolve('')
|
||||||
|
),
|
||||||
|
deleteProject: fromPromise(
|
||||||
|
(_: { input: { defaultDirectory: string; name: string } }) =>
|
||||||
|
Promise.resolve('')
|
||||||
|
),
|
||||||
|
},
|
||||||
|
guards: {
|
||||||
|
'Has at least 1 project': () => false,
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
/** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAHTK6xampYAOATqgFZj4AusAxAMLMwuLthbtOXANoAGALqJQjVLGJdiqUopAAPRAHYAbPooAWABwBGUwE5zAJgeGArM-MAaEAE9EN0wGYKGX97GX1nGVNDS0MbfwBfeM80TBwCEnIqGiZWDm4+ACUwUlxU8TzpeW1lVXVNbT0EcJNg02d-fzt7fU77Tx8EQ0iKCPtnfUsjGRtLGXtE5IxsPCIySmpacsk+QWFRHIluWQUkEBq1DS1TxqN7ChjzOxtXf0t7a37EcwsRibH-ZzRezA8wLEApZbpNZZTa5ba8AAiYAANmB9lsjlVTuc6ldQDdDOYKP5bm0os5TDJDJ8mlEzPpzIZHA4bO9umCIWlVpkNgcKnwAPKMYp8yTHaoqC71a6IEmBUz6BkWZzWDq2Uw0qzOIJAwz+PXWfSmeZJcFLLkZSi7ERkKCi7i8CCaShkABuqAA1pR8EIRGAALQYyonJSS3ENRDA2wUeyvd6dPVhGw0-RhGOp8IA8xGFkc80rS0Ua3qUh2oO8MDMVjMCiMZEiABmqGY6AoPr2AaD4uxYcuEYQoQpQWNNjsMnMgLGKbT3TC7TcOfsNjzqQL0KKJXQtvtXEdzoobs9lCEm87cMxIbOvel+MQqtMQRmS5ks31sZpAUsZkcIX+cQZJIrpC3KUBupTbuWlbVrW9ZcE2LYUCepRnocwYSrUfYyggbzvBQ+jMq49imLYwTUt4iCft+5i-u0-7UfoQEWtCSKoiWZbnruTqZIeXoUBAKJoihFTdqGGE3rod7UdqsQTI8hiGAqrIauRA7RvYeoqhO1jtAqjFrpkLFohBHEVlWzYwY2zatvxrFCWKWKiVKeISdh4yBJE-jGs4fhhA4zg0kRNgxhplhaW0nn4XpUKZEUuAQMZqF8FxLqkO6vG+hAgYcbAIlXmJzmNERdy0RYNiKgpthxDSEU6q8MSTJYjWGFFIEULF8WljuSX7jxx7CJlQY5ZYl44pht4IP61gyPc8njt0lIuH51UKrVVITEyMy2C1hbtQl-KmdBdaWQhGVZYluWjeJjSTf402shMEyuEyljPAFL0UNmMiuN86lWHMiSmvQ-HwKcnL6WA6FOf2k3mESMRDA4RpUm4U4qf6gSEt0QIvvqfjOCaiyrtF6zZPQXWQ+GWFlUEsbmNMf1TV9NLeXDcqRIySnNaaYPEzC5M9vl-b+IyFCjupryPF9jKWP5Kks-cbMWLERHRNt0LFntkgU2NLk4dqsz43YsTK++Kk2C+MbTOOcxzOMrhqzFxTgZ1Qba1dd6BUE1jGsLMxxK9KlDNqm3tMLUQvqYlgO5QhlsTubsFXesTTUuPTfHExshDS0RftRftGgEnTZtHbX9Zr+QJ-2S4Y3qnmTC+4tMyp1EfeOnmeQqdOhyXQrFOXXCV1hCkmLDOnBJYvRRDSsyRzGjiKj0lKdAkANAA */
|
||||||
|
id: 'Home machine',
|
||||||
|
|
||||||
|
initial: 'Reading projects',
|
||||||
|
|
||||||
|
context: {
|
||||||
|
projects: [],
|
||||||
|
defaultProjectName: '',
|
||||||
|
defaultDirectory: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
on: {
|
||||||
|
assign: {
|
||||||
|
actions: assign(({ event }) => ({
|
||||||
|
...event.data,
|
||||||
|
})),
|
||||||
|
target: '.Reading projects',
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
states: {
|
||||||
|
'Has no projects': {
|
||||||
|
on: {
|
||||||
|
'Create project': {
|
||||||
|
target: 'Creating project',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Has projects': {
|
||||||
|
on: {
|
||||||
|
'Rename project': {
|
||||||
|
target: 'Renaming project',
|
||||||
|
},
|
||||||
|
|
||||||
|
'Create project': {
|
||||||
|
target: 'Creating project',
|
||||||
|
},
|
||||||
|
|
||||||
|
'Delete project': {
|
||||||
|
target: 'Deleting project',
|
||||||
|
},
|
||||||
|
|
||||||
|
'Open project': {
|
||||||
|
target: 'Opening project',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Creating project': {
|
||||||
|
invoke: {
|
||||||
|
id: 'create-project',
|
||||||
|
src: 'createProject',
|
||||||
|
input: ({ event }) => {
|
||||||
|
if (event.type !== 'Create project') {
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: event.data.name,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDone: [
|
||||||
|
{
|
||||||
|
target: 'Reading projects',
|
||||||
|
actions: ['toastSuccess'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onError: [
|
||||||
|
{
|
||||||
|
target: 'Reading projects',
|
||||||
|
actions: ['toastError'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Renaming project': {
|
||||||
|
invoke: {
|
||||||
|
id: 'rename-project',
|
||||||
|
src: 'renameProject',
|
||||||
|
input: ({ event, context }) => {
|
||||||
|
if (event.type !== 'Rename project') {
|
||||||
|
// This is to make TS happy
|
||||||
|
return {
|
||||||
|
defaultProjectName: context.defaultProjectName,
|
||||||
|
defaultDirectory: context.defaultDirectory,
|
||||||
|
oldName: '',
|
||||||
|
newName: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
defaultProjectName: context.defaultProjectName,
|
||||||
|
defaultDirectory: context.defaultDirectory,
|
||||||
|
oldName: event.data.oldName,
|
||||||
|
newName: event.data.newName,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDone: [
|
||||||
|
{
|
||||||
|
target: '#Home machine.Reading projects',
|
||||||
|
actions: ['toastSuccess'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onError: [
|
||||||
|
{
|
||||||
|
target: '#Home machine.Reading projects',
|
||||||
|
actions: ['toastError'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Deleting project': {
|
||||||
|
invoke: {
|
||||||
|
id: 'delete-project',
|
||||||
|
src: 'deleteProject',
|
||||||
|
input: ({ event, context }) => {
|
||||||
|
if (event.type !== 'Delete project') {
|
||||||
|
// This is to make TS happy
|
||||||
|
return {
|
||||||
|
defaultDirectory: context.defaultDirectory,
|
||||||
|
name: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
defaultDirectory: context.defaultDirectory,
|
||||||
|
name: event.data.name,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDone: [
|
||||||
|
{
|
||||||
|
actions: ['toastSuccess'],
|
||||||
|
target: '#Home machine.Reading projects',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onError: {
|
||||||
|
actions: ['toastError'],
|
||||||
|
target: '#Home machine.Has projects',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Reading projects': {
|
||||||
|
invoke: {
|
||||||
|
id: 'read-projects',
|
||||||
|
src: 'readProjects',
|
||||||
|
onDone: [
|
||||||
|
{
|
||||||
|
guard: 'Has at least 1 project',
|
||||||
|
target: 'Has projects',
|
||||||
|
actions: ['setProjects'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'Has no projects',
|
||||||
|
actions: ['setProjects'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onError: [
|
||||||
|
{
|
||||||
|
target: 'Has no projects',
|
||||||
|
actions: ['toastError'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Opening project': {
|
||||||
|
entry: ['navigateToProject'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
File diff suppressed because one or more lines are too long
@ -1,4 +1,4 @@
|
|||||||
import { assign, createMachine } from 'xstate'
|
import { assign, setup } from 'xstate'
|
||||||
import { Themes, getSystemTheme, setThemeClass } from 'lib/theme'
|
import { Themes, getSystemTheme, setThemeClass } from 'lib/theme'
|
||||||
import { createSettings, settings } from 'lib/settings/initialSettings'
|
import { createSettings, settings } from 'lib/settings/initialSettings'
|
||||||
import {
|
import {
|
||||||
@ -9,177 +9,187 @@ import {
|
|||||||
WildcardSetEvent,
|
WildcardSetEvent,
|
||||||
} from 'lib/settings/settingsTypes'
|
} from 'lib/settings/settingsTypes'
|
||||||
|
|
||||||
export const settingsMachine = createMachine(
|
export const settingsMachine = setup({
|
||||||
{
|
types: {
|
||||||
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlnXwEMAHW-Ae2wCNHqAnCHKZNatAFdYAbQAMAXUShajWJizNpIAB6IALAFYAnPgBMARgDsBsQDY969QGYjmzQBoQAT0SnrADnwePY61r0PAwNtMyMAX3CnVAweAiJSCio6BjQACzAAWzAAYUZiRg5xKSQQWXlFbGU1BD1PfFtfE3UzTUNNaydXBCD1b209PTEPTTMtdQNNSOj0LFx4knJKNHxMxggwYh58DYAzakFiNABVbAVi5XKFTCVSmusxPXx7bRt1DzMxI3UjD3UutwhAz4MyeHxiV5+AYRKIgGJzPCERZJFYpfDpLJgC6lK6VaqIExPMwWGwdGxBPRmAE9PSafCPMQ-EzWbQ6ELTOGzOJIxLLVbrTbbNKYKBpLaitAAUWgcGxMjk11uoBqVmBH0ZLKCrVs-xciCCwLCvhCjyMFhGHPh3IS5AASnB0AACZYI0SSS4KvF3AlafADRl1YZ2IxiRx6hBtIzPb7abQ+DxGaxmYKWrnzHnkGKO6jEYjOtN4OVlT03KrehAtOnm7Qaup6Ixm6mR6OaR4dAwjM1mVOxdM2lH8jZbXD4WBpRgAd2QAGMc2AAOIcIhF3Gl-EIRPA6yGcyh4whSnU0xGJ5GAat0OfFowma9xH9gBUK5LStUiECdMmfx+mg8hmNTY-PgMYQpoZoxh41g9q6+C0GAHDyLACL5nesBkBAzBgIQ2AAG6MAA1lhcEIZgSFWvMz4VGu5YALTbtYwEnj8HhxnooT1mG3QhmY-TmJ82gGCyjzaJEsLYAK8ClOReAelRr41HRJiMZYvysexdjUuohh+poBiGDuXzGKy0HWossmKmWyqIDR3zAZWLSahM2jWJ04YjDxHbDMmmhaYE3wmemxGIchLpxOZXpWQgNEjMB1h6WEYHqK8ZgJk2EL6N8wR1Cy-gJqJ4RAA */
|
|
||||||
id: 'Settings',
|
|
||||||
predictableActionArguments: true,
|
|
||||||
context: {} as ReturnType<typeof createSettings>,
|
context: {} as ReturnType<typeof createSettings>,
|
||||||
initial: 'idle',
|
input: {} as ReturnType<typeof createSettings>,
|
||||||
states: {
|
events: {} as
|
||||||
idle: {
|
| WildcardSetEvent<SettingsPaths>
|
||||||
entry: ['setThemeClass', 'setClientSideSceneUnits'],
|
| SetEventTypes
|
||||||
|
| {
|
||||||
|
type: 'set.app.theme'
|
||||||
|
data: { level: SettingsLevel; value: Themes }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'set.modeling.units'
|
||||||
|
data: { level: SettingsLevel; value: BaseUnit }
|
||||||
|
}
|
||||||
|
| { type: 'Reset settings'; defaultDirectory: string }
|
||||||
|
| { type: 'Set all settings'; settings: typeof settings },
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
setEngineTheme: () => {},
|
||||||
|
setClientTheme: () => {},
|
||||||
|
'Execute AST': () => {},
|
||||||
|
toastSuccess: () => {},
|
||||||
|
setEngineEdges: () => {},
|
||||||
|
setEngineScaleGridVisibility: () => {},
|
||||||
|
setClientSideSceneUnits: () => {},
|
||||||
|
persistSettings: () => {},
|
||||||
|
resetSettings: assign(({ context, event }) => {
|
||||||
|
if (!('defaultDirectory' in event)) return {}
|
||||||
|
// Reset everything except onboarding status,
|
||||||
|
// which should be preserved
|
||||||
|
const newSettings = createSettings()
|
||||||
|
if (context.app.onboardingStatus.user) {
|
||||||
|
newSettings.app.onboardingStatus.user =
|
||||||
|
context.app.onboardingStatus.user
|
||||||
|
}
|
||||||
|
// We instead pass in the default directory since it's asynchronous
|
||||||
|
// to re-initialize, and that can be done by the caller.
|
||||||
|
newSettings.app.projectDirectory.default = event.defaultDirectory
|
||||||
|
|
||||||
on: {
|
return newSettings
|
||||||
'*': {
|
}),
|
||||||
target: 'persisting settings',
|
setAllSettings: assign(({ event }) => {
|
||||||
actions: ['setSettingAtLevel', 'toastSuccess'],
|
if (!('settings' in event)) return {}
|
||||||
},
|
return event.settings
|
||||||
|
}),
|
||||||
|
setSettingAtLevel: assign(({ context, event }) => {
|
||||||
|
if (!('data' in event)) return {}
|
||||||
|
const { level, value } = event.data
|
||||||
|
const [category, setting] = event.type
|
||||||
|
.replace(/^set./, '')
|
||||||
|
.split('.') as [keyof typeof settings, string]
|
||||||
|
|
||||||
'set.app.onboardingStatus': {
|
// @ts-ignore
|
||||||
target: 'persisting settings',
|
context[category][setting][level] = value
|
||||||
|
|
||||||
// No toast
|
const newContext = {
|
||||||
actions: ['setSettingAtLevel'],
|
...context,
|
||||||
},
|
[category]: {
|
||||||
|
...context[category],
|
||||||
'set.app.themeColor': {
|
// @ts-ignore
|
||||||
target: 'persisting settings',
|
[setting]: context[category][setting],
|
||||||
|
|
||||||
// No toast
|
|
||||||
actions: ['setSettingAtLevel'],
|
|
||||||
},
|
|
||||||
|
|
||||||
'set.modeling.defaultUnit': {
|
|
||||||
target: 'persisting settings',
|
|
||||||
|
|
||||||
actions: [
|
|
||||||
'setSettingAtLevel',
|
|
||||||
'toastSuccess',
|
|
||||||
'setClientSideSceneUnits',
|
|
||||||
'Execute AST',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
'set.app.theme': {
|
|
||||||
target: 'persisting settings',
|
|
||||||
|
|
||||||
actions: [
|
|
||||||
'setSettingAtLevel',
|
|
||||||
'toastSuccess',
|
|
||||||
'setThemeClass',
|
|
||||||
'setEngineTheme',
|
|
||||||
'setClientTheme',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
'set.app.streamIdleMode': {
|
|
||||||
target: 'persisting settings',
|
|
||||||
|
|
||||||
actions: ['setSettingAtLevel', 'toastSuccess'],
|
|
||||||
},
|
|
||||||
|
|
||||||
'set.modeling.highlightEdges': {
|
|
||||||
target: 'persisting settings',
|
|
||||||
|
|
||||||
actions: ['setSettingAtLevel', 'toastSuccess', 'setEngineEdges'],
|
|
||||||
},
|
|
||||||
|
|
||||||
'Reset settings': {
|
|
||||||
target: 'persisting settings',
|
|
||||||
|
|
||||||
actions: [
|
|
||||||
'resetSettings',
|
|
||||||
'setThemeClass',
|
|
||||||
'setEngineTheme',
|
|
||||||
'setClientSideSceneUnits',
|
|
||||||
'Execute AST',
|
|
||||||
'setClientTheme',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
'Set all settings': {
|
|
||||||
actions: [
|
|
||||||
'setAllSettings',
|
|
||||||
'setThemeClass',
|
|
||||||
'setEngineTheme',
|
|
||||||
'setClientSideSceneUnits',
|
|
||||||
'Execute AST',
|
|
||||||
'setClientTheme',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
'set.modeling.showScaleGrid': {
|
|
||||||
target: 'persisting settings',
|
|
||||||
actions: [
|
|
||||||
'setSettingAtLevel',
|
|
||||||
'toastSuccess',
|
|
||||||
'setEngineScaleGridVisibility',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
|
|
||||||
'persisting settings': {
|
return newContext
|
||||||
invoke: {
|
}),
|
||||||
src: 'Persist settings',
|
setThemeClass: ({ context }) => {
|
||||||
id: 'persistSettings',
|
const currentTheme = context.app.theme.current ?? Themes.System
|
||||||
onDone: 'idle',
|
setThemeClass(
|
||||||
},
|
currentTheme === Themes.System ? getSystemTheme() : currentTheme
|
||||||
},
|
)
|
||||||
},
|
|
||||||
tsTypes: {} as import('./settingsMachine.typegen').Typegen0,
|
|
||||||
schema: {
|
|
||||||
events: {} as
|
|
||||||
| WildcardSetEvent<SettingsPaths>
|
|
||||||
| SetEventTypes
|
|
||||||
| {
|
|
||||||
type: 'set.app.theme'
|
|
||||||
data: { level: SettingsLevel; value: Themes }
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'set.modeling.units'
|
|
||||||
data: { level: SettingsLevel; value: BaseUnit }
|
|
||||||
}
|
|
||||||
| { type: 'Reset settings'; defaultDirectory: string }
|
|
||||||
| { type: 'Set all settings'; settings: typeof settings },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
}).createMachine({
|
||||||
actions: {
|
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlnXwEMAHW-Ae2wCNHqAnCHKZNatAFdYAbQAMAXUShajWJizNpIAB6IALAFYAnPgBMARgDsBsQDY969QGYjmzQBoQAT0SnrADnwePY61r0PAwNtMyMAX3CnVAweAiJSCio6BjQACzAAWzAAYUZiRg5xKSQQWXlFbGU1BD1PfFtfE3UzTUNNaydXBCD1b209PTEPTTMtdQNNSOj0LFx4knJKNHxMxggwYh58DYAzakFiNABVbAVi5XKFTCVSmusxPXx7bRt1DzMxI3UjD3UutwhAz4MyeHxiV5+AYRKIgGJzPCERZJFYpfDpLJgC6lK6VaqIExPMwWGwdGxBPRmAE9PSafCPMQ-EzWbQ6ELTOGzOJIxLLVbrTbbNKYKBpLaitAAUWgcGxMjk11uoBqVmBH0ZLKCrVs-xciCCwLCvhCjyMFhGHPh3IS5AASnB0AACZYI0SSS4KvF3AlafADRl1YZ2IxiRx6hBtIzPb7abQ+DxGaxmYKWrnzHnkGKO6jEYjOtN4OVlT03KrehAtOnm7Qaup6Ixm6mR6OaR4dAwjM1mVOxdM2lH8jZbXD4WBpRgAd2QAGMc2AAOIcIhF3Gl-EIRPA6yGcyh4whSnU0xGJ5GAat0OfFowma9xH9gBUK5LStUiECdMmfx+mg8hmNTY-PgMYQpoZoxh41g9q6+C0GAHDyLACL5nesBkBAzBgIQ2AAG6MAA1lhcEIZgSFWvMz4VGu5YALTbtYwEnj8HhxnooT1mG3QhmY-TmJ82gGCyjzaJEsLYAK8ClOReAelRr41HRJiMZYvysexdjUuohh+poBiGDuXzGKy0HWossmKmWyqIDR3zAZWLSahM2jWJ04YjDxHbDMmmhaYE3wmemxGIchLpxOZXpWQgNEjMB1h6WEYHqK8ZgJk2EL6N8wR1Cy-gJqJ4RAA */
|
||||||
resetSettings: assign((context, { defaultDirectory }) => {
|
id: 'Settings',
|
||||||
// Reset everything except onboarding status,
|
initial: 'idle',
|
||||||
// which should be preserved
|
context: ({ input }) => {
|
||||||
const newSettings = createSettings()
|
return {
|
||||||
if (context.app.onboardingStatus.user) {
|
...createSettings(),
|
||||||
newSettings.app.onboardingStatus.user =
|
...input,
|
||||||
context.app.onboardingStatus.user
|
}
|
||||||
}
|
},
|
||||||
// We instead pass in the default directory since it's asynchronous
|
states: {
|
||||||
// to re-initialize, and that can be done by the caller.
|
idle: {
|
||||||
newSettings.app.projectDirectory.default = defaultDirectory
|
entry: ['setThemeClass', 'setClientSideSceneUnits'],
|
||||||
|
|
||||||
return newSettings
|
on: {
|
||||||
}),
|
'*': {
|
||||||
setAllSettings: assign((_, event) => {
|
target: 'persisting settings',
|
||||||
return event.settings
|
actions: ['setSettingAtLevel', 'toastSuccess'],
|
||||||
}),
|
},
|
||||||
setSettingAtLevel: assign((context, event) => {
|
|
||||||
const { level, value } = event.data
|
|
||||||
const [category, setting] = event.type
|
|
||||||
.replace(/^set./, '')
|
|
||||||
.split('.') as [keyof typeof settings, string]
|
|
||||||
|
|
||||||
// @ts-ignore
|
'set.app.onboardingStatus': {
|
||||||
context[category][setting][level] = value
|
target: 'persisting settings',
|
||||||
|
|
||||||
const newContext = {
|
// No toast
|
||||||
...context,
|
actions: ['setSettingAtLevel'],
|
||||||
[category]: {
|
},
|
||||||
...context[category],
|
|
||||||
// @ts-ignore
|
|
||||||
[setting]: context[category][setting],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return newContext
|
'set.app.themeColor': {
|
||||||
}),
|
target: 'persisting settings',
|
||||||
setThemeClass: (context) => {
|
|
||||||
const currentTheme = context.app.theme.current ?? Themes.System
|
// No toast
|
||||||
setThemeClass(
|
actions: ['setSettingAtLevel'],
|
||||||
currentTheme === Themes.System ? getSystemTheme() : currentTheme
|
},
|
||||||
)
|
|
||||||
|
'set.modeling.defaultUnit': {
|
||||||
|
target: 'persisting settings',
|
||||||
|
|
||||||
|
actions: [
|
||||||
|
'setSettingAtLevel',
|
||||||
|
'toastSuccess',
|
||||||
|
'setClientSideSceneUnits',
|
||||||
|
'Execute AST',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
'set.app.theme': {
|
||||||
|
target: 'persisting settings',
|
||||||
|
|
||||||
|
actions: [
|
||||||
|
'setSettingAtLevel',
|
||||||
|
'toastSuccess',
|
||||||
|
'setThemeClass',
|
||||||
|
'setEngineTheme',
|
||||||
|
'setClientTheme',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
'set.app.streamIdleMode': {
|
||||||
|
target: 'persisting settings',
|
||||||
|
|
||||||
|
actions: ['setSettingAtLevel', 'toastSuccess'],
|
||||||
|
},
|
||||||
|
|
||||||
|
'set.modeling.highlightEdges': {
|
||||||
|
target: 'persisting settings',
|
||||||
|
|
||||||
|
actions: ['setSettingAtLevel', 'toastSuccess', 'setEngineEdges'],
|
||||||
|
},
|
||||||
|
|
||||||
|
'Reset settings': {
|
||||||
|
target: 'persisting settings',
|
||||||
|
|
||||||
|
actions: [
|
||||||
|
'resetSettings',
|
||||||
|
'setThemeClass',
|
||||||
|
'setEngineTheme',
|
||||||
|
'setClientSideSceneUnits',
|
||||||
|
'Execute AST',
|
||||||
|
'setClientTheme',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
'Set all settings': {
|
||||||
|
actions: [
|
||||||
|
'setAllSettings',
|
||||||
|
'setThemeClass',
|
||||||
|
'setEngineTheme',
|
||||||
|
'setClientSideSceneUnits',
|
||||||
|
'Execute AST',
|
||||||
|
'setClientTheme',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
'set.modeling.showScaleGrid': {
|
||||||
|
target: 'persisting settings',
|
||||||
|
actions: [
|
||||||
|
'setSettingAtLevel',
|
||||||
|
'toastSuccess',
|
||||||
|
'setEngineScaleGridVisibility',
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
|
||||||
)
|
'persisting settings': {
|
||||||
|
entry: ['persistSettings'],
|
||||||
|
always: 'idle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
42
src/main.ts
42
src/main.ts
@ -19,6 +19,7 @@ import electronUpdater, { type AppUpdater } from 'electron-updater'
|
|||||||
import minimist from 'minimist'
|
import minimist from 'minimist'
|
||||||
import getCurrentProjectFile from 'lib/getCurrentProjectFile'
|
import getCurrentProjectFile from 'lib/getCurrentProjectFile'
|
||||||
import os from 'node:os'
|
import os from 'node:os'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null
|
let mainWindow: BrowserWindow | null = null
|
||||||
|
|
||||||
@ -87,28 +88,30 @@ const createWindow = (filePath?: string): BrowserWindow => {
|
|||||||
|
|
||||||
// and load the index.html of the app.
|
// and load the index.html of the app.
|
||||||
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
||||||
newWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL)
|
newWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL).catch(reportRejection)
|
||||||
} else {
|
} else {
|
||||||
getProjectPathAtStartup(filePath).then((projectPath) => {
|
getProjectPathAtStartup(filePath)
|
||||||
const startIndex = path.join(
|
.then(async (projectPath) => {
|
||||||
__dirname,
|
const startIndex = path.join(
|
||||||
`../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`
|
__dirname,
|
||||||
)
|
`../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`
|
||||||
|
)
|
||||||
|
|
||||||
if (projectPath === null) {
|
if (projectPath === null) {
|
||||||
newWindow.loadFile(startIndex)
|
await newWindow.loadFile(startIndex)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Loading file', projectPath)
|
console.log('Loading file', projectPath)
|
||||||
|
|
||||||
const fullUrl = `/file/${encodeURIComponent(projectPath)}`
|
const fullUrl = `/file/${encodeURIComponent(projectPath)}`
|
||||||
console.log('Full URL', fullUrl)
|
console.log('Full URL', fullUrl)
|
||||||
|
|
||||||
newWindow.loadFile(startIndex, {
|
await newWindow.loadFile(startIndex, {
|
||||||
hash: fullUrl,
|
hash: fullUrl,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
.catch(reportRejection)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open the DevTools.
|
// Open the DevTools.
|
||||||
@ -175,6 +178,7 @@ ipcMain.handle('login', async (event, host) => {
|
|||||||
|
|
||||||
const handle = await client.deviceAuthorization()
|
const handle = await client.deviceAuthorization()
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
shell.openExternal(handle.verification_uri_complete)
|
shell.openExternal(handle.verification_uri_complete)
|
||||||
|
|
||||||
// Wait for the user to login.
|
// Wait for the user to login.
|
||||||
@ -241,12 +245,12 @@ export async function checkForUpdates(autoUpdater: AppUpdater) {
|
|||||||
console.log(result)
|
console.log(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.on('ready', async () => {
|
app.on('ready', () => {
|
||||||
const autoUpdater = getAutoUpdater()
|
const autoUpdater = getAutoUpdater()
|
||||||
checkForUpdates(autoUpdater)
|
checkForUpdates(autoUpdater).catch(reportRejection)
|
||||||
const fifteenMinutes = 15 * 60 * 1000
|
const fifteenMinutes = 15 * 60 * 1000
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
checkForUpdates(autoUpdater)
|
checkForUpdates(autoUpdater).catch(reportRejection)
|
||||||
}, fifteenMinutes)
|
}, fifteenMinutes)
|
||||||
|
|
||||||
autoUpdater.on('update-available', (info) => {
|
autoUpdater.on('update-available', (info) => {
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
import { ReportHandler } from 'web-vitals'
|
import { ReportHandler } from 'web-vitals'
|
||||||
|
|
||||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
import('web-vitals')
|
||||||
getCLS(onPerfEntry)
|
.then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||||
getFID(onPerfEntry)
|
getCLS(onPerfEntry)
|
||||||
getFCP(onPerfEntry)
|
getFID(onPerfEntry)
|
||||||
getLCP(onPerfEntry)
|
getFCP(onPerfEntry)
|
||||||
getTTFB(onPerfEntry)
|
getLCP(onPerfEntry)
|
||||||
})
|
getTTFB(onPerfEntry)
|
||||||
|
})
|
||||||
|
.catch(reportRejection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ import { type HomeLoaderData } from 'lib/types'
|
|||||||
import Loading from 'components/Loading'
|
import Loading from 'components/Loading'
|
||||||
import { useMachine } from '@xstate/react'
|
import { useMachine } from '@xstate/react'
|
||||||
import { homeMachine } from '../machines/homeMachine'
|
import { homeMachine } from '../machines/homeMachine'
|
||||||
import { ContextFrom, EventFrom } from 'xstate'
|
import { fromPromise } from 'xstate'
|
||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
import {
|
import {
|
||||||
getNextSearchParams,
|
getNextSearchParams,
|
||||||
@ -68,95 +68,102 @@ const Home = () => {
|
|||||||
)
|
)
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const [state, send, actor] = useMachine(homeMachine, {
|
const [state, send, actor] = useMachine(
|
||||||
context: {
|
homeMachine.provide({
|
||||||
projects: loadedProjects,
|
actions: {
|
||||||
defaultProjectName: settings.projects.defaultProjectName.current,
|
navigateToProject: ({ context, event }) => {
|
||||||
defaultDirectory: settings.app.projectDirectory.current,
|
if ('data' in event && event.data && 'name' in event.data) {
|
||||||
},
|
let projectPath =
|
||||||
actions: {
|
context.defaultDirectory +
|
||||||
navigateToProject: (
|
window.electron.path.sep +
|
||||||
context: ContextFrom<typeof homeMachine>,
|
event.data.name
|
||||||
event: EventFrom<typeof homeMachine>
|
onProjectOpen(
|
||||||
) => {
|
{
|
||||||
if (event.data && 'name' in event.data) {
|
name: event.data.name,
|
||||||
let projectPath =
|
path: projectPath,
|
||||||
context.defaultDirectory +
|
},
|
||||||
window.electron.path.sep +
|
null
|
||||||
event.data.name
|
)
|
||||||
onProjectOpen(
|
commandBarSend({ type: 'Close' })
|
||||||
{
|
navigate(`${PATHS.FILE}/${encodeURIComponent(projectPath)}`)
|
||||||
name: event.data.name,
|
|
||||||
path: projectPath,
|
|
||||||
},
|
|
||||||
null
|
|
||||||
)
|
|
||||||
commandBarSend({ type: 'Close' })
|
|
||||||
navigate(`${PATHS.FILE}/${encodeURIComponent(projectPath)}`)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toastSuccess: (_, event) => toast.success((event.data || '') + ''),
|
|
||||||
toastError: (_, event) => toast.error((event.data || '') + ''),
|
|
||||||
},
|
|
||||||
services: {
|
|
||||||
readProjects: async (context: ContextFrom<typeof homeMachine>) =>
|
|
||||||
listProjects(),
|
|
||||||
createProject: async (
|
|
||||||
context: ContextFrom<typeof homeMachine>,
|
|
||||||
event: EventFrom<typeof homeMachine, 'Create project'>
|
|
||||||
) => {
|
|
||||||
let name = (
|
|
||||||
event.data && 'name' in event.data
|
|
||||||
? event.data.name
|
|
||||||
: settings.projects.defaultProjectName.current
|
|
||||||
).trim()
|
|
||||||
|
|
||||||
if (doesProjectNameNeedInterpolated(name)) {
|
|
||||||
const nextIndex = getNextProjectIndex(name, projects)
|
|
||||||
name = interpolateProjectNameWithIndex(name, nextIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
await createNewProjectDirectory(name)
|
|
||||||
|
|
||||||
return `Successfully created "${name}"`
|
|
||||||
},
|
|
||||||
renameProject: async (
|
|
||||||
context: ContextFrom<typeof homeMachine>,
|
|
||||||
event: EventFrom<typeof homeMachine, 'Rename project'>
|
|
||||||
) => {
|
|
||||||
const { oldName, newName } = event.data
|
|
||||||
let name = newName ? newName : context.defaultProjectName
|
|
||||||
if (doesProjectNameNeedInterpolated(name)) {
|
|
||||||
const nextIndex = await getNextProjectIndex(name, projects)
|
|
||||||
name = interpolateProjectNameWithIndex(name, nextIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
await renameProjectDirectory(
|
|
||||||
window.electron.path.join(context.defaultDirectory, oldName),
|
|
||||||
name
|
|
||||||
)
|
|
||||||
return `Successfully renamed "${oldName}" to "${name}"`
|
|
||||||
},
|
|
||||||
deleteProject: async (
|
|
||||||
context: ContextFrom<typeof homeMachine>,
|
|
||||||
event: EventFrom<typeof homeMachine, 'Delete project'>
|
|
||||||
) => {
|
|
||||||
await window.electron.rm(
|
|
||||||
window.electron.path.join(context.defaultDirectory, event.data.name),
|
|
||||||
{
|
|
||||||
recursive: true,
|
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
return `Successfully deleted "${event.data.name}"`
|
toastSuccess: ({ event }) =>
|
||||||
|
toast.success(
|
||||||
|
('data' in event && typeof event.data === 'string' && event.data) ||
|
||||||
|
('output' in event &&
|
||||||
|
typeof event.output === 'string' &&
|
||||||
|
event.output) ||
|
||||||
|
''
|
||||||
|
),
|
||||||
|
toastError: ({ event }) =>
|
||||||
|
toast.error(
|
||||||
|
('data' in event && typeof event.data === 'string' && event.data) ||
|
||||||
|
('output' in event &&
|
||||||
|
typeof event.output === 'string' &&
|
||||||
|
event.output) ||
|
||||||
|
''
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
actors: {
|
||||||
guards: {
|
readProjects: fromPromise(() => listProjects()),
|
||||||
'Has at least 1 project': (_, event: EventFrom<typeof homeMachine>) => {
|
createProject: fromPromise(async ({ input }) => {
|
||||||
if (event.type !== 'done.invoke.read-projects') return false
|
let name = (
|
||||||
return event?.data?.length ? event.data?.length >= 1 : false
|
input && 'name' in input && input.name
|
||||||
|
? input.name
|
||||||
|
: settings.projects.defaultProjectName.current
|
||||||
|
).trim()
|
||||||
|
|
||||||
|
if (doesProjectNameNeedInterpolated(name)) {
|
||||||
|
const nextIndex = getNextProjectIndex(name, projects)
|
||||||
|
name = interpolateProjectNameWithIndex(name, nextIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
await createNewProjectDirectory(name)
|
||||||
|
|
||||||
|
return `Successfully created "${name}"`
|
||||||
|
}),
|
||||||
|
renameProject: fromPromise(async ({ input }) => {
|
||||||
|
const { oldName, newName, defaultProjectName, defaultDirectory } =
|
||||||
|
input
|
||||||
|
let name = newName ? newName : defaultProjectName
|
||||||
|
if (doesProjectNameNeedInterpolated(name)) {
|
||||||
|
const nextIndex = await getNextProjectIndex(name, projects)
|
||||||
|
name = interpolateProjectNameWithIndex(name, nextIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
await renameProjectDirectory(
|
||||||
|
window.electron.path.join(defaultDirectory, oldName),
|
||||||
|
name
|
||||||
|
)
|
||||||
|
return `Successfully renamed "${oldName}" to "${name}"`
|
||||||
|
}),
|
||||||
|
deleteProject: fromPromise(async ({ input }) => {
|
||||||
|
await window.electron.rm(
|
||||||
|
window.electron.path.join(input.defaultDirectory, input.name),
|
||||||
|
{
|
||||||
|
recursive: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return `Successfully deleted "${input.name}"`
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
guards: {
|
||||||
})
|
'Has at least 1 project': ({ event }) => {
|
||||||
|
if (event.type !== 'xstate.done.actor.read-projects') return false
|
||||||
|
console.log(`from has at least 1 project: ${event.output.length}`)
|
||||||
|
return event.output.length ? event.output.length >= 1 : false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
projects: loadedProjects,
|
||||||
|
defaultProjectName: settings.projects.defaultProjectName.current,
|
||||||
|
defaultDirectory: settings.app.projectDirectory.current,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
const { projects } = state.context
|
const { projects } = state.context
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
const { searchResults, query, setQuery } = useProjectSearch(projects)
|
const { searchResults, query, setQuery } = useProjectSearch(projects)
|
||||||
@ -197,14 +204,18 @@ const Home = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (newProjectName !== project.name) {
|
if (newProjectName !== project.name) {
|
||||||
send('Rename project', {
|
send({
|
||||||
data: { oldName: project.name, newName: newProjectName },
|
type: 'Rename project',
|
||||||
|
data: { oldName: project.name, newName: newProjectName as string },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteProject(project: Project) {
|
async function handleDeleteProject(project: Project) {
|
||||||
send('Delete project', { data: { name: project.name || '' } })
|
send({
|
||||||
|
type: 'Delete project',
|
||||||
|
data: { name: project.name || '' },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -217,7 +228,9 @@ const Home = () => {
|
|||||||
<h1 className="text-3xl font-bold">Your Projects</h1>
|
<h1 className="text-3xl font-bold">Your Projects</h1>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={() => send('Create project')}
|
onClick={() =>
|
||||||
|
send({ type: 'Create project', data: { name: '' } })
|
||||||
|
}
|
||||||
className="group !bg-primary !text-chalkboard-10 !border-primary hover:shadow-inner hover:hue-rotate-15"
|
className="group !bg-primary !text-chalkboard-10 !border-primary hover:shadow-inner hover:hue-rotate-15"
|
||||||
iconStart={{
|
iconStart={{
|
||||||
icon: 'plus',
|
icon: 'plus',
|
||||||
|
@ -13,6 +13,7 @@ export default function FutureWork() {
|
|||||||
useDemoCode()
|
useDemoCode()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
send({ type: 'Cancel' }) // in case the user hit 'Next' while still in sketch mode
|
send({ type: 'Cancel' }) // in case the user hit 'Next' while still in sketch mode
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
sceneInfra.camControls.resetCameraPosition()
|
sceneInfra.camControls.resetCameraPosition()
|
||||||
}, [send])
|
}, [send])
|
||||||
|
|
||||||
|
@ -13,6 +13,8 @@ import { IndexLoaderData } from 'lib/types'
|
|||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
import { useFileContext } from 'hooks/useFileContext'
|
import { useFileContext } from 'hooks/useFileContext'
|
||||||
import { useLspContext } from 'components/LspProvider'
|
import { useLspContext } from 'components/LspProvider'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
import { toSync } from 'lib/utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show either a welcome screen or a warning screen
|
* Show either a welcome screen or a warning screen
|
||||||
@ -80,7 +82,7 @@ function OnboardingWarningDesktop(props: OnboardingResetWarningProps) {
|
|||||||
<OnboardingButtons
|
<OnboardingButtons
|
||||||
className="mt-6"
|
className="mt-6"
|
||||||
dismiss={dismiss}
|
dismiss={dismiss}
|
||||||
next={onAccept}
|
next={toSync(onAccept, reportRejection)}
|
||||||
nextText="Make a new project"
|
nextText="Make a new project"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@ -102,14 +104,14 @@ function OnboardingWarningWeb(props: OnboardingResetWarningProps) {
|
|||||||
<OnboardingButtons
|
<OnboardingButtons
|
||||||
className="mt-6"
|
className="mt-6"
|
||||||
dismiss={dismiss}
|
dismiss={dismiss}
|
||||||
next={async () => {
|
next={toSync(async () => {
|
||||||
// We do want to update both the state and editor here.
|
// We do want to update both the state and editor here.
|
||||||
codeManager.updateCodeStateEditor(bracket)
|
codeManager.updateCodeStateEditor(bracket)
|
||||||
await codeManager.writeToFile()
|
await codeManager.writeToFile()
|
||||||
|
|
||||||
await kclManager.executeCode(true)
|
await kclManager.executeCode(true)
|
||||||
props.setShouldShowWarning(false)
|
props.setShouldShowWarning(false)
|
||||||
}}
|
}, reportRejection)}
|
||||||
nextText="Overwrite code and continue"
|
nextText="Overwrite code and continue"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
@ -16,6 +16,7 @@ export default function Sketching() {
|
|||||||
await kclManager.executeCode(true)
|
await kclManager.executeCode(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
clearEditor()
|
clearEditor()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
@ -21,6 +21,8 @@ import { ActionButton } from 'components/ActionButton'
|
|||||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||||
import { codeManager, editorManager, kclManager } from 'lib/singletons'
|
import { codeManager, editorManager, kclManager } from 'lib/singletons'
|
||||||
import { bracket } from 'lib/exampleKcl'
|
import { bracket } from 'lib/exampleKcl'
|
||||||
|
import { toSync } from 'lib/utils'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
export const kbdClasses =
|
export const kbdClasses =
|
||||||
'py-0.5 px-1 text-sm rounded bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50 border-b-2'
|
'py-0.5 px-1 text-sm rounded bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50 border-b-2'
|
||||||
@ -80,11 +82,13 @@ export const onboardingRoutes = [
|
|||||||
export function useDemoCode() {
|
export function useDemoCode() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editorManager.editorView || codeManager.code === bracket) return
|
if (!editorManager.editorView || codeManager.code === bracket) return
|
||||||
setTimeout(async () => {
|
setTimeout(
|
||||||
codeManager.updateCodeStateEditor(bracket)
|
toSync(async () => {
|
||||||
await kclManager.executeCode(true)
|
codeManager.updateCodeStateEditor(bracket)
|
||||||
await codeManager.writeToFile()
|
await kclManager.executeCode(true)
|
||||||
})
|
await codeManager.writeToFile()
|
||||||
|
}, reportRejection)
|
||||||
|
)
|
||||||
}, [editorManager.editorView])
|
}, [editorManager.editorView])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,8 @@ import { CustomIcon } from 'components/CustomIcon'
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { APP_VERSION } from './Settings'
|
import { APP_VERSION } from './Settings'
|
||||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
|
import { toSync } from 'lib/utils'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
const subtleBorder =
|
const subtleBorder =
|
||||||
'border border-solid border-chalkboard-30 dark:border-chalkboard-80'
|
'border border-solid border-chalkboard-30 dark:border-chalkboard-80'
|
||||||
@ -104,7 +106,7 @@ const SignIn = () => {
|
|||||||
</p>
|
</p>
|
||||||
{isDesktop() ? (
|
{isDesktop() ? (
|
||||||
<button
|
<button
|
||||||
onClick={signInDesktop}
|
onClick={toSync(signInDesktop, reportRejection)}
|
||||||
className={
|
className={
|
||||||
'm-0 mt-8 flex gap-4 items-center px-3 py-1 ' +
|
'm-0 mt-8 flex gap-4 items-center px-3 py-1 ' +
|
||||||
'!border-transparent !text-lg !text-chalkboard-10 !bg-primary hover:hue-rotate-15'
|
'!border-transparent !text-lg !text-chalkboard-10 !bg-primary hover:hue-rotate-15'
|
||||||
|
50
yarn.lock
50
yarn.lock
@ -2952,13 +2952,13 @@
|
|||||||
"@babel/types" "^7.21.4"
|
"@babel/types" "^7.21.4"
|
||||||
recast "^0.23.1"
|
recast "^0.23.1"
|
||||||
|
|
||||||
"@xstate/react@^3.2.2":
|
"@xstate/react@^4.1.1":
|
||||||
version "3.2.2"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@xstate/react/-/react-3.2.2.tgz#ddf0f9d75e2c19375b1e1b7335e72cb99762aed8"
|
resolved "https://registry.yarnpkg.com/@xstate/react/-/react-4.1.1.tgz#2f580fc5f83d195f95b56df6cd8061c66660d9fa"
|
||||||
integrity sha512-feghXWLedyq8JeL13yda3XnHPZKwYDN5HPBLykpLeuNpr9178tQd2/3d0NrH6gSd0sG5mLuLeuD+ck830fgzLQ==
|
integrity sha512-pFp/Y+bnczfaZ0V8B4LOhx3d6Gd71YKAPbzerGqydC2nsYN/mp7RZu3q/w6/kvI2hwR/jeDeetM7xc3JFZH2NA==
|
||||||
dependencies:
|
dependencies:
|
||||||
use-isomorphic-layout-effect "^1.1.2"
|
use-isomorphic-layout-effect "^1.1.2"
|
||||||
use-sync-external-store "^1.0.0"
|
use-sync-external-store "^1.2.0"
|
||||||
|
|
||||||
"@xstate/tools-shared@^4.1.0":
|
"@xstate/tools-shared@^4.1.0":
|
||||||
version "4.1.0"
|
version "4.1.0"
|
||||||
@ -8757,16 +8757,7 @@ string-natural-compare@^3.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
||||||
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0":
|
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||||
version "4.2.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
|
||||||
dependencies:
|
|
||||||
emoji-regex "^8.0.0"
|
|
||||||
is-fullwidth-code-point "^3.0.0"
|
|
||||||
strip-ansi "^6.0.1"
|
|
||||||
|
|
||||||
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
@ -8860,14 +8851,7 @@ string_decoder@~1.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer "~5.1.0"
|
safe-buffer "~5.1.0"
|
||||||
|
|
||||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||||
version "6.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
|
||||||
dependencies:
|
|
||||||
ansi-regex "^5.0.1"
|
|
||||||
|
|
||||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
@ -9412,7 +9396,7 @@ use-latest@^1.2.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
use-isomorphic-layout-effect "^1.1.1"
|
use-isomorphic-layout-effect "^1.1.1"
|
||||||
|
|
||||||
use-sync-external-store@^1.0.0:
|
use-sync-external-store@^1.2.0:
|
||||||
version "1.2.2"
|
version "1.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9"
|
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9"
|
||||||
integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==
|
integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==
|
||||||
@ -9741,16 +9725,7 @@ word-wrap@^1.2.3, word-wrap@^1.2.5:
|
|||||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
||||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||||
|
|
||||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||||
version "7.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
|
||||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
|
||||||
dependencies:
|
|
||||||
ansi-styles "^4.0.0"
|
|
||||||
string-width "^4.1.0"
|
|
||||||
strip-ansi "^6.0.0"
|
|
||||||
|
|
||||||
wrap-ansi@^7.0.0:
|
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||||
@ -9793,11 +9768,16 @@ xmlbuilder@>=11.0.1, xmlbuilder@^15.1.1:
|
|||||||
resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.0.0-beta.54.tgz#d80f1a9e43ad883a65fc9b399161bd39633bd9bf"
|
resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.0.0-beta.54.tgz#d80f1a9e43ad883a65fc9b399161bd39633bd9bf"
|
||||||
integrity sha512-BTnCPBQ2iTKe4uCnHEe1hNx6VTbXU+5mQGybSQHOjTLiBi4Ryi+tL9T6N1tmqagvM8rfl4XRfvndogfWCWcdpw==
|
integrity sha512-BTnCPBQ2iTKe4uCnHEe1hNx6VTbXU+5mQGybSQHOjTLiBi4Ryi+tL9T6N1tmqagvM8rfl4XRfvndogfWCWcdpw==
|
||||||
|
|
||||||
xstate@^4.33.4, xstate@^4.38.2:
|
xstate@^4.33.4:
|
||||||
version "4.38.3"
|
version "4.38.3"
|
||||||
resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.3.tgz#4e15e7ad3aa0ca1eea2010548a5379966d8f1075"
|
resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.3.tgz#4e15e7ad3aa0ca1eea2010548a5379966d8f1075"
|
||||||
integrity sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw==
|
integrity sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw==
|
||||||
|
|
||||||
|
xstate@^5.17.4:
|
||||||
|
version "5.17.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.17.4.tgz#334ab2da123973634097f7ca48387ae1589c774e"
|
||||||
|
integrity sha512-KM2FYVOUJ04HlOO4TY3wEXqoYPR/XsDu+ewm+IWw0vilXqND0jVfvv04tEFwp8Mkk7I/oHXM8t1Ex9xJyUS4ZA==
|
||||||
|
|
||||||
xterm-addon-fit@^0.5.0:
|
xterm-addon-fit@^0.5.0:
|
||||||
version "0.5.0"
|
version "0.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz#2d51b983b786a97dcd6cde805e700c7f913bc596"
|
resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz#2d51b983b786a97dcd6cde805e700c7f913bc596"
|
||||||
|
Reference in New Issue
Block a user