diff --git a/e2e/playwright/command-bar-tests.spec.ts b/e2e/playwright/command-bar-tests.spec.ts
index fc8ca0bce..a0f900b94 100644
--- a/e2e/playwright/command-bar-tests.spec.ts
+++ b/e2e/playwright/command-bar-tests.spec.ts
@@ -219,7 +219,11 @@ test.describe('Command bar tests', { tag: ['@skipWin'] }, () => {
}
)
- test('Can extrude from the command bar', async ({ page, homePage }) => {
+ test('Can extrude from the command bar', async ({
+ page,
+ homePage,
+ cmdBar,
+ }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
@@ -254,7 +258,7 @@ test.describe('Command bar tests', { tag: ['@skipWin'] }, () => {
await expect(cmdSearchBar).toBeVisible()
// Search for extrude command and choose it
- await page.getByRole('option', { name: 'Extrude' }).click()
+ await cmdBar.cmdOptions.getByText('Extrude').click()
// Assert that we're on the selection step
await expect(page.getByRole('button', { name: 'selection' })).toBeDisabled()
diff --git a/e2e/playwright/point-click.spec.ts b/e2e/playwright/point-click.spec.ts
index a4fb9bfb0..b90fd114b 100644
--- a/e2e/playwright/point-click.spec.ts
+++ b/e2e/playwright/point-click.spec.ts
@@ -2796,4 +2796,107 @@ radius = 8.69
expect(editor.expectEditor.toContain(newCodeToFind)).toBeTruthy()
})
})
+
+ test(`Set appearance`, async ({
+ context,
+ page,
+ homePage,
+ scene,
+ editor,
+ toolbar,
+ cmdBar,
+ }) => {
+ const initialCode = `sketch001 = startSketchOn('XZ')
+profile001 = circle({
+ center = [0, 0],
+ radius = 100
+}, sketch001)
+extrude001 = extrude(profile001, length = 100)
+`
+ await context.addInitScript((initialCode) => {
+ localStorage.setItem('persistCode', initialCode)
+ }, initialCode)
+ await page.setBodyDimensions({ width: 1000, height: 500 })
+ await homePage.goToModelingScene()
+ await scene.waitForExecutionDone()
+
+ // One dumb hardcoded screen pixel value
+ const testPoint = { x: 500, y: 250 }
+ const initialColor: [number, number, number] = [135, 135, 135]
+
+ await test.step(`Confirm extrude exists with default appearance`, async () => {
+ await toolbar.closePane('code')
+ await scene.expectPixelColor(initialColor, testPoint, 15)
+ })
+
+ async function setApperanceAndCheck(
+ option: string,
+ hex: string,
+ shapeColor: [number, number, number]
+ ) {
+ await toolbar.openPane('feature-tree')
+ const operationButton = await toolbar.getFeatureTreeOperation(
+ 'Extrude',
+ 0
+ )
+ await operationButton.click({ button: 'right' })
+ const menuButton = page.getByTestId('context-menu-set-appearance')
+ await menuButton.click()
+ await cmdBar.expectState({
+ commandName: 'Appearance',
+ currentArgKey: 'color',
+ currentArgValue: '',
+ headerArguments: {
+ Color: '',
+ },
+ highlightedHeaderArg: 'color',
+ stage: 'arguments',
+ })
+ const item = page.getByText(option, { exact: true })
+ await item.click()
+ await cmdBar.expectState({
+ commandName: 'Appearance',
+ headerArguments: {
+ Color: hex,
+ },
+ stage: 'review',
+ })
+ await cmdBar.progressCmdBar()
+ await toolbar.closePane('feature-tree')
+ await scene.expectPixelColor(shapeColor, testPoint, 40)
+ await toolbar.openPane('code')
+ if (hex === 'default') {
+ const anyAppearanceDeclaration = `|> appearance(`
+ await editor.expectEditor.not.toContain(anyAppearanceDeclaration)
+ } else {
+ const declaration = `|> appearance(%, color = '${hex}')`
+ await editor.expectEditor.toContain(declaration)
+ // TODO: fix selection range after appearance update
+ // await editor.expectState({
+ // diagnostics: [],
+ // activeLines: [declaration],
+ // highlightedCode: '',
+ // })
+ }
+ await toolbar.closePane('code')
+ }
+
+ await test.step(`Go through the Set Appearance flow for all options`, async () => {
+ await setApperanceAndCheck('Red', '#FF0000', [180, 0, 0])
+ await setApperanceAndCheck('Green', '#00FF00', [0, 180, 0])
+ await setApperanceAndCheck('Blue', '#0000FF', [0, 0, 180])
+ await setApperanceAndCheck('Turquoise', '#00FFFF', [0, 180, 180])
+ await setApperanceAndCheck('Purple', '#FF00FF', [180, 0, 180])
+ await setApperanceAndCheck('Yellow', '#FFFF00', [180, 180, 0])
+ await setApperanceAndCheck('Black', '#000000', [0, 0, 0])
+ await setApperanceAndCheck('Dark Grey', '#080808', [10, 10, 10])
+ await setApperanceAndCheck('Light Grey', '#D3D3D3', [190, 190, 190])
+ await setApperanceAndCheck('White', '#FFFFFF', [200, 200, 200])
+ await setApperanceAndCheck(
+ 'Default (clear appearance)',
+ 'default',
+ initialColor
+ )
+ })
+ })
})
diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Sketch-on-face-with-none-z-up-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Sketch-on-face-with-none-z-up-1-Google-Chrome-linux.png
index e244fafc6..94d577050 100644
Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Sketch-on-face-with-none-z-up-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Sketch-on-face-with-none-z-up-1-Google-Chrome-linux.png differ
diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-2d-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-2d-1-Google-Chrome-linux.png
index a6cab619a..3bb852179 100644
Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-2d-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-2d-1-Google-Chrome-linux.png differ
diff --git a/src/components/ModelingSidebar/ModelingPanes/FeatureTreePane.tsx b/src/components/ModelingSidebar/ModelingPanes/FeatureTreePane.tsx
index c7f68bb60..984fe1b21 100644
--- a/src/components/ModelingSidebar/ModelingPanes/FeatureTreePane.tsx
+++ b/src/components/ModelingSidebar/ModelingPanes/FeatureTreePane.tsx
@@ -324,6 +324,18 @@ const OperationItem = (props: {
}
}
+ function enterAppearanceFlow() {
+ if (props.item.type === 'StdLibCall') {
+ props.send({
+ type: 'enterAppearanceFlow',
+ data: {
+ targetSourceRange: sourceRangeFromRust(props.item.sourceRange),
+ currentOperation: props.item,
+ },
+ })
+ }
+ }
+
function deleteOperation() {
if (
props.item.type === 'StdLibCall' ||
@@ -380,6 +392,13 @@ const OperationItem = (props: {
: []),
...(props.item.type === 'StdLibCall'
? [
+
+ Set appearance
+ ,
(
node,
pathToExtrudeNode,
@@ -427,7 +427,7 @@ function locateExtrudeDeclarator(
return new Error('Extrude must be a PipeExpression or CallExpression')
}
- return { extrudeDeclarator }
+ return { extrudeDeclarator, shallowPath: nodeOfExtrudeCall.shallowPath }
}
function getPathToNodeOfEdgeTreatmentLiteral(
diff --git a/src/lang/modifyAst/setAppearance.ts b/src/lang/modifyAst/setAppearance.ts
new file mode 100644
index 000000000..6ccfeca19
--- /dev/null
+++ b/src/lang/modifyAst/setAppearance.ts
@@ -0,0 +1,70 @@
+import { PathToNode, Program } from 'lang/wasm'
+import { Node } from 'wasm-lib/kcl/bindings/Node'
+import { locateExtrudeDeclarator } from './addEdgeTreatment'
+import { err } from 'lib/trap'
+import {
+ createCallExpressionStdLibKw,
+ createLabeledArg,
+ createLiteral,
+ createPipeExpression,
+} from 'lang/modifyAst'
+import { createPipeSubstitution } from 'lang/modifyAst'
+import { COMMAND_APPEARANCE_COLOR_DEFAULT } from 'lib/commandBarConfigs/modelingCommandConfig'
+
+export function setAppearance({
+ ast,
+ nodeToEdit,
+ color,
+}: {
+ ast: Node
+ nodeToEdit: PathToNode
+ color: string
+}): Error | { modifiedAst: Node; pathToNode: PathToNode } {
+ const modifiedAst = structuredClone(ast)
+
+ // Locate the call (not necessarily an extrude here)
+ const result = locateExtrudeDeclarator(modifiedAst, nodeToEdit)
+ if (err(result)) {
+ return result
+ }
+
+ const declarator = result.extrudeDeclarator
+ const call = createCallExpressionStdLibKw(
+ 'appearance',
+ createPipeSubstitution(),
+ [createLabeledArg('color', createLiteral(color))]
+ )
+ // Modify the expression
+ if (
+ declarator.init.type === 'CallExpression' ||
+ declarator.init.type === 'CallExpressionKw'
+ ) {
+ // 1. case when no appearance exists, mutate in place
+ declarator.init = createPipeExpression([declarator.init, call])
+ } else if (declarator.init.type === 'PipeExpression') {
+ // 2. case when appearance exists or extrude in sketch pipe
+ const existingIndex = declarator.init.body.findIndex(
+ (v) =>
+ v.type === 'CallExpressionKw' &&
+ v.callee.type === 'Identifier' &&
+ v.callee.name === 'appearance'
+ )
+ if (existingIndex > -1) {
+ if (color === COMMAND_APPEARANCE_COLOR_DEFAULT) {
+ // Special case of unsetting the appearance aka deleting the node
+ declarator.init.body.splice(existingIndex, 1)
+ } else {
+ declarator.init.body[existingIndex] = call
+ }
+ } else {
+ declarator.init.body.push(call)
+ }
+ } else {
+ return new Error('Unsupported operation type.')
+ }
+
+ return {
+ modifiedAst,
+ pathToNode: result.shallowPath,
+ }
+}
diff --git a/src/lib/commandBarConfigs/modelingCommandConfig.ts b/src/lib/commandBarConfigs/modelingCommandConfig.ts
index da5d198c9..fac354b20 100644
--- a/src/lib/commandBarConfigs/modelingCommandConfig.ts
+++ b/src/lib/commandBarConfigs/modelingCommandConfig.ts
@@ -3,7 +3,11 @@ import { angleLengthInfo } from 'components/Toolbar/setAngleLength'
import { transformAstSketchLines } from 'lang/std/sketchcombos'
import { PathToNode } from 'lang/wasm'
import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes'
-import { KCL_DEFAULT_LENGTH, KCL_DEFAULT_DEGREE } from 'lib/constants'
+import {
+ KCL_DEFAULT_LENGTH,
+ KCL_DEFAULT_DEGREE,
+ KCL_DEFAULT_COLOR,
+} from 'lib/constants'
import { components } from 'lib/machine-api'
import { Selections } from 'lib/selections'
import { kclManager } from 'lib/singletons'
@@ -28,6 +32,8 @@ export const EXTRUSION_RESULTS = [
'intersect',
] as const
+export const COMMAND_APPEARANCE_COLOR_DEFAULT = 'default'
+
export type ModelingCommandSchema = {
'Enter sketch': {}
Export: {
@@ -107,6 +113,10 @@ export type ModelingCommandSchema = {
selection: Selections
}
'Delete selection': {}
+ Appearance: {
+ nodeToEdit?: PathToNode
+ color: string
+ }
}
export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
@@ -664,4 +674,40 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
},
},
},
+ Appearance: {
+ description:
+ 'Set the appearance of a solid. This only works on solids, not sketches or individual paths.',
+ icon: 'extrude',
+ needsReview: true,
+ args: {
+ nodeToEdit: {
+ description:
+ 'Path to the node in the AST to edit. Never shown to the user.',
+ skip: true,
+ inputType: 'text',
+ required: false,
+ },
+ color: {
+ inputType: 'options',
+ required: true,
+ options: [
+ { name: 'Red', value: '#FF0000' },
+ { name: 'Green', value: '#00FF00' },
+ { name: 'Blue', value: '#0000FF' },
+ { name: 'Turquoise', value: '#00FFFF' },
+ { name: 'Purple', value: '#FF00FF' },
+ { name: 'Yellow', value: '#FFFF00' },
+ { name: 'Black', value: '#000000' },
+ { name: 'Dark Grey', value: '#080808' },
+ { name: 'Light Grey', value: '#D3D3D3' },
+ { name: 'White', value: '#FFFFFF' },
+ {
+ name: 'Default (clear appearance)',
+ value: COMMAND_APPEARANCE_COLOR_DEFAULT,
+ },
+ ],
+ },
+ // Add more fields
+ },
+ },
}
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index cdf89b01e..19d2d144d 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -66,6 +66,9 @@ export const KCL_DEFAULT_LENGTH = `5`
/** The default KCL degree expression */
export const KCL_DEFAULT_DEGREE = `360`
+/** The default KCL color expression */
+export const KCL_DEFAULT_COLOR = `#3c73ff`
+
/** localStorage key for the playwright test-specific app settings file */
export const TEST_SETTINGS_FILE_KEY = 'playwright-test-settings'
diff --git a/src/lib/operations.ts b/src/lib/operations.ts
index d3f8ba6a9..c38a4aa0b 100644
--- a/src/lib/operations.ts
+++ b/src/lib/operations.ts
@@ -33,6 +33,7 @@ interface StdLibCallInfo {
| ExecuteCommandEventPayload
| PrepareToEditCallback
| PrepareToEditFailurePayload
+ supportsAppearance?: boolean
}
/**
@@ -204,6 +205,7 @@ export const stdLibMap: Record = {
label: 'Extrude',
icon: 'extrude',
prepareToEdit: prepareToEditExtrude,
+ supportsAppearance: true,
},
fillet: {
label: 'Fillet',
@@ -228,6 +230,7 @@ export const stdLibMap: Record = {
loft: {
label: 'Loft',
icon: 'loft',
+ supportsAppearance: true,
},
offsetPlane: {
label: 'Offset Plane',
@@ -253,10 +256,12 @@ export const stdLibMap: Record = {
revolve: {
label: 'Revolve',
icon: 'revolve',
+ supportsAppearance: true,
},
shell: {
label: 'Shell',
icon: 'shell',
+ supportsAppearance: true,
},
startSketchOn: {
label: 'Sketch',
@@ -280,6 +285,7 @@ export const stdLibMap: Record = {
sweep: {
label: 'Sweep',
icon: 'sweep',
+ supportsAppearance: true,
},
}
@@ -432,3 +438,37 @@ export async function enterEditFlow({
'Feature tree editing not yet supported for this operation. Please edit in the code editor.'
)
}
+
+export async function enterAppearanceFlow({
+ operation,
+ artifact,
+}: EnterEditFlowProps): Promise {
+ if (operation.type !== 'StdLibCall') {
+ return new Error(
+ 'Appearance setting not yet supported for user-defined functions. Please edit in the code editor.'
+ )
+ }
+ const stdLibInfo = stdLibMap[operation.name]
+
+ if (stdLibInfo && stdLibInfo.supportsAppearance) {
+ const argDefaultValues = {
+ nodeToEdit: getNodePathFromSourceRange(
+ kclManager.ast,
+ sourceRangeFromRust(operation.sourceRange)
+ ),
+ }
+ console.log('argDefaultValues', argDefaultValues)
+ return {
+ type: 'Find and select command',
+ data: {
+ name: 'Appearance',
+ groupId: 'modeling',
+ argDefaultValues,
+ },
+ }
+ }
+
+ return new Error(
+ 'Appearance setting not yet supported for this operation. Please edit in the code editor.'
+ )
+}
diff --git a/src/machines/featureTreeMachine.ts b/src/machines/featureTreeMachine.ts
index 614762775..6a5c6617c 100644
--- a/src/machines/featureTreeMachine.ts
+++ b/src/machines/featureTreeMachine.ts
@@ -1,6 +1,10 @@
import { Artifact, getArtifactFromRange } from 'lang/std/artifactGraph'
import { SourceRange } from 'lang/wasm'
-import { enterEditFlow, EnterEditFlowProps } from 'lib/operations'
+import {
+ enterAppearanceFlow,
+ enterEditFlow,
+ EnterEditFlowProps,
+} from 'lib/operations'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { err } from 'lib/trap'
import toast from 'react-hot-toast'
@@ -30,6 +34,10 @@ type FeatureTreeEvent =
type: 'enterEditFlow'
data: { targetSourceRange: SourceRange; currentOperation: Operation }
}
+ | {
+ type: 'enterAppearanceFlow'
+ data: { targetSourceRange: SourceRange; currentOperation: Operation }
+ }
| { type: 'goToError' }
| { type: 'codePaneOpened' }
| { type: 'selected' }
@@ -75,6 +83,29 @@ export const featureTreeMachine = setup({
})
}
),
+ prepareAppearanceCommand: fromPromise(
+ ({
+ input,
+ }: {
+ input: EnterEditFlowProps & {
+ commandBarSend: (typeof commandBarActor)['send']
+ }
+ }) => {
+ return new Promise((resolve, reject) => {
+ const { commandBarSend, ...editFlowProps } = input
+ enterAppearanceFlow(editFlowProps)
+ .then((result) => {
+ if (err(result)) {
+ reject(result)
+ return
+ }
+ input.commandBarSend(result)
+ resolve(result)
+ })
+ .catch(reject)
+ })
+ }
+ ),
sendDeleteCommand: fromPromise(
({
input,
@@ -138,7 +169,7 @@ export const featureTreeMachine = setup({
sendDeleteSelection: () => {},
},
}).createMachine({
- /** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0ANhoBWAHQAOAMwB2KQEY5AFgCcGqWqkAaEAE9Ew0RLEqa64TIBMKmTUXCAvk71oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBEFFcSkJGUVFFWs5NRsxMWE1PUMENUk1ORSVYWyCpSkxFzd0LDxCXwCAW1QAVyDA9hJ2MAjeGI4ueNBE5Joi6S0zHKkamWKDRBkxOVNGvJVstWFZFWaQdzavTv9ybhG+djGoibjeWerdrTspX8UGlS6TYIWoyCTHMRZMQ0fI2aznS6eDp+fy+KBdMC4AIAeQAbmAAE6YEj6WDPZisSbcd5CTI0NSmdT0uQKawnNQqEqISE0CS2WpFcRsmwyBGtJHETEjAm3EgYgkkfzcQLBUJTcnRSlvKKlQSFazSKxqFLCOTQwFAxIyKTCRQSf5qZkOGhmtRijztCTYCCYMAEACiWMJgQA1n5yAALDWvKY0pLWSESdQyW0OKS2NQJrkIOTWBkKBRSFk2lSKBPuq5QL0+v2B6Wh8NRxSRCmxWMJITWWwSVknayKSq-YRiazZ4x2gqQxwqHbaY0VpHV30Bx4E3oYaNa9szIRibQSYTWenG-mVDalHKH9L-OTGM0phee73LgBKYFxqEw+M3bepHaShQMocijQhodjbEU2bKAU6QsvyYhwkoziuBc4pPjWPgAO5gGATA-lS0z8LuHLSJU4ggccNRFtmCYJkm5gcmW9hSDQ8IoYi6HLgAMqgABmTz0OMW5-juAGrD2uYwrYt4pI0NH9gamQ2gUxp5NaZzsWh+BLn63gRlgmD4dqol6r89o0A4xgyDYjgODRdI9kcCjTtkNSPtpz5+gAYtgmC+gJLaar+hGzGIRbSDIM4pr8SGcsCtgWAe9gqHINCrKxjoaS0HoeRhRARnKvGEkZ25EUkoh2qIDhmCx-wnHFF5WCo4LaIC5TQvMhTuVWnkriwBIBUJwVxnquZ8tYWiHmlCYWKOwIgTBLonFZkIJre3U6QQyAkGGJUiWVo1gtFZZFKsh55FBhxgsskIsmpD6aTlPUYQAIsEIwqr6arhIJLzCSFQjpmkd6HLeLI1MI2b2Os405JFsjHGaTSPZWm0ACpgI8gjsKgghEAAgi9e0AwBqg9loSjrFd2RQZFvJaJm9SFIcpYbb12K8bxQQBEwxK4KMv2tgRI22s1jTVcOZYpOIUEaAac6ZPYuQsnIooo4uvUABJtHwxMjWFaQ-KltgFOI2w0b8DIwo6VkOqobMYQACgSqBdEw7DY7jkAcHr-4iN2HLrDkE1mmyig0fYFT5CbA6HNYavZajnkSBG3pgI7fNwAQ+MDdgvEkKE-hQAqTARv4LBML0xIjBAvsmaIki1CxqipYCwdQfMzXRc5UW2rYDu+hIsARqgmEZ2QWc55w+eF8XJCl-4YBu5wkB1wde5pGaqg5FUbLG1B-bAxY5SZnvqvIYni7eGG7CRsQZCUJg1+NmviQpJF6Rsnm2wWZFUGZLsBCRQsjWRqilDaz9b4RgIG9fyYAVToilK-RAUIJCdyLDkR0XYYTZkEPHSQE09zW1NLmEOECb53zuBgReEAOD+AgL0Ak7QGxQOQQgTIeQewOFaixco9I5qlF4aYeORYGY2i7G6dWnpIGRgkDIiMABJDC20wwIIxFifw+Jc7kBIIZQWQVhb-kVkBSyeR36VHkLTdQPZ-hlkaMYVmUjtLyLkRQxRyidrwKCIgjRI8mEAC97i6LYZkLs0gcgzjNjkLIB8UrgisCxCGVE2KX2kW41xjYlHLiINwWA7AFR4H8H47AgSsS6PodgPJD8BaBRjPtN+KQDRhwWgmQEw5IbxWsgaFKKYYTqAieQxsGSoFZL9Dk3AeSCm4E0YSTgOjMAVKqRQGpQ1DGiUyMoe0VQiipVTBkeS9JwTZBDkoNKmYL6oSesM2R8jRnEFyfkkghT8YACFvD+AABohMUNZA8hRenbCYueRAE1jSwQtJHY4FlkapOcek25eUHlTP8K895ABNb5vyajS1yBBHBwJ7B0RAcOGo2R5CKEGVA657jslIqedMsgUBfTfLBaIPe6xtBlh2DRK6B5bwnBdJlHIlKbluLueMyZ9L-C+nwOwKM+i6kkyUpIMKhwzAFC7LaIEF5Jz2iLCOZkZ0sqXNRi4hFtKJmPMKUwQkNrcC0PIFXEgMpaFLMoCEpaNilA7HTP8F0DUQVmjtD8liFkzTN3AU4qsZqxWIstci4ppShh+X0P4XR2AoC4G+ZkNBLJrJLQQghS6I4bHrGhDJAcIqIzUvFXSwpWi5m6MwKm9Nmbvk5D+coDMKZIoCJQfqNBgqHDbNFlWmtcbJWFNgLgee-gcafO+baJM6YSXrAaGFWWCgDx2FvMcKmGgLkcThUM81Yy63TOnbO+dGKFX-TjJkJdeZIRWBAqlUCNE0pgmOH2JYKRjTGqPdG+FsaLWTumWAAAjr0cpMqoByu+aWHs1lDyrQNRYKCxhhDSEBOYewjoByHq0kBk9IGz3xqlUwZ1TbqC3uGkY6CfIZxrFqJmAcUFfjNVYkoB0-YqhhTHae+55HCkEiXqgfEtxz3sDJLRtZZU-1YdkD84dFgUhiCgmDJMP944QyqBkATpGCBvkEJjEIvQRghIQs1Ul-ZGa-EIdmPIkgbzCkhAhSKcgDOZIwlQ+B3sAgMKYfgFhkYWVglSgOVYvDQJ9oQEWXkZZ+wpRZLaB0XmRkTqtdMzCHAy4zoxBATRujegrL+nR9ZZZrPmG9UoPIlQobKEU6WLsM5-g7HS5GO5kZGXwJxp+NhbIZxJiKPDEbqgA05hIgoEc+bDxvopVG6lEh8aYSeQERNQSFmuqGMs-weBeKoAIBAbgYAvS4A-GGCQMAPYbbKZgQQ+3UADZAgaYwI4CgUUzBN-BWGcj8KmhNVYY6VtraKagAJm3Fk7coHt3AB2CCEhdgSCQvNhgHYJF0K7fhBC3eTQ9uHT3ZPGTKv2BC6QumFFvDN4cEd-hJjCqlayNprRSGB6tuhDbsDzKh9U2H8Pjv8zOxd0713BCc-mfjg7z2TAQWHD6vT5R5JHj5GpD7d4NAJxNVfdJIOOezK5+U7bvPHsI4JEjlH1d0eY9F+L3RkvCe1Lvf+fsS7WI1AsHueX9kt1hXkOIbIA4Fps9B6iz5fPDsC9O3gYXWOPYkAAEawEEHwe30vmraDSilU02w2QdIvBTUwYVIQcmMK0gDRGlu64CKHj54fTfm9R+wK3sfBAJ6Tynx70vG4jlzPh4+NQLYlpS2aW01p5CEauS4qvKK3n+DRXXyPQvUCXdF23wQ+hU9E9KokF3zUdju5+cmfsMgoa-3BMoM2tpjBKGD3Q0P8+TeI-BxbtH4PrfY7XxvzvW-6kgscAQ0lFdcRX4E-AlaWZdFLEcVYDQSRWFYjKlafRlX0BfE7JfFfD-fAX0TfR3crEnf-UwAcDPU0OcbVEFZMdINpVTb4coW-AIJA+BR-M3Z-RvZvVfTAsAbA1ZYnHfRwLDU+CxMwT9OSeKKKUwDIbPCaUsMwYHJgXmVNWDOVSTYTLEI7VA6PZfU7fGbwNGQQO4ZQwQBQ+VHAuTHgqwTteOE+UsdQdTeKQ8O0QELII8MtayTMGQuQ6VTEODMuPQsDdgevZgy3N-ZbbQ3Q89Awzw+DH-EmXfa8TMGEA2UsYFBANaK2FiPcGaDkGEWg8uW1TEB1J1F1SpaHBggnVQwXdQ9Aj2G1AkO1fI4kAkQQI3ZZTgsrEwv-HNZQKoVKWac6UAi8GScmcoIoKwDId7bI6o2ornAonnXbRghvQIjHFvCYvIqY+oxooo6pFooWbg9o4NFkC-Ho7+GiPTQYwCEYl7GFLXNJIZLiPAXrVAT8AgbrfAe4-rKIuMIOSqNKXMU4c+OQDTa0AgklFKHYZDTXQDJbW4-mOdB4zACQBRXADgbOCAQrKpAacuVAPAQaVonYhAC0AhL+WwHTRnf+D+O8LsFMOXfYMdKE14uEhEpE0gZZPRYw3EhMWoCg-1GbCRdDeaCwXYL7LIUsUEXDGku4mEz8CQXAN-XRYgTAVgLxNxNhQENKcaaWGcDkaEYQ0oH5HNFKQ4PsekU2cvSfdJWkiUuEnyRE4ecuF2XiXyP0RfCo07bmXoJgXQnAKUQQNgDAb0txb0sAHxaTOQZUpWaQQsHdXg-sA+MFIsZyeYZQAcfjRbFxc0vrS0vASpMuJgO0h0-w5HFgoI10908gT0rEb0tOP0xsAMoM2AEM94-8UGNIRwRoQ4IY0JWJBkcWD3FuI8LIlM9JBkvwthR0MEJuZ9GoHYcGf+KoHsBMRobeJaNKMdIcggKgZsLg7fRAPILDDZU0ctUBJQA+DtYBeYLsRLS4iElxNGHrLEbAcpZ1cgGEp4gqF4i05U2SFXBGLQflRXPk5XNkb1SwYvW0MdG8l4u8h8gkJ8nGeExEvw8ZTgXAErRePgIo5hHMviB05U4BL8zo1iPpPolBNlTtViZiRoGwVnAcoZcCmASChZR8mEqUmUzAOUhUkLIwzc3-PE3eQdPIKoZTdQJI5QG0caffDINSAcFJK449KlWiqUe8hi6Cpiq0rM20rC5cJ087DQoePwN0j07AL0n0jg2Af07xdRaTYQZUo8O0bBMwW8DUxwDDfcHtW0CESmLqaiuS28zgKCmC1ACQVSm0zC+05cJ-AshYzHYsgyoyys0y6s8yqUWAKyhs0SVrQ2f5BQY0Q8eODTcQdIBxIsECLIf-MdXoe1VAQYZ1dgeRbEXAMoqPbSy7LQnQ8q47b0oYAaWqrNVK+TdMMERwEvYCaJSEDuOnSoYqpLeQDBMdN8UIegi0l8nrd83qq0U0BLVVTuMwBadjFkenDIcwMbfsuApbOanbJlOk5bdnJCqAFUPQwrO4AkfmAkAgIKsuUTea9g5U7QeWDkWQN7QHcOMA+QRYDIVKSaTVCfU1dJM6ha9Mq6tbZhcHDNPAZEwrD6865A5GqAPAb69MaQSrIcIkzIC2bIdIF0MGlKVSKik6lxWG9gi0wKzM4eSAfwemi6hqtAl0vSksssj2YyqsqBGsiymTVkrcvEiaO0b4yKcoVkerAleYK2BwzU40YhWakITGy6kgFE5hCABUfifwDG+gzm503Sj2fSvWkgfiQQI2r61axAdQaxMiyOVYfsGJBWmoT+RwH5RiQOTzLy2Rdm5A+G7W2hYLS2g222jm8Kl-JvIs7HC2-Wj2KO5le2vE+wO0aKUsNKMweQWLPMF0PlPMR0BCUc46mS+A2RIgKUYMIOukpat89MgbIqvkF0ZQaa8QBQPPFBRiPVXMH5SEQEZM2m9JauoMGUOuxmqvZhR656165m7wmumUFO0rbY8W6EewUwTlQ4eYINeSWQbDR2uxekaFMdMe+sSe+G6e4LbG1G-GFE24Jew2jWha2+nqsW7i6ELIQ0fBdKAjb7M5e0HYPcOwAoBys+p+y+2Epm60-SQrc+2ul+9gk2pq7m9gfS0swy8sgW+KoWxKrEWAaway2c-9Lug9GcGwi8ZDLhWaWOBMEBiB8etmpBi6xm0O+hJOx+phlelBmPbmQQROq2j2Sgcem2lh1Oj+kmL+6zRnZiYgyxeKf4NIF0PMccCLFkRhi+8RrWh+iOgIER+sHhmOwsxY-hwR62gxwkMRz6i66yzhbYA9Q8hueyTZQoJuZTCyLPMdUssAMgQIauP0EJL+cJHeqmH2jQDuaxVyBSJQGSGmiupbcZTnYLedfLVm3EYrR0tQ1BiQEgPJCVLLAAOTlEgAADVMnnt45P41I4jpZSCEBRjG4shd4Rwchboz7uBknbrUmSnCsMnMASt8zY7m88n2ACmplimCtymBnV6DE2SfkBreFHaAagbSgRRN4shxARxMxVBlBsifCstn6egJMDmplpNeGdL2rTn6UxHjmODrmsTKnXtEs4J5AGINMl1-htghKQJjB5wA7q1p8Hnplccm19ALnLsrmwjQWU1ntrEasLB+rekJsmJmzzybRUpVIrB9nz0ZltEwWIXTsoX9DbdYW07SdAE6Q-qMh0xYsFnJB0o5Y1oqdwSK8p9rqlCwMwcIc7tm000cBM1CWJBiWwMcdkak0wXW8BX36uLojNmkNIyyLbQFBLohsUwIlUhbAy0cXlC8XG0U1+WM16qtKY8RWssxd9d5lm0pWjWnm+V7B6RshUgWQoJBDyYsgf0Nlh6En2XQdgXAgZ0mAYTPkhWzWplvTA3PZk84W0hWz6g8g2RMxN0JxzAapTRv4JodWuXL0g3r1Q3cZgWI354o39BnszADxHRtkKTqsP0rxVBrYPsVJrIs3DnINoMFlDD83Qj9C227dDC4WEtQHjhjQFAbAUXhwDRCgixRiqasgW3kVKMFQ-IsAu3C3F3qMWTZW4wKX7R2TMEt4sx5oixmo5cmQ2yoQTToahl8plq0ZYS9suY1y07rRchDRagBxppSUD5oR6JrRRBRiWyz7XyYB-A73PwH3YA1yNycTxaX2uyHEP30wv35pmJf2bQYRiqTggPb37285IOqBrBJGPiix4P33HQkPahaZjA0P-3MP4mrzR7gP4EwOFk8O1yZAiP-w4O33EjP3KP5pXIaOMPn16O2XGOcPwO2OqAVBOPRJuObQyO+OUW24hOAOsOAWJAb23yWOIO1yxBZOyp5OEPyP+QUW3Wrp0O1OgcNOtOQOdOpPhADOrQSOePEPTPXX9wLPaORPL3tdr3sBoLg7YSG6QOm7yWQC+Q-0bQpLsV2M4lREbY+xR9-aR7-PAvLrr7bq360bbgAufGlQmEcaZWYPP6LIsNPdrDcx32oYN4VdooThzQh6z68ugvJTMu2btbsB+h57YHcv0u2Fy0DQ87QUdhQQiKGnXb0hGg9wCNYnahmv0vGa3rWaiAWusnyicnorMHYrfTcHIxhakqVBiGBSbA90LJlB5a1nZBJBVYN5ch6HVAoa-OqVVvFuQ6dbw7OHyA1uhWzH3S9HdC1uBv5gjpGIOpQY7AoZwauFMXj3jYQIFv8v3uw7bq9G+ufGhmTGoqE7-uk7Af+u07wI98qWPd0wi0CU9x8wdz-UKbxBsPtPoHYBeYOAbqOLy5sAbVfvmf+b-SmAOeOC85BB+ZIBV406HReRQSWNxBmQbAxwJoxLpqnDoCUufXxOGfJSmecB2BWe9vsz+fMfIqh5ufBb9u+ebUHteIhecIMBa4xfIoVV2oEx1gQIUxsw9xdgFoPd5yfaFtUuXumPQPGeea2f0dFqTWdKtu+aKzduzLAyRa15Sh+k9hQUnNCgfa9Ad8+NTAc626FAcNWXTTr2A+WOza3SQ-wdFrjHDfI+sH+a4rY-ayE+HbMxk+GZtg0-1gM-A0QJs-xYuios7BEfg6IxRN4EWAsTFrnjQvYTlSPdwQwZKeTlu68TBO2UrBZpS7LyxO0ukeR+cIMSJ+r7rrmF7SCQ8kD+VD77CtT-z-x+sRvr8qwpok8wzR+lKGHaIkuEwEHEOQh-es9+x+mJDREf0RrBYgg91C-n4Sv53VuAhWO-tiTXrcULQWGa0GbG9T0grA2YcCAaDSK1A-sFMWAqrx37D9R+kAthh91R5fc1uc6AAZAN+449GiePb7ul2xgADBA8A76hkFIg-ljkO6CbCzF5Avp1W8gKoOXQY7ED-+pA+AVPWP4pNU4BIOAUAL8JvV0eJA-fhwMJ4F4qoHUNxhyiwGFAVU+6ayNpj-40CpBSgpbgvVZrMDd+5grEvQPQa81a+0fEyg3xFpiBju26TuA4SFCrMHaU7NBIeHkAQQ6YW-QvlAn8DSl-AM8XSKqACxgB84VcHmJnDYTJBygheUfJRAnbu1Gov2bPJOBHBlhz4G0MgNgC6DDBmE86VHPzCFalDyhIwKNjEOVKiVVaI7MCEmXG57wew2eFphkE7glDES9QyoZVWqF+gq+r+RYnUOGAcEcYggJoWLzoiDZiwMIdMByFvBQxKgSUOOJFmf5PdPQUw1nvOkxjoUwBbiWoYMOmFRtdezQvgn+jCgpBfmwxMcFeAsDkQjwtENIgMLKEVCUmlVY4XkmYS68DeEwzHAcJmG4xrh5LRKOdwwFUk+42YaAvRFcjGA0BI4DaPjFkLNpmEmMfJOuHW6NUY8OItcBgHxh5J3As-VuqmCsBWBcUCjfot-WkjFUbAlkaQotgxFyFsRq4PEcCLjqLEiReI0kewHJGE9eQR4KkT2lpH-F4oeaNBJCFkD2YWRvnT0OyKxE31OY3McuJnCFZ8QuYfgcePzEFHCinOWwCoMpj-Y1B8My-AcKlFMD8ouUdgbIHsO0gqj9ASNdUSiFGE8jm8Oo7mPqLACGj0AbCFMHsHUAgRvi9IGKB+hSAFU-cw4OEKCXRGYjXRwWfSDgD4BCtUx2APgAGNt7GiEAP7BNgmDay2B7Mfg5IkWBQGaoPcOVYIYmI5EpidYXooIpmOzFkjAxadAsfGOLEWhoy8UJzLu17xiiKSqsOsaqNuqwBsIuEX7pOKYA5jlSKqC0esD7iQh7Ay-btPaGSzDgqgFoX-myKTGAiZxTY0xjOLnGE9fsSjWyD3kaT1MnehscQlYQcCuQC+qMF0cwnlL8QhW749gKeLzF7g+Qh4VYCyAKBxjOhF+NBG1EHBpRMgBQUccmNupfijxmOL8T+K3b-hhw4IayP0n1Tko2Q8kb+m7h2BJZTkw4WCYCNTGsVw+l2Fmn5BQklcpGimS2EKT-Q50pRF4eYM2VeHBwvmbIUiWAPImISh45E2iYgJJhLpqRESQsKrAUguNJ2vSFdI4QQgq8ISr44LKFX8hCs1JfgYSXM3XoClMos4XeMlDLEiJOM5aKSG7RTBKSK8Kk26ppL8LjDeRmOOydpMVRxgwoB4IIZFFLCOEqcPKTet-AdBcofavE26t1i6BFQXqlE07GFIikuSnccnNIO5VqDZ0WMuVWwjmi-i5BTkB6aSspP3HBYYphIASYVIJBxTcCVoBkDSPrbiwjUBQGiCSki5Hg5YCEHeCFP8DOxXY7sKNv5l+69B48XQDgOwJdjLwTMtCBATpO4rbA0E4hcQraE37mx4oE0LDHn1WG2Rwaz4xcDZPanDSupswnqQ5ObywA+pA0qojtI9j+YgxN3eIlBOHSWS1xcIcEE4TsCSEFOVkq5FtNgR+AvEcQqYKG3ehgBfAX0OIGVLaLsJN4E0bYG80VqqAbQNEA9PaChQrAe8uYNqZ9I+hBAgZv0g6UEQ8BfTAZGtKYCDNxIDgkwFkBKJOXJRFDT8P7E4FVyNCtZjgLgFCNKQwDwAoggGVCSZArEeTxsYMR7sv0EAPTpszkRSQ3FykV5PInMg6FuLQQ1N0wFkEuvUy0C8gRZmeboi6FE5XJk4qcDAH6LZkiSRYhyekC4Xlkn0N080DhOGXOQ2hMwlMQgRCWTjDxR4esqWbMBzyDFMiY+QVMJVYgEJ0BL-DWSjIBauzEAqsBkOUBf5hiUw6qV1q3zAi94HWmsq9lSlPQhz2ERYoEmO1bgjhjApJCXnuGMF3hm2GnafDCxmIw5HsacxMlhmHA-VSwNgLohHHl63g6YfuUFNZz96yJp8pLcuSUQOxVzVYvIaao0CWBBCVWijUSnHEkIdQRK2RGvOHgHmIYM6lQFKPLKzwWxTQ4ZHccfAa5KjZKXcjlvfgXl0T709bRyLmDgi2APWrErYOd23SUkQeJsJ0ZXUBYcsFqlck+UYlUBggosMUAEOsJEJDYr55QEsFyhdBuE+WhhTlllnYADzASOVbyRYQyB-x4ooJSLgrLbrMRIo4xXIvalWLOpe5x8g2V-PyrVRIZSHY0McTpw-Jcgdhb4PHDFLQl0yacuoF2QjLkpS8ZYz4PaHTYZhDq2zRhZdSHIsLvJSYb9HmHobJR3+7CH2l4OHHZ4YQb05ObIjTLQNpSGOXRGnOhBhJ5yxoSiqrBSi0wOiDjCJC9mtBhDlF1aVRZKRUEhUHSLC8lGIoa6-NSwVQKxGiwBr3hd6K5eCmnILDghtmfGGECSmkUPoDQx8Aumul3RKLnusieSvRTTTKUcYLCl3uNH9wOUZuVo9QM2TFlNw3ckaTudWniW+UlK-lOChwAcXf16g2g93pgPmhF4PJQoA1LdA0YadililRJWUvUXlDMALCrQdinWqshx5OpFLFsLiKM598Sc2JUUp8odLGKsFWxbmV9AOKHAASyKLTKQ7WQNMJaDQOdyDgBypl1xKlG1UqpokaqbiOqmnIUjp5C0F+MCHUp1JhoEZSgbIEDB+TizwhgdbRhaRYXxwwQ0tSrqsJvnJFaZ9oMfAUG2AxRfeRAqlFAza6yDxxIQWAZJieqEgWFIxcaFfh+A+oJsB1QQY0j+zfptA6tGxq1zhLtc366K+YPTj+z8JoYN48GElGgJvZBqjgElZrUsGwNWaddFhQzj1QU1zAuzLlFDHPJOKtqg8wEOyrhrQNQ6utThivV5UsRuhZFB0UKWkUp95+mRTDCCQ2lHKq6kDb5cws-lpUKS5+VeUUEoh1T5ocRWIsWA8x7xn5iTA1aSoy4IqUVz1FhdD3MAmhVY94OkSCkvB6oNA2c1ph3JhX6qmGcK8lW6spXGqyoawewnCBSgPDoC6qp5bVhVgqM+ye8l+Zp2dUcr4ay3eBvmvoK8qSIksbhOhxnDAr8E9hP1BQycxFhNGiDF1eQMKxo9LGy9bRmWpPagQgYPaEcPZA-hg0Toh5fuBpx8Z+NYAATKuV2DSAGDxwGdQocCr4QIz+UDQRGF2A6bnZ9cvwyIb0yKwzMB5pCgcNDCDTqBhlWwX5hJCPAqR7uBS8Na-L9a4tRMdzaBWc31kTSlURQRSJh1YyFAjSGmKJg8PdycoYQuq-eU+roT+sYWzaKuT+vtB-rSctoY8igLaRd1VApsD5ZYoRrQbcWpLODXGrfgIboJg1ZDVwswl8gbApoLICxGHAWB52UqWDS2mlbwaW+0Eo8ICB7StZLovyMKPwQLqH4c1leDlv6wI0sajWbG39Qai41NRCgrrPMIOnu4NwKFEG3NUC1xY5tg2HyY9ZIG4RKQR1UkWWH+LBiKLUBc2GJXqqg36NNNgbYNmil01oJA8XzRMkZviidwew2i27pht+aMbCkvbDthEQjBSbENMm-6vby4UYqB+vuIIafD83TJ12y7XpURpQSM4YxkWWJvYnqZ1QcBvqQCiVSYj087OuHLmGnPJQMgFIhE34FUC+yOZy2X2f9BnhLrYbplmnagUauIUmrFNCbM7sOku5bBQYeqBMJUBsr2JTBIAyYDfUK54B0V6QzqFZkBQpBgVkMk9kh3cz8bMg426Bu1xfCdd+gnqz2oWA1lbM5RUPA+kh1VhlgjSuYNTYk3a3QMi1-gV7j43RXlsMBaUAENUChi-AbutEMsLsoU6OqXEz2slbkwoEcMhGqgsAOiqmm4ZA4KYTImWOjkMsEdLOQeUBKK3MdYS8Gzeb7NyA4qlAE0KGDYDQSrDBUQlb1ZjsD4a9ueOvNxOzxtQDye+eO2WlFjsRYCNA8-SOH2WyoGkqdJfYsuXxlAdav127WzL33LQiV8+HOydkDA1zATgpNndrbQOkEi7XJjZCRF4OSmnJChWAtKAxJZxHAVhnlQpW1re4q6LBE21njfx5hKDFVzUWjeqjqjjgsBoqsfM3EG48Sld5uuwcAO21urwByK+AbytvBOa3GCbJyFgIkrghoUpwLQPIHG0W7D+Mq8HR2uV2+7YFKWvEiOAd1ngIxDojQNIrjixsDJuQOwJygsWtaQdkg9QZbv92gDum8gxQViXRXdhi6yU6CNymBAzhqO+oQlBElED2zt+L3dPbXuT02KrBD1UfYAJb1Z6Zw3YSLJs1YiDV+BtQXYG3FibGyTYVaSIZVRiFpy8EJaN7ICG3hOELoBKThMpGZGFCYC5YRbAcOGGaiJ4ac8KEELsJ2ATgwg+qZIGTANaIQj3L4UML3X-C6djYPxVTwobX5KKwpMcKJWwRMRBVisNqfyIwAH67AdoJCMy2ppmBvsqsedfeHeG50pCbUn0R6MzhoHy2wDdZfQ3hHWqFgbUN3HloegnUtpLYtA-2CTAiVjg4iZQIcA3kEJ2ynKCau1j3H1jxxM49g9Zlrl4HHurZNcV-SORQSQG3qR1VtK-FoHEonKPuLUFWjmLvcDLfih1BHZ5o2p1E5LZ1oOjmB08Ze2hXOsaSdCXQFQVLFqlITgw2pdkjQ-8qW31zxCqsCOOxuOjjZzkt2raSVLQMfxoQ5oF3nHE6GpYxFCnCwGvxa3Kj8pt1DqSNL2ljS0D1iPcEXJ0O4ozA+9bpA3LHaKTDgqM-6Z9AJn7R1dJkdA+Tn5nNZ-25PNZsvrnIbJl9LMSNC4CAA */
+ /** @xstate-layout N4IgpgJg5mDOIC5QDMwEMAuBXATmAKnmAHQCWEANmAMRQD2+dA0gMYUDKduLYA2gAwBdRKAAOdWKQyk6AOxEgAHogCMAFhXF+27QDYArAGZ9AJhWGAHIYA0IAJ6q1ATkPELFkxf1qL-NQHYLNX0AXxDbVExcAiIyShpYMCoWDAB5UTAcTBlZAWEkEHFJaTkFZQQVJ00dPSNTcytbBwRnXWJ-fR0Op3cXJzUwiPRsPEIwEnIqajBZDEyAUQgpADEKOgB3PIUiqRyy1V1qmoNjM0sbe1V1TSd-XV1+XUM-EyeB8JBIkZjxuKmIJJgObpTLZORbAo7EryArlFQWI46E71c5NRD+fxqdqdfivDQmfx+QyDT7DaJjCbxWgMOjzHA4Og4CFiCS7UqwxBmRG1U4NC7NFSBYgmHH8O5VfgeR4kr7k2L0UiyKCMVgcLg4HjERLJaRK6jasApSDMwqs6H7Cr8IzEfQqby6NS4-ROK1ONEVfT6Npefr8FTmZyGXQysmjeV0RXK5hsTjcEgQOQ0E1QvYciqGbmPOpnCy6d0qe5OYjGFyBEVVExOENRMO-BVKlUx9WaugZWSRgDCdABAAU0LIaCxu2A+wOQQOIMmzanQOVDiZiA7XfwXK8MxZ857vc7Hf7DIHgx9ZbWSAaUpGtYDz3qz3NJ0JttP2bPELoLEWESK1CYTK6TBn3U9It-FeT1DCcSs-FMatvgpS8dQvBMB2oKdihnJRVH8TNkRzPNLgQJwnH0YhBScAxBQsEC7mJI9Qx+EgZjmHBI0WFY1nWeDDV1KB9SvO9ULZGEXwQIwF0MTwVDxPEN3wyTnkXL1nl0SsnmMUJaJrejiEYzIWKWDBVg2YgkKTB9ISfISMI9bDswaPCBUMDpF3En13CscSqw02DYh05ilVYgz2OIUQ8FENA8ACrsAFsov7CBqBMshZAANzoABrEhjy03y9LYoyQrAMKIv06LYtkCAEEVVKWDBXIhAE800wg1wvRFZ1XjI55-HzTwLBIlzCStbxKxooZNLgnL-P0wyOIKoqwEiugYri6Z6UZYKKEwZBGSi4gsom2ZdKmvLZtC8KFpKpayoqqq6Bq6E8ga9Dymam1lKAjqnjFfNKmI0j-EMSwfwxMUYLlX4ASobiQSyaFOOvHjb2NMyWTQ58rM9MSJO0NQHXcNR+UQLx+GFfgMw0XHcatFQwZPYzAWhjJYZyYzExQlHTTRyzyl-GzeVRfDHL65xCNXf0TGCfdaa0yGgUjGHavpqHI3YPicgSxMktSjK9rouDZcZ0E4YNlW1bkSqUru2rHo5lN0Ze79SeeXxnluKp9HdF1-EXfwyMOIiCZAw8xu8iGGflpnFZNpVVYQuRVoZHANq2nbdfG2Jo6gBXjfDmOzdkC3qut+rbYsi1+gXX9nbJldfbtd0ESxIP9AxB1nCtd4Q-Bkh6yjOlE+IVsZk7YdR0HUf+zAcdkfyVHBItZ02lFN9yy6kx3TMPrRXMAlAc6dSu7p3vGH79aTPZ2fOfntMuS0Y5bIF5pAczQHAn3Aswg+WRh3gAp9qIR8XMLQAFp7KIGARLaWcFJhgEAdfYS358xkXaFRDMmJ-TKXuFA8MkZGxqjjHAxqwl9yuAeC4ZS-QAZmAdPmNQuNiw7kopUbQv1sF1gjA2aM+CNSnjVkqQhz0iZWmLJJCmLdTCNFkkYVwPoCaVB6PoBEwdSTp3YbgrhsYeGswHAI+2VxfZaGUnvDEPRvpSMBm4HcGZCJeCUWwnuHCoyqk0S2NsI9eyT10dzRA4FNBkKDJWAI4kCxqE3BY2R1iFF2K8t3eG3EvEWnAqQlclh3ySiqADbq+E6FFh-AYZSK4XAaH8PYuJF5byRgSWmOhfiUm9HSYKRygEgztFAooqwKgVzQRiXTCpSptGwPMkAtMCJiIlg0OBZ0HhEH4SMG0Zu7SMxdJMKUyaUAAozSqcQp4i46lpPfI0rJAoyZYl0L7B0BZeYWFWYdPy6zppBT6VALZVkNC1JcPUg5mSeoZhIopR4Kk94HxUaHBitzcqBSMiZF5cICze03i7PwZFtDr1kj+LEdoKEQTuD4EpPTsrguOpC06hVzqLWWuVGFr4W5uExDY+EZMEQqB6mKBSykvD-jOZ5Q+Mtc5Z0jkQq+gryjnF2eQwJVCQnugMFiPJnpCTqCsN+Upmds4syeVSlovz-EUKCdQ0J+FibtDJv6T0NcVydxBbE1VAqWbQqGfAqy-ogjEBFkwlcdpBQGuaEEVwhIMyCnMISV4NN8X6z5WquQSs5Z5zjpZO23iEBnO9pRZwIFN6BG8A3AkTtJJ+CtK6S1-81GcNPjgTV4lva40ou4YwkohretUJKO+OggjsucKGnlcFj60jWknIe7YlRdg8Toh1wqfFOWrYEKwnRfCekbRUIiNoai2n6A8EU1yw04NLX2gZmrAhFkDQcl0v0ghIOIqKama6nSbrCEAA */
id: 'featureTree',
description: 'Workflows for interacting with the feature tree pane',
context: ({ input }) => input,
@@ -160,6 +191,11 @@ export const featureTreeMachine = setup({
actions: ['saveTargetSourceRange', 'saveCurrentOperation'],
},
+ enterAppearanceFlow: {
+ target: 'enteringAppearanceFlow',
+ actions: ['saveTargetSourceRange', 'saveCurrentOperation'],
+ },
+
deleteOperation: {
target: 'deletingOperation',
actions: ['saveTargetSourceRange'],
@@ -271,6 +307,60 @@ export const featureTreeMachine = setup({
exit: ['clearContext'],
},
+ enteringAppearanceFlow: {
+ states: {
+ selecting: {
+ on: {
+ selected: {
+ target: 'prepareAppearanceCommand',
+ reenter: true,
+ },
+ },
+ },
+
+ done: {
+ always: '#featureTree.idle',
+ },
+
+ prepareAppearanceCommand: {
+ invoke: {
+ src: 'prepareAppearanceCommand',
+ input: ({ context }) => {
+ const artifact = context.targetSourceRange
+ ? getArtifactFromRange(
+ context.targetSourceRange,
+ engineCommandManager.artifactGraph
+ ) ?? undefined
+ : undefined
+ return {
+ // currentOperation is guaranteed to be defined here
+ operation: context.currentOperation!,
+ artifact,
+ commandBarSend: commandBarActor.send,
+ }
+ },
+ onDone: {
+ target: 'done',
+ reenter: true,
+ },
+ onError: {
+ target: 'done',
+ reenter: true,
+ actions: ({ event }) => {
+ if ('error' in event && err(event.error)) {
+ toast.error(event.error.message)
+ }
+ },
+ },
+ },
+ },
+ },
+
+ initial: 'selecting',
+ entry: 'sendSelectionEvent',
+ exit: ['clearContext'],
+ },
+
deletingOperation: {
states: {
selecting: {
diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts
index bb33e4700..7e5dd0ead 100644
--- a/src/machines/modelingMachine.ts
+++ b/src/machines/modelingMachine.ts
@@ -88,6 +88,7 @@ import {
import { getPathsFromPlaneArtifact } from 'lang/std/artifactGraph'
import { createProfileStartHandle } from 'clientSideScene/segments'
import { DRAFT_POINT } from 'clientSideScene/sceneInfra'
+import { setAppearance } from 'lang/modifyAst/setAppearance'
export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY'
@@ -314,6 +315,7 @@ export type ModelingMachineEvent =
type: 'Delete selection'
data: ModelingCommandSchema['Delete selection']
}
+ | { type: 'Appearance'; data: ModelingCommandSchema['Appearance'] }
| {
type: 'Add rectangle origin'
data: [x: number, y: number]
@@ -2172,6 +2174,47 @@ export const modelingMachine = setup({
})
}
),
+ appearanceAstMod: fromPromise(
+ async ({
+ input,
+ }: {
+ input: ModelingCommandSchema['Appearance'] | undefined
+ }) => {
+ if (!input) return new Error('No input provided')
+ // Extract inputs
+ const ast = kclManager.ast
+ const { color, nodeToEdit } = input
+ if (!(nodeToEdit && typeof nodeToEdit[1][0] === 'number')) {
+ return new Error('Appearance is only an edit flow')
+ }
+
+ const result = setAppearance({
+ ast,
+ nodeToEdit,
+ color,
+ })
+
+ if (err(result)) {
+ return err(result)
+ }
+
+ const updateAstResult = await kclManager.updateAst(
+ result.modifiedAst,
+ true,
+ {
+ focusPath: [result.pathToNode],
+ }
+ )
+
+ await codeManager.updateEditorWithAstAndWriteToFile(
+ updateAstResult.newAst
+ )
+
+ if (updateAstResult?.selections) {
+ editorManager.selectRange(updateAstResult?.selections)
+ }
+ }
+ ),
},
// end actors
}).createMachine({
@@ -2267,6 +2310,11 @@ export const modelingMachine = setup({
},
'Prompt-to-edit': 'Applying Prompt-to-edit',
+
+ Appearance: {
+ target: 'Applying appearance',
+ reenter: true,
+ },
},
entry: 'reset client scene mouse handlers',
@@ -3389,6 +3437,19 @@ export const modelingMachine = setup({
},
},
},
+
+ 'Applying appearance': {
+ invoke: {
+ src: 'appearanceAstMod',
+ id: 'appearanceAstMod',
+ input: ({ event }) => {
+ if (event.type !== 'Appearance') return undefined
+ return event.data
+ },
+ onDone: ['idle'],
+ onError: ['idle'],
+ },
+ },
},
initial: 'idle',