Compare commits

...

6 Commits

Author SHA1 Message Date
0604da9c37 Update 2020 tests 2024-07-13 19:45:15 -05:00
29bf77bb82 Show descriptions for generated commands, make them look better and sort better (#3023) 2024-07-12 17:48:38 -04:00
e81b614523 Lf94/save settings between reconnects (#2997)
* Keep settings between reconnects

* Set idle timeout to 2 minutes

* Put idle behind flags

* Remove pauses

* Fix online->offline->online

* Revert "Remove pauses"

This reverts commit 267ef4ff4b86f2d8014bfb2a8e8a633adc8001dc.

* ci

* call correct setmediastream
2024-07-12 20:42:23 +00:00
5a5fe3bb95 Add sketch tools back to the command bar (#3008)
* Make machine command type names more explicit

* Prepare "change tool" event for command bar

* Make it so that state machine events can each map to multiple command configs

* Make commands with all skippable args possible

* Add back the tools to the command bar

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Update to use new `groupId` property name

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Oops didn't save this other instance of `ownerMachine`

* Add a playwright test

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-12 16:16:26 -04:00
0710f6e5f2 Add format code to the command palette (#3001)
* Add format code to the command palette

* Fix to use renamed groupId parameter

* Add icon to format code command

* Fix to remove commands during teardown

* Fix dependencies

* Change formatting
2024-07-12 17:08:01 +00:00
c9d5633647 Bump thiserror from 1.0.61 to 1.0.62 in /src/wasm-lib (#3016)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.61 to 1.0.62.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.61...1.0.62)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-12 09:11:49 -07:00
144 changed files with 674 additions and 329 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

View File

@ -3691,6 +3691,46 @@ const extrude001 = extrude(distance001, sketch001)`.replace(
) // remove newlines
)
})
test('Can switch between sketch tools via command bar', async ({ page }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
const sketchButton = page.getByRole('button', { name: 'Start Sketch' })
const cmdBarButton = page.getByRole('button', { name: 'Commands' })
const rectangleToolCommand = page.getByRole('option', {
name: 'Rectangle',
})
const rectangleToolButton = page.getByRole('button', { name: 'Rectangle' })
const lineToolCommand = page.getByRole('option', { name: 'Line' })
const lineToolButton = page.getByRole('button', { name: 'Line' })
const arcToolCommand = page.getByRole('option', { name: 'Tangential Arc' })
const arcToolButton = page.getByRole('button', { name: 'Tangential Arc' })
// Start a sketch
await sketchButton.click()
await page.mouse.click(700, 200)
// Switch between sketch tools via the command bar
await expect(lineToolButton).toHaveAttribute('aria-pressed', 'true')
await cmdBarButton.click()
await rectangleToolCommand.click()
await expect(rectangleToolButton).toHaveAttribute('aria-pressed', 'true')
await cmdBarButton.click()
await lineToolCommand.click()
await expect(lineToolButton).toHaveAttribute('aria-pressed', 'true')
// Click in the scene a couple times to draw a line
// so tangential arc is valid
await page.mouse.click(700, 200)
await page.mouse.click(700, 300)
// switch to tangential arc via command bar
await cmdBarButton.click()
await arcToolCommand.click()
await expect(arcToolButton).toHaveAttribute('aria-pressed', 'true')
})
})
test.describe('Regression tests', () => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -60,7 +60,7 @@ export function Toolbar({
? send('CancelSketch')
: send({
type: 'change tool',
data: 'line',
data: { tool: 'line' },
}),
{ enabled: !disableLineButton, scopes: ['sketch'] }
)
@ -75,7 +75,7 @@ export function Toolbar({
? send('CancelSketch')
: send({
type: 'change tool',
data: 'tangentialArc',
data: { tool: 'tangentialArc' },
}),
{ enabled: !disableTangentialArc, scopes: ['sketch'] }
)
@ -89,7 +89,7 @@ export function Toolbar({
? send('CancelSketch')
: send({
type: 'change tool',
data: 'rectangle',
data: { tool: 'rectangle' },
}),
{ enabled: !disableRectangle, scopes: ['sketch'] }
)
@ -263,7 +263,7 @@ export function Toolbar({
? send('CancelSketch')
: send({
type: 'change tool',
data: 'line',
data: { tool: 'line' },
})
}
aria-pressed={state?.matches('Sketch.Line tool')}
@ -293,7 +293,7 @@ export function Toolbar({
? send('CancelSketch')
: send({
type: 'change tool',
data: 'tangentialArc',
data: { tool: 'tangentialArc' },
})
}
aria-pressed={state.matches('Sketch.Tangential arc to')}
@ -323,7 +323,7 @@ export function Toolbar({
? send('CancelSketch')
: send({
type: 'change tool',
data: 'rectangle',
data: { tool: 'rectangle' },
})
}
aria-pressed={state.matches('Sketch.Rectangle tool')}

View File

@ -28,6 +28,11 @@ export const CommandBarProvider = ({
Object.keys(context.selectedCommand?.args).length === 0
)
},
'All arguments are skippable': (context, _event) => {
return Object.values(context.selectedCommand!.args!).every(
(argConfig) => argConfig.skip
)
},
},
})

View File

@ -70,19 +70,23 @@ function CommandComboBox({
>
{filteredOptions?.map((option) => (
<Combobox.Option
key={option.name}
key={option.groupId + option.name + (option.displayName || '')}
value={option}
className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90"
className="flex items-center gap-4 px-4 py-1.5 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90"
>
{'icon' in option && option.icon && (
<CustomIcon name={option.icon} className="w-5 h-5" />
)}
<p className="flex-grow">{option.displayName || option.name} </p>
{option.description && (
<p className="text-xs text-chalkboard-60 dark:text-chalkboard-40">
{option.description}
<div className="flex-grow flex flex-col">
<p className="my-0 leading-tight">
{option.displayName || option.name}{' '}
</p>
)}
{option.description && (
<p className="my-0 text-xs text-chalkboard-60 dark:text-chalkboard-50">
{option.description}
</p>
)}
</div>
</Combobox.Option>
))}
</Combobox.Options>

View File

@ -131,6 +131,16 @@ const CustomIconMap = {
/>
</svg>
),
code: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.7071 5L7.77734 14.7794L8.73527 15.0663L11.665 5.28698L10.7071 5ZM2.35356 9.64644L5.85362 6.14644L6.56072 6.85355L3.41423 10L6.56072 13.1464L5.85362 13.8536L2.35356 10.3536L2 10L2.35356 9.64644ZM17.0607 9.64644L13.5607 6.14644L12.8536 6.85355L16 10L12.8536 13.1464L13.5607 13.8535L17.0607 10.3536L17.4142 10L17.0607 9.64644Z"
fill="currentColor"
/>
</svg>
),
dimension: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path

View File

@ -42,7 +42,7 @@ import {
import { applyConstraintIntersect } from './Toolbar/Intersect'
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
import useStateMachineCommands from 'hooks/useStateMachineCommands'
import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConfig'
import { modelingMachineCommandConfig } from 'lib/commandBarConfigs/modelingCommandConfig'
import {
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
@ -920,7 +920,7 @@ export const ModelingMachineProvider = ({
state: modelingState,
send: modelingSend,
actor: modelingActor,
commandBarConfig: modelingMachineConfig,
commandBarConfig: modelingMachineCommandConfig,
allCommandsRequireNetwork: true,
// TODO for when sketch tools are in the toolbar: This was added when we used one "Cancel" event,
// but we need to support "SketchCancel" and basically

View File

@ -1,3 +1,4 @@
import { DEV } from 'env'
import { MouseEventHandler, useEffect, useRef, useState } from 'react'
import { getNormalisedCoordinates } from '../lib/utils'
import Loading from './Loading'
@ -22,6 +23,8 @@ export const Stream = () => {
const { overallState } = useNetworkContext()
const [isFreezeFrame, setIsFreezeFrame] = useState(false)
const IDLE = true
const isNetworkOkay =
overallState === NetworkHealthState.Ok ||
overallState === NetworkHealthState.Weak
@ -53,7 +56,7 @@ export const Stream = () => {
capture: true,
})
const IDLE_TIME_MS = 1000 * 20
const IDLE_TIME_MS = 1000 * 60 * 2
let timeoutIdIdleA: ReturnType<typeof setTimeout> | undefined = undefined
const teardown = () => {
@ -62,19 +65,21 @@ export const Stream = () => {
sceneInfra.modelingSend({ type: 'Cancel' })
// Give video time to pause
window.requestAnimationFrame(() => {
engineCommandManager.engineConnection?.tearDown({ freeze: true })
engineCommandManager.tearDown()
})
}
// Teardown everything if we go hidden or reconnect
if (globalThis?.window?.document) {
globalThis.window.document.onvisibilitychange = () => {
if (globalThis.window.document.visibilityState === 'hidden') {
clearTimeout(timeoutIdIdleA)
timeoutIdIdleA = setTimeout(teardown, IDLE_TIME_MS)
} else if (!engineCommandManager.engineConnection?.isReady()) {
clearTimeout(timeoutIdIdleA)
engineCommandManager.engineConnection?.connect(true)
if (IDLE && DEV) {
if (globalThis?.window?.document) {
globalThis.window.document.onvisibilitychange = () => {
if (globalThis.window.document.visibilityState === 'hidden') {
clearTimeout(timeoutIdIdleA)
timeoutIdIdleA = setTimeout(teardown, IDLE_TIME_MS)
} else if (!engineCommandManager.engineConnection?.isReady()) {
clearTimeout(timeoutIdIdleA)
engineCommandManager.engineConnection?.connect(true)
}
}
}
}
@ -82,35 +87,44 @@ export const Stream = () => {
let timeoutIdIdleB: ReturnType<typeof setTimeout> | undefined = undefined
const onAnyInput = () => {
if (!engineCommandManager.engineConnection?.isReady()) {
engineCommandManager.engineConnection?.connect(true)
}
// Clear both timers
clearTimeout(timeoutIdIdleA)
clearTimeout(timeoutIdIdleB)
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
}
globalThis?.window?.document?.addEventListener('keydown', onAnyInput)
globalThis?.window?.document?.addEventListener('mousemove', onAnyInput)
globalThis?.window?.document?.addEventListener('mousedown', onAnyInput)
globalThis?.window?.document?.addEventListener('scroll', onAnyInput)
globalThis?.window?.document?.addEventListener('touchstart', onAnyInput)
if (IDLE && DEV) {
globalThis?.window?.document?.addEventListener('keydown', onAnyInput)
globalThis?.window?.document?.addEventListener('mousemove', onAnyInput)
globalThis?.window?.document?.addEventListener('mousedown', onAnyInput)
globalThis?.window?.document?.addEventListener('scroll', onAnyInput)
globalThis?.window?.document?.addEventListener('touchstart', onAnyInput)
}
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
if (IDLE && DEV) {
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
}
return () => {
globalThis?.window?.document?.removeEventListener('paste', handlePaste, {
capture: true,
})
globalThis?.window?.document?.removeEventListener('keydown', onAnyInput)
globalThis?.window?.document?.removeEventListener('mousemove', onAnyInput)
globalThis?.window?.document?.removeEventListener('mousedown', onAnyInput)
globalThis?.window?.document?.removeEventListener('scroll', onAnyInput)
globalThis?.window?.document?.removeEventListener(
'touchstart',
onAnyInput
)
if (IDLE && DEV) {
globalThis?.window?.document?.removeEventListener('keydown', onAnyInput)
globalThis?.window?.document?.removeEventListener(
'mousemove',
onAnyInput
)
globalThis?.window?.document?.removeEventListener(
'mousedown',
onAnyInput
)
globalThis?.window?.document?.removeEventListener('scroll', onAnyInput)
globalThis?.window?.document?.removeEventListener(
'touchstart',
onAnyInput
)
}
}
}, [])

View File

@ -1,4 +1,4 @@
import { useLayoutEffect, useEffect, useRef } from 'react'
import { useLayoutEffect, useEffect, useRef, useState } from 'react'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { deferExecution } from 'lib/utils'
import { Themes } from 'lib/theme'
@ -30,9 +30,6 @@ export function useSetupEngineManager(
const { setAppState } = useAppState()
const { setMediaStream } = useAppStream()
const streamWidth = streamRef?.current?.offsetWidth
const streamHeight = streamRef?.current?.offsetHeight
const hasSetNonZeroDimensions = useRef<boolean>(false)
if (settings.pool) {
@ -41,55 +38,60 @@ export function useSetupEngineManager(
engineCommandManager.pool = settings.pool
}
const startEngineInstance = () => {
const startEngineInstance = (restart: boolean = false) => {
// Load the engine command manager once with the initial width and height,
// then we do not want to reload it.
const { width: quadWidth, height: quadHeight } = getDimensions(
streamWidth,
streamHeight
streamRef?.current?.offsetWidth ?? 0,
streamRef?.current?.offsetHeight ?? 0
)
if (
!hasSetNonZeroDimensions.current &&
quadHeight &&
quadWidth &&
settings.modelingSend
) {
engineCommandManager.start({
setMediaStream: setMediaStream,
setIsStreamReady: (isStreamReady) => setAppState({ isStreamReady }),
width: quadWidth,
height: quadHeight,
executeCode: () => {
// We only want to execute the code here that we already have set.
// Nothing else.
kclManager.isFirstRender = true
return kclManager.executeCode(true, true).then(() => {
kclManager.isFirstRender = false
})
},
token,
settings,
makeDefaultPlanes: () => {
return makeDefaultPlanes(kclManager.engineCommandManager)
},
modifyGrid: (hidden: boolean) => {
return modifyGrid(kclManager.engineCommandManager, hidden)
},
})
settings.modelingSend({
type: 'Set context',
data: {
streamDimensions: {
streamWidth: quadWidth,
streamHeight: quadHeight,
},
},
})
hasSetNonZeroDimensions.current = true
if (restart) {
kclManager.isFirstRender = false
}
engineCommandManager.start({
restart,
setMediaStream: (mediaStream) => setMediaStream(mediaStream),
setIsStreamReady: (isStreamReady) => setAppState({ isStreamReady }),
width: quadWidth,
height: quadHeight,
executeCode: () => {
// We only want to execute the code here that we already have set.
// Nothing else.
kclManager.isFirstRender = true
return kclManager.executeCode(true, true).then(() => {
kclManager.isFirstRender = false
})
},
token,
settings,
makeDefaultPlanes: () => {
return makeDefaultPlanes(kclManager.engineCommandManager)
},
modifyGrid: (hidden: boolean) => {
return modifyGrid(kclManager.engineCommandManager, hidden)
},
})
settings.modelingSend({
type: 'Set context',
data: {
streamDimensions: {
streamWidth: quadWidth,
streamHeight: quadHeight,
},
},
})
hasSetNonZeroDimensions.current = true
}
useLayoutEffect(startEngineInstance, [
useLayoutEffect(() => {
const { width: quadWidth, height: quadHeight } = getDimensions(
streamRef?.current?.offsetWidth ?? 0,
streamRef?.current?.offsetHeight ?? 0
)
if (!hasSetNonZeroDimensions.current && quadHeight && quadWidth) {
startEngineInstance()
}
}, [
streamRef?.current?.offsetWidth,
streamRef?.current?.offsetHeight,
settings.modelingSend,
@ -98,8 +100,8 @@ export function useSetupEngineManager(
useEffect(() => {
const handleResize = deferExecution(() => {
const { width, height } = getDimensions(
streamRef?.current?.offsetWidth,
streamRef?.current?.offsetHeight
streamRef?.current?.offsetWidth ?? 0,
streamRef?.current?.offsetHeight ?? 0
)
if (
settings.modelingContext.store.streamDimensions.streamWidth !== width ||
@ -122,10 +124,37 @@ export function useSetupEngineManager(
}, 500)
const onOnline = () => {
startEngineInstance()
startEngineInstance(true)
}
const onVisibilityChange = () => {
if (window.document.visibilityState === 'visible') {
if (
!engineCommandManager.engineConnection?.isReady() &&
!engineCommandManager.engineConnection?.isConnecting()
) {
startEngineInstance()
}
}
}
window.document.addEventListener('visibilitychange', onVisibilityChange)
const onAnyInput = () => {
if (
!engineCommandManager.engineConnection?.isReady() &&
!engineCommandManager.engineConnection?.isConnecting()
) {
startEngineInstance()
}
}
window.document.addEventListener('keydown', onAnyInput)
window.document.addEventListener('mousemove', onAnyInput)
window.document.addEventListener('mousedown', onAnyInput)
window.document.addEventListener('scroll', onAnyInput)
window.document.addEventListener('touchstart', onAnyInput)
const onOffline = () => {
kclManager.isFirstRender = true
engineCommandManager.tearDown()
}
@ -133,11 +162,30 @@ export function useSetupEngineManager(
window.addEventListener('offline', onOffline)
window.addEventListener('resize', handleResize)
return () => {
window.document.removeEventListener(
'visibilitychange',
onVisibilityChange
)
window.document.removeEventListener('keydown', onAnyInput)
window.document.removeEventListener('mousemove', onAnyInput)
window.document.removeEventListener('mousedown', onAnyInput)
window.document.removeEventListener('scroll', onAnyInput)
window.document.removeEventListener('touchstart', onAnyInput)
window.removeEventListener('online', onOnline)
window.removeEventListener('offline', onOffline)
window.removeEventListener('resize', handleResize)
}
}, [])
// Engine relies on many settings so we should rebind events when it changes
// We have to list out the ones we care about because the settings object holds
// non-settings too...
}, [
settings.enableSSAO,
settings.highlightEdges,
settings.showScaleGrid,
settings.theme,
settings.pool,
])
}
function getDimensions(streamWidth?: number, streamHeight?: number) {

View File

@ -6,7 +6,11 @@ import { modelingMachine } from 'machines/modelingMachine'
import { authMachine } from 'machines/authMachine'
import { settingsMachine } from 'machines/settingsMachine'
import { homeMachine } from 'machines/homeMachine'
import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes'
import {
Command,
StateMachineCommandSetConfig,
StateMachineCommandSetSchema,
} from 'lib/commandTypes'
import { useKclContext } from 'lang/KclProvider'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
@ -21,20 +25,20 @@ export type AllMachines =
interface UseStateMachineCommandsArgs<
T extends AllMachines,
S extends CommandSetSchema<T>
S extends StateMachineCommandSetSchema<T>
> {
machineId: T['id']
state: StateFrom<T>
send: Function
actor: InterpreterFrom<T>
commandBarConfig?: CommandSetConfig<T, S>
commandBarConfig?: StateMachineCommandSetConfig<T, S>
allCommandsRequireNetwork?: boolean
onCancel?: () => void
}
export default function useStateMachineCommands<
T extends AnyStateMachine,
S extends CommandSetSchema<T>
S extends StateMachineCommandSetSchema<T>
>({
machineId,
state,
@ -58,7 +62,7 @@ export default function useStateMachineCommands<
const newCommands = state.nextEvents
.filter((_) => !allCommandsRequireNetwork || !disableAllButtons)
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
.map((type) =>
.flatMap((type) =>
createMachineCommand<T, S>({
// The group is the owner machine's ID.
groupId: machineId,

View File

@ -3,6 +3,8 @@ import { createContext, useContext, useEffect, useState } from 'react'
import { type IndexLoaderData } from 'lib/types'
import { useLoaderData } from 'react-router-dom'
import { codeManager, kclManager } from 'lib/singletons'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { Command } from 'lib/commandTypes'
const KclContext = createContext({
code: codeManager?.code || '',
@ -35,6 +37,7 @@ export function KclContextProvider({
const [errors, setErrors] = useState<KCLError[]>([])
const [logs, setLogs] = useState<string[]>([])
const [wasmInitFailed, setWasmInitFailed] = useState(false)
const { commandBarSend } = useCommandsContext()
useEffect(() => {
codeManager.registerCallBacks({
@ -50,6 +53,28 @@ export function KclContextProvider({
})
}, [])
// Add format code to command palette.
useEffect(() => {
const commands: Command[] = [
{
name: 'format-code',
displayName: 'Format Code',
description: 'Nicely formats the KCL code in the editor.',
needsReview: false,
groupId: 'code',
icon: 'code',
onSubmit: (data) => {
kclManager.format()
},
},
]
commandBarSend({ type: 'Add commands', data: { commands } })
return () => {
commandBarSend({ type: 'Remove commands', data: { commands } })
}
}, [kclManager, commandBarSend])
return (
<KclContext.Provider
value={{

View File

@ -302,6 +302,30 @@ class EngineConnection extends EventTarget {
mediaStream?: MediaStream
freezeFrame: boolean = false
onIceCandidate = function (
this: RTCPeerConnection,
event: RTCPeerConnectionIceEvent
) {}
onIceCandidateError = function (
this: RTCPeerConnection,
event: RTCPeerConnectionIceErrorEvent
) {}
onConnectionStateChange = function (this: RTCPeerConnection, event: Event) {}
onDataChannelOpen = function (this: RTCDataChannel, event: Event) {}
onDataChannelClose = function (this: RTCDataChannel, event: Event) {}
onDataChannelError = function (this: RTCDataChannel, event: Event) {}
onDataChannelMessage = function (this: RTCDataChannel, event: MessageEvent) {}
onDataChannel = function (
this: RTCPeerConnection,
event: RTCDataChannelEvent
) {}
onTrack = function (this: RTCPeerConnection, event: RTCTrackEvent) {}
onWebSocketOpen = function (event: Event) {}
onWebSocketClose = function (event: Event) {}
onWebSocketError = function (event: Event) {}
onWebSocketMessage = function (event: MessageEvent) {}
onNetworkStatusReady = () => {}
private _state: EngineConnectionState = {
type: EngineConnectionStateType.Fresh,
}
@ -346,6 +370,7 @@ class EngineConnection extends EventTarget {
private engineCommandManager: EngineCommandManager
private pingPongSpan: { ping?: Date; pong?: Date }
private pingIntervalId: ReturnType<typeof setInterval>
constructor({
engineCommandManager,
@ -368,7 +393,7 @@ class EngineConnection extends EventTarget {
// Without an interval ping, our connection will timeout.
// If this.freezeFrame is true we skip this logic so only reconnect
// happens on mouse move
setInterval(() => {
this.pingIntervalId = setInterval(() => {
if (this.freezeFrame) return
switch (this.state.type as EngineConnectionStateType) {
@ -434,6 +459,44 @@ class EngineConnection extends EventTarget {
tearDown(opts?: { freeze: boolean }) {
this.freezeFrame = opts?.freeze ?? false
this.disconnectAll()
clearInterval(this.pingIntervalId)
this.pc?.removeEventListener('icecandidate', this.onIceCandidate)
this.pc?.removeEventListener('icecandidateerror', this.onIceCandidateError)
this.pc?.removeEventListener(
'connectionstatechange',
this.onConnectionStateChange
)
this.pc?.removeEventListener('track', this.onTrack)
this.unreliableDataChannel?.removeEventListener(
'open',
this.onDataChannelOpen
)
this.unreliableDataChannel?.removeEventListener(
'close',
this.onDataChannelClose
)
this.unreliableDataChannel?.removeEventListener(
'error',
this.onDataChannelError
)
this.unreliableDataChannel?.removeEventListener(
'message',
this.onDataChannelMessage
)
this.pc?.removeEventListener('datachannel', this.onDataChannel)
this.websocket?.removeEventListener('open', this.onWebSocketOpen)
this.websocket?.removeEventListener('close', this.onWebSocketClose)
this.websocket?.removeEventListener('error', this.onWebSocketError)
this.websocket?.removeEventListener('message', this.onWebSocketMessage)
window.removeEventListener(
'use-network-status-ready',
this.onNetworkStatusReady
)
this.state = {
type: EngineConnectionStateType.Disconnecting,
value: { type: DisconnectingType.Quit },
@ -477,7 +540,7 @@ class EngineConnection extends EventTarget {
},
}
this.pc.addEventListener('icecandidate', (event) => {
this.onIceCandidate = (event: RTCPeerConnectionIceEvent) => {
if (event.candidate === null) {
return
}
@ -499,18 +562,20 @@ class EngineConnection extends EventTarget {
usernameFragment: event.candidate.usernameFragment || undefined,
},
})
})
}
this.pc.addEventListener('icecandidate', this.onIceCandidate)
this.pc.addEventListener('icecandidateerror', (_event: Event) => {
this.onIceCandidateError = (_event: Event) => {
const event = _event as RTCPeerConnectionIceErrorEvent
console.warn(
`ICE candidate returned an error: ${event.errorCode}: ${event.errorText} for ${event.url}`
)
})
}
this.pc.addEventListener('icecandidateerror', this.onIceCandidateError)
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionstatechange_event
// Event type: generic Event type...
this.pc.addEventListener('connectionstatechange', (event: any) => {
this.onConnectionStateChange = (event: any) => {
console.log('connectionstatechange: ' + event.target?.connectionState)
switch (event.target?.connectionState) {
// From what I understand, only after have we done the ICE song and
@ -539,9 +604,13 @@ class EngineConnection extends EventTarget {
default:
break
}
})
}
this.pc.addEventListener(
'connectionstatechange',
this.onConnectionStateChange
)
this.pc.addEventListener('track', (event) => {
this.onTrack = (event) => {
const mediaStream = event.streams[0]
this.state = {
@ -625,9 +694,10 @@ class EngineConnection extends EventTarget {
// to pass it to the rest of the application.
this.mediaStream = mediaStream
})
}
this.pc.addEventListener('track', this.onTrack)
this.pc.addEventListener('datachannel', (event) => {
this.onDataChannel = (event) => {
this.unreliableDataChannel = event.channel
this.state = {
@ -638,7 +708,7 @@ class EngineConnection extends EventTarget {
},
}
this.unreliableDataChannel.addEventListener('open', (event) => {
this.onDataChannelOpen = (event) => {
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
@ -654,14 +724,22 @@ class EngineConnection extends EventTarget {
this.dispatchEvent(
new CustomEvent(EngineConnectionEvents.Opened, { detail: this })
)
})
}
this.unreliableDataChannel?.addEventListener(
'open',
this.onDataChannelOpen
)
this.unreliableDataChannel.addEventListener('close', (event) => {
this.onDataChannelClose = (event) => {
this.disconnectAll()
this.finalizeIfAllConnectionsClosed()
})
}
this.unreliableDataChannel?.addEventListener(
'close',
this.onDataChannelClose
)
this.unreliableDataChannel.addEventListener('error', (event) => {
this.onDataChannelError = (event) => {
this.disconnectAll()
this.state = {
@ -674,8 +752,13 @@ class EngineConnection extends EventTarget {
},
},
}
})
this.unreliableDataChannel.addEventListener('message', (event) => {
}
this.unreliableDataChannel?.addEventListener(
'error',
this.onDataChannelError
)
this.onDataChannelMessage = (event) => {
const result: UnreliableResponses = JSON.parse(event.data)
Object.values(
this.engineCommandManager.unreliableSubscriptions[result.type] || {}
@ -697,8 +780,13 @@ class EngineConnection extends EventTarget {
}
}
)
})
})
}
this.unreliableDataChannel.addEventListener(
'message',
this.onDataChannelMessage
)
}
this.pc.addEventListener('datachannel', this.onDataChannel)
}
const createWebSocketConnection = () => {
@ -712,7 +800,7 @@ class EngineConnection extends EventTarget {
this.websocket = new WebSocket(this.url, [])
this.websocket.binaryType = 'arraybuffer'
this.websocket.addEventListener('open', (event) => {
this.onWebSocketOpen = (event) => {
this.state = {
type: EngineConnectionStateType.Connecting,
value: {
@ -733,14 +821,16 @@ class EngineConnection extends EventTarget {
// Send an initial ping
this.send({ type: 'ping' })
this.pingPongSpan.ping = new Date()
})
}
this.websocket.addEventListener('open', this.onWebSocketOpen)
this.websocket.addEventListener('close', (event) => {
this.onWebSocketClose = (event) => {
this.disconnectAll()
this.finalizeIfAllConnectionsClosed()
})
}
this.websocket.addEventListener('close', this.onWebSocketClose)
this.websocket.addEventListener('error', (event) => {
this.onWebSocketError = (event) => {
this.disconnectAll()
this.state = {
@ -753,9 +843,10 @@ class EngineConnection extends EventTarget {
},
},
}
})
}
this.websocket.addEventListener('error', this.onWebSocketError)
this.websocket.addEventListener('message', (event) => {
this.onWebSocketMessage = (event) => {
// In the EngineConnection, we're looking for messages to/from
// the server that relate to the ICE handshake, or WebRTC
// negotiation. There may be other messages (including ArrayBuffer
@ -960,15 +1051,20 @@ class EngineConnection extends EventTarget {
})
break
}
})
}
this.websocket.addEventListener('message', this.onWebSocketMessage)
}
if (reconnecting) {
createWebSocketConnection()
} else {
window.addEventListener('use-network-status-ready', () => {
this.onNetworkStatusReady = () => {
createWebSocketConnection()
})
}
window.addEventListener(
'use-network-status-ready',
this.onNetworkStatusReady
)
}
}
// Do not change this back to an object or any, we should only be sending the
@ -1154,7 +1250,15 @@ export class EngineCommandManager extends EventTarget {
private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null
private modifyGrid: (hidden: boolean) => Promise<void> | null = () => null
private onEngineConnectionOpened = () => {}
private onEngineConnectionClosed = () => {}
private onEngineConnectionStarted = ({ detail: engineConnection }: any) => {}
private onEngineConnectionNewTrack = ({
detail,
}: CustomEvent<NewTrackArgs>) => {}
start({
restart,
setMediaStream,
setIsStreamReady,
width,
@ -1170,6 +1274,7 @@ export class EngineCommandManager extends EventTarget {
showScaleGrid: false,
},
}: {
restart?: boolean
setMediaStream: (stream: MediaStream) => void
setIsStreamReady: (isStreamReady: boolean) => void
width: number
@ -1215,162 +1320,168 @@ export class EngineCommandManager extends EventTarget {
})
)
this.onEngineConnectionOpened = () => {
// Set the stream background color
// This takes RGBA values from 0-1
// So we convert from the conventional 0-255 found in Figma
void this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'set_background_color',
color: getThemeColorForEngine(settings.theme),
},
})
// Sets the default line colors
const opposingTheme = getOppositeTheme(settings.theme)
this.sendSceneCommand({
cmd_id: uuidv4(),
type: 'modeling_cmd_req',
cmd: {
type: 'set_default_system_properties',
color: getThemeColorForEngine(opposingTheme),
},
})
// Set the edge lines visibility
this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'edge_lines_visible' as any, // TODO: update kittycad.ts to use the correct type
hidden: !settings.highlightEdges,
},
})
this._camControlsCameraChange()
this.sendSceneCommand({
// CameraControls subscribes to default_camera_get_settings response events
// firing this at connection ensure the camera's are synced initially
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
// We want modify the grid first because we don't want it to flash.
// Ideally these would already be default hidden in engine (TODO do
// that) https://github.com/KittyCAD/engine/issues/2282
this.modifyGrid(!settings.showScaleGrid)?.then(async () => {
await this.initPlanes()
this.resolveReady()
setIsStreamReady(true)
await executeCode()
})
}
this.engineConnection.addEventListener(
EngineConnectionEvents.Opened,
() => {
// Set the stream background color
// This takes RGBA values from 0-1
// So we convert from the conventional 0-255 found in Figma
void this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'set_background_color',
color: getThemeColorForEngine(settings.theme),
},
})
// Sets the default line colors
const opposingTheme = getOppositeTheme(settings.theme)
this.sendSceneCommand({
cmd_id: uuidv4(),
type: 'modeling_cmd_req',
cmd: {
type: 'set_default_system_properties',
color: getThemeColorForEngine(opposingTheme),
},
})
// Set the edge lines visibility
this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'edge_lines_visible' as any, // TODO: update kittycad.ts to use the correct type
hidden: !settings.highlightEdges,
},
})
this._camControlsCameraChange()
this.sendSceneCommand({
// CameraControls subscribes to default_camera_get_settings response events
// firing this at connection ensure the camera's are synced initially
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
// We want modify the grid first because we don't want it to flash.
// Ideally these would already be default hidden in engine (TODO do
// that) https://github.com/KittyCAD/engine/issues/2282
this.modifyGrid(!settings.showScaleGrid)?.then(async () => {
await this.initPlanes()
this.resolveReady()
setIsStreamReady(true)
await executeCode()
})
}
this.onEngineConnectionOpened
)
this.onEngineConnectionClosed = () => {
setIsStreamReady(false)
}
this.engineConnection.addEventListener(
EngineConnectionEvents.Closed,
() => {
setIsStreamReady(false)
}
this.onEngineConnectionClosed
)
this.engineConnection.addEventListener(
EngineConnectionEvents.ConnectionStarted,
({ detail: engineConnection }: any) => {
engineConnection?.pc?.addEventListener(
'datachannel',
(event: RTCDataChannelEvent) => {
let unreliableDataChannel = event.channel
this.onEngineConnectionStarted = ({ detail: engineConnection }: any) => {
engineConnection?.pc?.addEventListener(
'datachannel',
(event: RTCDataChannelEvent) => {
let unreliableDataChannel = event.channel
unreliableDataChannel.addEventListener(
'message',
(event: MessageEvent) => {
const result: UnreliableResponses = JSON.parse(event.data)
Object.values(
this.unreliableSubscriptions[result.type] || {}
).forEach(
// TODO: There is only one response that uses the unreliable channel atm,
// highlight_set_entity, if there are more it's likely they will all have the same
// sequence logic, but I'm not sure if we use a single global sequence or a sequence
// per unreliable subscription.
(callback) => {
let data = result?.data
if (isHighlightSetEntity_type(data)) {
if (
data.sequence !== undefined &&
data.sequence > this.inSequence
) {
this.inSequence = data.sequence
callback(result)
}
unreliableDataChannel.addEventListener(
'message',
(event: MessageEvent) => {
const result: UnreliableResponses = JSON.parse(event.data)
Object.values(
this.unreliableSubscriptions[result.type] || {}
).forEach(
// TODO: There is only one response that uses the unreliable channel atm,
// highlight_set_entity, if there are more it's likely they will all have the same
// sequence logic, but I'm not sure if we use a single global sequence or a sequence
// per unreliable subscription.
(callback) => {
let data = result?.data
if (isHighlightSetEntity_type(data)) {
if (
data.sequence !== undefined &&
data.sequence > this.inSequence
) {
this.inSequence = data.sequence
callback(result)
}
}
)
}
)
}
)
// When the EngineConnection starts a connection, we want to register
// callbacks into the WebSocket/PeerConnection.
engineConnection.websocket?.addEventListener('message', ((
event: MessageEvent
) => {
if (event.data instanceof ArrayBuffer) {
// If the data is an ArrayBuffer, it's the result of an export command,
// because in all other cases we send JSON strings. But in the case of
// export we send a binary blob.
// Pass this to our export function.
exportSave(event.data).then(() => {
this.pendingExport?.resolve()
}, this.pendingExport?.reject)
} else {
const message: Models['WebSocketResponse_type'] = JSON.parse(
event.data
)
if (
message.success &&
(message.resp.type === 'modeling' ||
message.resp.type === 'modeling_batch') &&
message.request_id
) {
this.handleModelingCommand(
message.resp,
message.request_id,
message
}
)
} else if (
!message.success &&
message.request_id &&
this.artifactMap[message.request_id]
) {
this.handleFailedModelingCommand(message.request_id, message)
}
)
}
)
// When the EngineConnection starts a connection, we want to register
// callbacks into the WebSocket/PeerConnection.
engineConnection.websocket?.addEventListener('message', ((
event: MessageEvent
) => {
if (event.data instanceof ArrayBuffer) {
// If the data is an ArrayBuffer, it's the result of an export command,
// because in all other cases we send JSON strings. But in the case of
// export we send a binary blob.
// Pass this to our export function.
exportSave(event.data).then(() => {
this.pendingExport?.resolve()
}, this.pendingExport?.reject)
} else {
const message: Models['WebSocketResponse_type'] = JSON.parse(
event.data
)
if (
message.success &&
(message.resp.type === 'modeling' ||
message.resp.type === 'modeling_batch') &&
message.request_id
) {
this.handleModelingCommand(
message.resp,
message.request_id,
message
)
} else if (
!message.success &&
message.request_id &&
this.artifactMap[message.request_id]
) {
this.handleFailedModelingCommand(message.request_id, message)
}
}) as EventListener)
}
}) as EventListener)
this.engineConnection?.addEventListener(
EngineConnectionEvents.NewTrack,
(({ detail: { mediaStream } }: CustomEvent<NewTrackArgs>) => {
mediaStream.getVideoTracks()[0].addEventListener('mute', () => {
console.error(
'video track mute: check webrtc internals -> inbound rtp'
)
})
this.onEngineConnectionNewTrack = ({
detail: { mediaStream },
}: CustomEvent<NewTrackArgs>) => {
mediaStream.getVideoTracks()[0].addEventListener('mute', () => {
console.error(
'video track mute: check webrtc internals -> inbound rtp'
)
})
setMediaStream(mediaStream)
}) as EventListener
)
this.engineConnection?.connect()
setMediaStream(mediaStream)
}
this.engineConnection?.addEventListener(
EngineConnectionEvents.NewTrack,
this.onEngineConnectionNewTrack as EventListener
)
this.engineConnection?.connect()
}
this.engineConnection.addEventListener(
EngineConnectionEvents.ConnectionStarted,
this.onEngineConnectionStarted
)
}
@ -1629,7 +1740,26 @@ export class EngineCommandManager extends EventTarget {
}
tearDown() {
if (this.engineConnection) {
this.engineConnection.removeEventListener(
EngineConnectionEvents.Opened,
this.onEngineConnectionOpened
)
this.engineConnection.removeEventListener(
EngineConnectionEvents.Closed,
this.onEngineConnectionClosed
)
this.engineConnection.removeEventListener(
EngineConnectionEvents.ConnectionStarted,
this.onEngineConnectionStarted
)
this.engineConnection.removeEventListener(
EngineConnectionEvents.NewTrack,
this.onEngineConnectionNewTrack as EventListener
)
this.engineConnection?.tearDown()
this.engineConnection = undefined
// Our window.tearDown assignment causes this case to happen which is
// only really for tests.
// @ts-ignore

View File

@ -1,9 +1,9 @@
import { CommandSetConfig } from 'lib/commandTypes'
import { StateMachineCommandSetConfig } from 'lib/commandTypes'
import { authMachine } from 'machines/authMachine'
type AuthCommandSchema = {}
export const authCommandBarConfig: CommandSetConfig<
export const authCommandBarConfig: StateMachineCommandSetConfig<
typeof authMachine,
AuthCommandSchema
> = {

View File

@ -1,4 +1,4 @@
import { CommandSetConfig } from 'lib/commandTypes'
import { StateMachineCommandSetConfig } from 'lib/commandTypes'
import { homeMachine } from 'machines/homeMachine'
export type HomeCommandSchema = {
@ -17,7 +17,7 @@ export type HomeCommandSchema = {
}
}
export const homeCommandBarConfig: CommandSetConfig<
export const homeCommandBarConfig: StateMachineCommandSetConfig<
typeof homeMachine,
HomeCommandSchema
> = {

View File

@ -1,8 +1,8 @@
import { Models } from '@kittycad/lib'
import { CommandSetConfig, KclCommandValue } from 'lib/commandTypes'
import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes'
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
import { Selections } from 'lib/selections'
import { modelingMachine } from 'machines/modelingMachine'
import { modelingMachine, SketchTool } from 'machines/modelingMachine'
type OutputFormat = Models['OutputFormat_type']
type OutputTypeKey = OutputFormat['type']
@ -27,9 +27,12 @@ export type ModelingCommandSchema = {
// result: (typeof EXTRUSION_RESULTS)[number]
distance: KclCommandValue
}
'change tool': {
tool: SketchTool
}
}
export const modelingMachineConfig: CommandSetConfig<
export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
typeof modelingMachine,
ModelingCommandSchema
> = {
@ -37,22 +40,47 @@ export const modelingMachineConfig: CommandSetConfig<
description: 'Enter sketch mode.',
icon: 'sketch',
},
// TODO the event is no 'change tool' with data: 'line', 'rectangle' etc
// 'Equip Line tool': {
// description: 'Start drawing straight lines.',
// icon: 'line',
// displayName: 'Line',
// },
// 'Equip tangential arc to': {
// description: 'Start drawing an arc tangent to the current segment.',
// icon: 'arc',
// displayName: 'Tangential Arc',
// },
// 'Equip rectangle tool': {
// description: 'Start drawing a rectangle.',
// icon: 'rectangle',
// displayName: 'Rectangle',
// },
'change tool': [
{
description: 'Start drawing straight lines.',
icon: 'line',
displayName: 'Line',
args: {
tool: {
defaultValue: 'line',
required: true,
skip: true,
inputType: 'string',
},
},
},
{
description: 'Start drawing an arc tangent to the current segment.',
icon: 'arc',
displayName: 'Tangential Arc',
args: {
tool: {
defaultValue: 'tangentialArc',
required: true,
skip: true,
inputType: 'string',
},
},
},
{
description: 'Start drawing a rectangle.',
icon: 'rectangle',
displayName: 'Rectangle',
args: {
tool: {
defaultValue: 'rectangle',
required: true,
skip: true,
inputType: 'string',
},
},
},
],
Export: {
description: 'Export the current model.',
icon: 'exportFile',

View File

@ -124,6 +124,7 @@ export function createSettingsCommand({
displayName: `Settings · ${decamelize(type.replaceAll('.', ' · '), {
separator: ' ',
})}`,
description: settingConfig.description,
groupId: 'settings',
icon: 'settings',
needsReview: false,

View File

@ -33,13 +33,13 @@ export interface KclExpressionWithVariable extends KclExpression {
export type KclCommandValue = KclExpression | KclExpressionWithVariable
export type CommandInputType = (typeof INPUT_TYPES)[number]
export type CommandSetSchema<T extends AnyStateMachine> = Partial<{
export type StateMachineCommandSetSchema<T extends AnyStateMachine> = Partial<{
[EventType in EventFrom<T>['type']]: Record<string, any>
}>
export type CommandSet<
export type StateMachineCommandSet<
T extends AllMachines,
Schema extends CommandSetSchema<T>
Schema extends StateMachineCommandSetSchema<T>
> = Partial<{
[EventType in EventFrom<T>['type']]: Command<
T,
@ -48,21 +48,25 @@ export type CommandSet<
>
}>
export type CommandSetConfig<
/**
* A configuration object for a set of commands tied to a state machine.
* Each event type can have one or more commands associated with it.
* @param T The state machine type.
* @param Schema The schema for the command set, defined by the developer.
*/
export type StateMachineCommandSetConfig<
T extends AllMachines,
Schema extends CommandSetSchema<T>
Schema extends StateMachineCommandSetSchema<T>
> = Partial<{
[EventType in EventFrom<T>['type']]: CommandConfig<
T,
EventFrom<T>['type'],
Schema[EventType]
>
[EventType in EventFrom<T>['type']]:
| CommandConfig<T, EventFrom<T>['type'], Schema[EventType]>
| CommandConfig<T, EventFrom<T>['type'], Schema[EventType]>[]
}>
export type Command<
T extends AnyStateMachine = AnyStateMachine,
CommandName extends EventFrom<T>['type'] = EventFrom<T>['type'],
CommandSchema extends CommandSetSchema<T>[CommandName] = CommandSetSchema<T>[CommandName]
CommandSchema extends StateMachineCommandSetSchema<T>[CommandName] = StateMachineCommandSetSchema<T>[CommandName]
> = {
name: CommandName
groupId: T['id']
@ -81,7 +85,7 @@ export type Command<
export type CommandConfig<
T extends AnyStateMachine = AnyStateMachine,
CommandName extends EventFrom<T>['type'] = EventFrom<T>['type'],
CommandSchema extends CommandSetSchema<T>[CommandName] = CommandSetSchema<T>[CommandName]
CommandSchema extends StateMachineCommandSetSchema<T>[CommandName] = StateMachineCommandSetSchema<T>[CommandName]
> = Omit<
Command<T, CommandName, CommandSchema>,
'name' | 'groupId' | 'onSubmit' | 'onCancel' | 'args' | 'needsReview'

View File

@ -11,20 +11,20 @@ import {
CommandArgument,
CommandArgumentConfig,
CommandConfig,
CommandSetConfig,
CommandSetSchema,
StateMachineCommandSetConfig,
StateMachineCommandSetSchema,
} from './commandTypes'
interface CreateMachineCommandProps<
T extends AnyStateMachine,
S extends CommandSetSchema<T>
S extends StateMachineCommandSetSchema<T>
> {
type: EventFrom<T>['type']
groupId: T['id']
state: StateFrom<T>
send: Function
actor: InterpreterFrom<T>
commandBarConfig?: CommandSetConfig<T, S>
commandBarConfig?: StateMachineCommandSetConfig<T, S>
onCancel?: () => void
}
@ -32,7 +32,7 @@ interface CreateMachineCommandProps<
// from a more terse Command Bar Meta definition.
export function createMachineCommand<
T extends AnyStateMachine,
S extends CommandSetSchema<T>
S extends StateMachineCommandSetSchema<T>
>({
groupId,
type,
@ -41,13 +41,30 @@ export function createMachineCommand<
actor,
commandBarConfig,
onCancel,
}: CreateMachineCommandProps<T, S>): Command<
T,
typeof type,
S[typeof type]
> | null {
}: CreateMachineCommandProps<T, S>):
| Command<T, typeof type, S[typeof type]>
| Command<T, typeof type, S[typeof type]>[]
| null {
const commandConfig = commandBarConfig && commandBarConfig[type]
if (!commandConfig) return null
// There may be no command config for this event type,
// or there may be multiple commands to create.
if (!commandConfig) {
return null
} else if (commandConfig instanceof Array) {
return commandConfig
.map((config) =>
createMachineCommand({
groupId,
type,
state,
send,
actor,
commandBarConfig: { [type]: config },
onCancel,
})
)
.filter((c) => c !== null) as Command<T, typeof type, S[typeof type]>[]
}
// Hide commands based on platform by returning `null`
// so the consumer can filter them out
@ -64,6 +81,7 @@ export function createMachineCommand<
name: type,
groupId,
icon,
description: commandConfig.description,
needsReview: commandConfig.needsReview || false,
onSubmit: (data?: S[typeof type]) => {
if (data !== undefined && data !== null) {
@ -84,6 +102,10 @@ export function createMachineCommand<
command.onCancel = onCancel
}
if ('displayName' in commandConfig) {
command.displayName = commandConfig.displayName
}
return command
}
@ -92,7 +114,7 @@ export function createMachineCommand<
// bundled together into the args for a Command.
function buildCommandArguments<
T extends AnyStateMachine,
S extends CommandSetSchema<T>,
S extends StateMachineCommandSetSchema<T>,
CommandName extends EventFrom<T>['type'] = EventFrom<T>['type']
>(
state: StateFrom<T>,
@ -112,7 +134,7 @@ function buildCommandArguments<
export function buildCommandArgument<
T extends AnyStateMachine,
O extends CommandSetSchema<T> = CommandSetSchema<T>
O extends StateMachineCommandSetSchema<T> = StateMachineCommandSetSchema<T>
>(
arg: CommandArgumentConfig<O, T>,
context: ContextFrom<T>,

View File

@ -66,7 +66,7 @@ export type CommandBarMachineEvent =
export const commandBarMachine = createMachine(
{
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswZJbPltM0U3eXLZAsRFeQcrerUHRbTFvfkmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUilkskqdiUWjWCAcDC0kjUqnElkc6lkoK0JzOT0CnWo1zIAEEIARvn48LA-uwuIC4qAEqIHJjJPJcYtxFoYdJFFjVsZEG5pqCquJxJoGvJxOUCT82sSrj0AEpgdCoABuYC+yog9OYIyZsQmYmhWzMmTqLnU0kyikRGVRbj5lg2SmUam5StpFxIkgAymBXh8HlADQGyKHw58aecGZEzUDWYgchY7CisWoSlpQQ5EVDLJJxKkdIpyypUop-ed2kHCW0Xu9uD1kwDzcCEJCtlUNgWhZZ1OlnaKEBpZJItHVzDZ5xlpPWiZdDc8w22Ow5wozosyLUiVNMSg5ytVKmfrIipzO564z2otPVpI1vKd18SAOJYbgACzAEhI3wUgoAAV3QQZuFgCg-1wGAvjAkgSCgkCSHAyCcG4TtUxZYRED2TQZGSOw80qaQ7ARCc9mmZYSksextGkNJBRXFUOh-f9AOA0CIKgmCABE4E3D5oyTE1-lww8clKBjZF2Bh5SFZwXUUyRVDzJwNE2LQzzYwNJE4gCgJwKNeMw6DJHJAB3LAYlM-AHjYMDuFjMCACN0B4NCMKgnD927dMkWSUs8yY8RISfWQshvcsrDlapshULJPHfZsDKM7iHPM-jJAANSwShOAgX9IzICB+DASQHh1VAAGsqp1Qrit-MByXQvisP8sY8ISZwbCsDllBLSwz1qRFBQsRZ5WRIVkui-TG0M39jJ4jqLNgfLmpK3hTLIQCSFQIM2EoX8ADMjvQSQmqKna2vWvyJL3HrD1SEoyzSdJUkUm9dnUtQn10aK6hkxbiU1HVODAay3M87zE1+J6UwCtN8IQUauR0OZ0ksFwUUqRFPWmUdouPXR3TBjoIahmHKQIHKuqRrtUYSaVShhLF+TkeTzBFQosVKDRM2RVInEdSmg2p6GyE1bU9R8zrsKZqSexFqQHWlHNXHzCbFhnX1+UydFkVxiXJClmGAFEIG8hmld3ZGXp7TR5C5RSCxdjlQV1tR9bqaVdhsSFxDN5AALeOrgPa3ysJgiqcCqmr6sa7bWujxXjQd5nesQZYthsblIQYJx6hUic9AsdEFT2HQzByVLmgDJaw-eSOHPTjbysq6qcFqhrrtT9sO-4ugd1NFGc4QXEPo5eVLGsFxlnKREXGnZI9HcT1mOikO0s-S5w7bqNh9j-bkKOyQTvOy6B9utOHtj7qD1V8pSyrHJZ3STY4QmjQ0R2Ww0oSjuF3u+HAqAIBwEEOlRs48nZBVENkKQNprC42qBoJ0LothaXMJ9aElRXDLj3k3VUpJIBwOfgg4oMx8y41SPUOY8p5DFk2GiKoxsoRVmhGoM2cY2zAQRngChgU0a7HZtFH0ilRp1D5usVh6JXCKD2CUdI8lQ7rlbB8chkkJ6HkxFsBeulbQZAUCwrYCiqzIhyE+fqZtMomTMg-aCwiWYEV2NOBUCghQ6EFGzYsvpwQUT5MxawFECx2JWllRxMdLI2TsrtKMTkXIuMnmeCiZYwrePMFWCKv1XZKVGroWwmxNARK4g4hWG0tp3wSSk16lQpC41ULjV8uxUjSBvFCNEDTdCOkWCY44xCGzgzAJDaGdTnaZHBADYcqg5DpCovzeRno5izA0BeUOh8o5OPgDo+BaNvqV0xNFaE7gdYTnnvkmUChdKEMyF4LwQA */
/** @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 */
predictableActionArguments: true,
tsTypes: {} as import('./commandBarMachine.typegen').Typegen0,
context: {
@ -147,6 +147,10 @@ export const commandBarMachine = createMachine(
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'],
@ -510,7 +514,9 @@ export const commandBarMachine = createMachine(
)
function sortCommands(a: Command, b: Command) {
if (b.groupId === 'auth') return -1
if (a.groupId === 'auth') return 1
if (b.groupId === 'auth' && !(a.groupId === 'auth')) return -2
if (a.groupId === 'auth' && !(b.groupId === 'auth')) return 2
if (b.groupId === 'settings' && !(a.groupId === 'settings')) return -1
if (a.groupId === 'settings' && !(b.groupId === 'settings')) return 1
return a.name.localeCompare(b.name)
}

File diff suppressed because one or more lines are too long

View File

@ -1842,7 +1842,7 @@ dependencies = [
"bincode",
"either",
"fnv",
"itertools 0.10.5",
"itertools 0.12.1",
"lazy_static",
"nom",
"quick-xml",
@ -2928,18 +2928,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.61"
version = "1.0.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.61"
version = "1.0.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c"
dependencies = [
"proc-macro2",
"quote",

View File

@ -35,7 +35,7 @@ schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1"]
serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.120"
sha2 = "0.10.8"
thiserror = "1.0.61"
thiserror = "1.0.62"
toml = "0.8.14"
ts-rs = { version = "9.0.1", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings", "serde-json-impl"] }
url = { version = "2.5.2", features = ["serde"] }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 KiB

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

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