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',