Merge remote-tracking branch 'origin/main' into paultag/refgraph
							
								
								
									
										2
									
								
								.github/workflows/build-and-store-wasm.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -27,7 +27,7 @@ jobs: | ||||
|  | ||||
|  | ||||
|       # Upload the WASM bundle as an artifact | ||||
|       - uses: actions/upload-artifact@v3 | ||||
|       - uses: actions/upload-artifact@v4 | ||||
|         with: | ||||
|           name: wasm-bundle | ||||
|           path: src/wasm-lib/pkg | ||||
|  | ||||
							
								
								
									
										8
									
								
								.github/workflows/build-apps.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -126,7 +126,13 @@ jobs: | ||||
|           node-version-file: '.nvmrc' | ||||
|           cache: 'yarn' # Set this to npm, yarn or pnpm. | ||||
|  | ||||
|       - run: yarn install | ||||
|       - name: yarn install | ||||
|         # Windows is picky sometimes and fails on fetch. Step takes about ~30s | ||||
|         uses: nick-fields/retry@v3.0.0 | ||||
|         with: | ||||
|           timeout_minutes: 2 | ||||
|           max_attempts: 3 | ||||
|           command: yarn install | ||||
|  | ||||
|       - run: yarn tronb:vite | ||||
|  | ||||
|  | ||||
| @ -24,5 +24,3 @@ once fixed in engine will just start working here with no language changes. | ||||
|     chamfer cases work currently. | ||||
|  | ||||
| - **Appearance**: Changing the appearance on a loft does not work. | ||||
|  | ||||
| - **Helix**: Currently sweeping a helix does not work. | ||||
|  | ||||
| @ -76961,9 +76961,9 @@ | ||||
|     "unpublished": false, | ||||
|     "deprecated": false, | ||||
|     "examples": [ | ||||
|       "// Create a helix around the Z axis.\nhelixPath = helix({\n  angleStart = 0,\n  ccw = true,\n  revolutions = 16,\n  length = 10,\n  radius = 5,\n  axis = 'Z'\n})\n\n// Create a spring by sweeping around the helix path.\nspringSketch = startSketchOn('YZ')\n  |> circle({ center = [0, 0], radius = 1 }, %)\n// |> sweep({ path = helixPath }, %)", | ||||
|       "// Create a helix around an edge.\nhelper001 = startSketchOn('XZ')\n  |> startProfileAt([0, 0], %)\n  |> line([0, 10], %, $edge001)\n\nhelixPath = helix({\n  angleStart = 0,\n  ccw = true,\n  revolutions = 16,\n  length = 10,\n  radius = 5,\n  axis = edge001\n})\n\n// Create a spring by sweeping around the helix path.\nspringSketch = startSketchOn('XY')\n  |> circle({ center = [0, 0], radius = 1 }, %)\n// |> sweep({ path = helixPath }, %)", | ||||
|       "// Create a helix around a custom axis.\nhelixPath = helix({\n  angleStart = 0,\n  ccw = true,\n  revolutions = 16,\n  length = 10,\n  radius = 5,\n  axis = {\n    custom = {\n      axis = [0, 0, 1.0],\n      origin = [0, 0.25, 0]\n    }\n  }\n})\n\n// Create a spring by sweeping around the helix path.\nspringSketch = startSketchOn('XY')\n  |> circle({ center = [0, 0], radius = 1 }, %)\n// |> sweep({ path = helixPath }, %)" | ||||
|       "// Create a helix around the Z axis.\nhelixPath = helix({\n  angleStart = 0,\n  ccw = true,\n  revolutions = 5,\n  length = 10,\n  radius = 5,\n  axis = 'Z'\n})\n\n// Create a spring by sweeping around the helix path.\nspringSketch = startSketchOn('YZ')\n  |> circle({ center = [0, 0], radius = 0.5 }, %)\n  |> sweep({ path = helixPath }, %)", | ||||
|       "// Create a helix around an edge.\nhelper001 = startSketchOn('XZ')\n  |> startProfileAt([0, 0], %)\n  |> line([0, 10], %, $edge001)\n\nhelixPath = helix({\n  angleStart = 0,\n  ccw = true,\n  revolutions = 5,\n  length = 10,\n  radius = 5,\n  axis = edge001\n})\n\n// Create a spring by sweeping around the helix path.\nspringSketch = startSketchOn('XY')\n  |> circle({ center = [0, 0], radius = 0.5 }, %)\n  |> sweep({ path = helixPath }, %)", | ||||
|       "// Create a helix around a custom axis.\nhelixPath = helix({\n  angleStart = 0,\n  ccw = true,\n  revolutions = 5,\n  length = 10,\n  radius = 5,\n  axis = {\n    custom = {\n      axis = [0, 0, 1.0],\n      origin = [0, 0.25, 0]\n    }\n  }\n})\n\n// Create a spring by sweeping around the helix path.\nspringSketch = startSketchOn('XY')\n  |> circle({ center = [0, 0], radius = 1 }, %)\n  |> sweep({ path = helixPath }, %)" | ||||
|     ] | ||||
|   }, | ||||
|   { | ||||
| @ -193684,7 +193684,7 @@ | ||||
|     "deprecated": false, | ||||
|     "examples": [ | ||||
|       "// Create a pipe using a sweep.\n\n\n// Create a path for the sweep.\nsweepPath = startSketchOn('XZ')\n  |> startProfileAt([0.05, 0.05], %)\n  |> line([0, 7], %)\n  |> tangentialArc({ offset = 90, radius = 5 }, %)\n  |> line([-3, 0], %)\n  |> tangentialArc({ offset = -90, radius = 5 }, %)\n  |> line([0, 7], %)\n\n// Create a hole for the pipe.\npipeHole = startSketchOn('XY')\n  |> circle({ center = [0, 0], radius = 1.5 }, %)\n\nsweepSketch = startSketchOn('XY')\n  |> circle({ center = [0, 0], radius = 2 }, %)\n  |> hole(pipeHole, %)\n  |> sweep({ path = sweepPath }, %)", | ||||
|       "// Create a spring by sweeping around a helix path.\n\n\n// Create a helix around the Z axis.\nhelixPath = helix({\n  angleStart = 0,\n  ccw = true,\n  revolutions = 16,\n  length = 10,\n  radius = 5,\n  axis = 'Z'\n})\n\n// Create a spring by sweeping around the helix path.\nspringSketch = startSketchOn('YZ')\n  |> circle({ center = [0, 0], radius = 1 }, %)\n// |> sweep({ path = helixPath }, %)" | ||||
|       "// Create a spring by sweeping around a helix path.\n\n\n// Create a helix around the Z axis.\nhelixPath = helix({\n  angleStart = 0,\n  ccw = true,\n  revolutions = 4,\n  length = 10,\n  radius = 5,\n  axis = 'Z'\n})\n\n// Create a spring by sweeping around the helix path.\nspringSketch = startSketchOn('YZ')\n  |> circle({ center = [0, 0], radius = 1 }, %)\n  |> sweep({ path = helixPath }, %)" | ||||
|     ] | ||||
|   }, | ||||
|   { | ||||
|  | ||||
| @ -45,46 +45,6 @@ test.describe('Command bar tests', () => { | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   // TODO: fix this test after the electron migration | ||||
|   test.fixme('Fillet from command bar', async ({ page, homePage }) => { | ||||
|     await page.addInitScript(async () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `sketch001 = startSketchOn('XY') | ||||
|     |> startProfileAt([-5, -5], %) | ||||
|     |> line([0, 10], %) | ||||
|     |> line([10, 0], %) | ||||
|     |> line([0, -10], %) | ||||
|     |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|     |> close(%) | ||||
|   extrude001 = extrude(-10, sketch001)` | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     const u = await getUtils(page) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|     await homePage.goToModelingScene() | ||||
|     await u.openDebugPanel() | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|     await u.closeDebugPanel() | ||||
|  | ||||
|     const selectSegment = () => page.getByText(`line([0, -10], %)`).click() | ||||
|  | ||||
|     await selectSegment() | ||||
|     await page.waitForTimeout(100) | ||||
|     await page.getByRole('button', { name: 'Fillet' }).click() | ||||
|     await page.waitForTimeout(100) | ||||
|     await page.keyboard.press('Enter') // skip selection | ||||
|     await page.waitForTimeout(100) | ||||
|     await page.keyboard.press('Enter') // accept default radius | ||||
|     await page.waitForTimeout(100) | ||||
|     await page.keyboard.press('Enter') // submit | ||||
|     await page.waitForTimeout(100) | ||||
|     await expect(page.locator('.cm-activeLine')).toContainText( | ||||
|       `fillet({ radius = ${KCL_DEFAULT_LENGTH}, tags = [seg01] }, %)` | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   test('Command bar can change a setting, and switch back and forth between arguments', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|  | ||||
| @ -15,6 +15,7 @@ export class ToolbarFixture { | ||||
|   extrudeButton!: Locator | ||||
|   loftButton!: Locator | ||||
|   sweepButton!: Locator | ||||
|   filletButton!: Locator | ||||
|   chamferButton!: Locator | ||||
|   shellButton!: Locator | ||||
|   offsetPlaneButton!: Locator | ||||
| @ -43,6 +44,7 @@ export class ToolbarFixture { | ||||
|     this.extrudeButton = page.getByTestId('extrude') | ||||
|     this.loftButton = page.getByTestId('loft') | ||||
|     this.sweepButton = page.getByTestId('sweep') | ||||
|     this.filletButton = page.getByTestId('fillet3d') | ||||
|     this.chamferButton = page.getByTestId('chamfer3d') | ||||
|     this.shellButton = page.getByTestId('shell') | ||||
|     this.offsetPlaneButton = page.getByTestId('plane-offset') | ||||
|  | ||||
| @ -829,12 +829,6 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => { | ||||
|         }) | ||||
|         await selectSketches() | ||||
|         await cmdBar.progressCmdBar() | ||||
|         await cmdBar.expectState({ | ||||
|           stage: 'review', | ||||
|           headerArguments: { Selection: '2 faces' }, | ||||
|           commandName: 'Loft', | ||||
|         }) | ||||
|         await cmdBar.progressCmdBar() | ||||
|       }) | ||||
|     } else { | ||||
|       await test.step(`Preselect the two sketches`, async () => { | ||||
| @ -844,12 +838,6 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => { | ||||
|       await test.step(`Go through the command bar flow with preselected sketches`, async () => { | ||||
|         await toolbar.loftButton.click() | ||||
|         await cmdBar.progressCmdBar() | ||||
|         await cmdBar.expectState({ | ||||
|           stage: 'review', | ||||
|           headerArguments: { Selection: '2 faces' }, | ||||
|           commandName: 'Loft', | ||||
|         }) | ||||
|         await cmdBar.progressCmdBar() | ||||
|       }) | ||||
|     } | ||||
|  | ||||
| @ -1032,6 +1020,221 @@ sketch002 = startSketchOn('XZ') | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| test(`Fillet point-and-click`, async ({ | ||||
|   context, | ||||
|   page, | ||||
|   homePage, | ||||
|   scene, | ||||
|   editor, | ||||
|   toolbar, | ||||
|   cmdBar, | ||||
| }) => { | ||||
|   // Code samples | ||||
|   const initialCode = `sketch001 = startSketchOn('XY') | ||||
|   |> startProfileAt([-12, -6], %) | ||||
|   |> line([0, 12], %) | ||||
|   |> line([24, 0], %) | ||||
|   |> line([0, -12], %) | ||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|   |> close(%) | ||||
| extrude001 = extrude(-12, sketch001) | ||||
| ` | ||||
|   const firstFilletDeclaration = 'fillet({ radius = 5, tags = [seg01] }, %)' | ||||
|   const secondFilletDeclaration = | ||||
|     'fillet({       radius = 5,       tags = [getOppositeEdge(seg01)]     }, %)' | ||||
|  | ||||
|   // Locators | ||||
|   const firstEdgeLocation = { x: 600, y: 193 } | ||||
|   const secondEdgeLocation = { x: 600, y: 383 } | ||||
|   const bodyLocation = { x: 630, y: 290 } | ||||
|   const [clickOnFirstEdge] = scene.makeMouseHelpers( | ||||
|     firstEdgeLocation.x, | ||||
|     firstEdgeLocation.y | ||||
|   ) | ||||
|   const [clickOnSecondEdge] = scene.makeMouseHelpers( | ||||
|     secondEdgeLocation.x, | ||||
|     secondEdgeLocation.y | ||||
|   ) | ||||
|  | ||||
|   // Colors | ||||
|   const edgeColorWhite: [number, number, number] = [248, 248, 248] | ||||
|   const edgeColorYellow: [number, number, number] = [251, 251, 40] // Mac:B=67 Ubuntu:B=12 | ||||
|   const bodyColor: [number, number, number] = [155, 155, 155] | ||||
|   const filletColor: [number, number, number] = [127, 127, 127] | ||||
|   const backgroundColor: [number, number, number] = [30, 30, 30] | ||||
|   const lowTolerance = 20 | ||||
|   const highTolerance = 40 | ||||
|  | ||||
|   // Setup | ||||
|   await test.step(`Initial test setup`, async () => { | ||||
|     await context.addInitScript((initialCode) => { | ||||
|       localStorage.setItem('persistCode', initialCode) | ||||
|     }, initialCode) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     // verify modeling scene is loaded | ||||
|     await scene.expectPixelColor( | ||||
|       backgroundColor, | ||||
|       secondEdgeLocation, | ||||
|       lowTolerance | ||||
|     ) | ||||
|  | ||||
|     // wait for stream to load | ||||
|     await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance) | ||||
|   }) | ||||
|  | ||||
|   // Test 1: Command bar flow with preselected edges | ||||
|   await test.step(`Select first edge`, async () => { | ||||
|     await scene.expectPixelColor( | ||||
|       edgeColorWhite, | ||||
|       firstEdgeLocation, | ||||
|       lowTolerance | ||||
|     ) | ||||
|     await clickOnFirstEdge() | ||||
|     await scene.expectPixelColor( | ||||
|       edgeColorYellow, | ||||
|       firstEdgeLocation, | ||||
|       highTolerance // Ubuntu color mismatch can require high tolerance | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   await test.step(`Apply fillet to the preselected edge`, async () => { | ||||
|     await page.waitForTimeout(100) | ||||
|     await toolbar.filletButton.click() | ||||
|     await cmdBar.expectState({ | ||||
|       commandName: 'Fillet', | ||||
|       highlightedHeaderArg: 'selection', | ||||
|       currentArgKey: 'selection', | ||||
|       currentArgValue: '', | ||||
|       headerArguments: { | ||||
|         Selection: '', | ||||
|         Radius: '', | ||||
|       }, | ||||
|       stage: 'arguments', | ||||
|     }) | ||||
|     await cmdBar.progressCmdBar() | ||||
|     await cmdBar.expectState({ | ||||
|       commandName: 'Fillet', | ||||
|       highlightedHeaderArg: 'radius', | ||||
|       currentArgKey: 'radius', | ||||
|       currentArgValue: '5', | ||||
|       headerArguments: { | ||||
|         Selection: '1 face', | ||||
|         Radius: '', | ||||
|       }, | ||||
|       stage: 'arguments', | ||||
|     }) | ||||
|     await cmdBar.progressCmdBar() | ||||
|     await cmdBar.expectState({ | ||||
|       commandName: 'Fillet', | ||||
|       headerArguments: { | ||||
|         Selection: '1 face', | ||||
|         Radius: '5', | ||||
|       }, | ||||
|       stage: 'review', | ||||
|     }) | ||||
|     await cmdBar.progressCmdBar() | ||||
|   }) | ||||
|  | ||||
|   await test.step(`Confirm code is added to the editor`, async () => { | ||||
|     await editor.expectEditor.toContain(firstFilletDeclaration) | ||||
|     await editor.expectState({ | ||||
|       diagnostics: [], | ||||
|       activeLines: ['|>fillet({radius=5,tags=[seg01]},%)'], | ||||
|       highlightedCode: '', | ||||
|     }) | ||||
|   }) | ||||
|  | ||||
|   await test.step(`Confirm scene has changed`, async () => { | ||||
|     await scene.expectPixelColor(filletColor, firstEdgeLocation, lowTolerance) | ||||
|   }) | ||||
|  | ||||
|   // Test 2: Command bar flow without preselected edges | ||||
|   await test.step(`Open fillet UI without selecting edges`, async () => { | ||||
|     await page.waitForTimeout(100) | ||||
|     await toolbar.filletButton.click() | ||||
|     await cmdBar.expectState({ | ||||
|       stage: 'arguments', | ||||
|       currentArgKey: 'selection', | ||||
|       currentArgValue: '', | ||||
|       headerArguments: { | ||||
|         Selection: '', | ||||
|         Radius: '', | ||||
|       }, | ||||
|       highlightedHeaderArg: 'selection', | ||||
|       commandName: 'Fillet', | ||||
|     }) | ||||
|   }) | ||||
|  | ||||
|   await test.step(`Select second edge`, async () => { | ||||
|     await scene.expectPixelColor( | ||||
|       edgeColorWhite, | ||||
|       secondEdgeLocation, | ||||
|       lowTolerance | ||||
|     ) | ||||
|     await clickOnSecondEdge() | ||||
|     await scene.expectPixelColor( | ||||
|       edgeColorYellow, | ||||
|       secondEdgeLocation, | ||||
|       highTolerance // Ubuntu color mismatch can require high tolerance | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   await test.step(`Apply fillet to the second edge`, async () => { | ||||
|     await cmdBar.expectState({ | ||||
|       commandName: 'Fillet', | ||||
|       highlightedHeaderArg: 'selection', | ||||
|       currentArgKey: 'selection', | ||||
|       currentArgValue: '', | ||||
|       headerArguments: { | ||||
|         Selection: '', | ||||
|         Radius: '', | ||||
|       }, | ||||
|       stage: 'arguments', | ||||
|     }) | ||||
|     await cmdBar.progressCmdBar() | ||||
|     await cmdBar.expectState({ | ||||
|       commandName: 'Fillet', | ||||
|       highlightedHeaderArg: 'radius', | ||||
|       currentArgKey: 'radius', | ||||
|       currentArgValue: '5', | ||||
|       headerArguments: { | ||||
|         Selection: '1 sweepEdge', | ||||
|         Radius: '', | ||||
|       }, | ||||
|       stage: 'arguments', | ||||
|     }) | ||||
|     await cmdBar.progressCmdBar() | ||||
|     await cmdBar.expectState({ | ||||
|       commandName: 'Fillet', | ||||
|       headerArguments: { | ||||
|         Selection: '1 sweepEdge', | ||||
|         Radius: '5', | ||||
|       }, | ||||
|       stage: 'review', | ||||
|     }) | ||||
|     await cmdBar.progressCmdBar() | ||||
|   }) | ||||
|  | ||||
|   await test.step(`Confirm code is added to the editor`, async () => { | ||||
|     await editor.expectEditor.toContain(secondFilletDeclaration) | ||||
|     await editor.expectState({ | ||||
|       diagnostics: [], | ||||
|       activeLines: ['radius=5,'], | ||||
|       highlightedCode: '', | ||||
|     }) | ||||
|   }) | ||||
|  | ||||
|   await test.step(`Confirm scene has changed`, async () => { | ||||
|     await scene.expectPixelColor( | ||||
|       backgroundColor, | ||||
|       secondEdgeLocation, | ||||
|       lowTolerance | ||||
|     ) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| test(`Chamfer point-and-click`, async ({ | ||||
|   context, | ||||
|   page, | ||||
| @ -1041,9 +1244,6 @@ test(`Chamfer point-and-click`, async ({ | ||||
|   toolbar, | ||||
|   cmdBar, | ||||
| }) => { | ||||
|   // TODO: fix this test on windows after the electron migration | ||||
|   test.skip(process.platform === 'win32', 'Skip on windows') | ||||
|  | ||||
|   // Code samples | ||||
|   const initialCode = `sketch001 = startSketchOn('XY') | ||||
|   |> startProfileAt([-12, -6], %) | ||||
| @ -1081,13 +1281,13 @@ extrude001 = extrude(-12, sketch001) | ||||
|   const highTolerance = 40 | ||||
|  | ||||
|   // Setup | ||||
|   await context.addInitScript((initialCode) => { | ||||
|     localStorage.setItem('persistCode', initialCode) | ||||
|   }, initialCode) | ||||
|   await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|   await homePage.goToModelingScene() | ||||
|   await test.step(`Initial test setup`, async () => { | ||||
|     await context.addInitScript((initialCode) => { | ||||
|       localStorage.setItem('persistCode', initialCode) | ||||
|     }, initialCode) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|   await test.step(`Verify scene is loaded`, async () => { | ||||
|     // verify modeling scene is loaded | ||||
|     await scene.expectPixelColor( | ||||
|       backgroundColor, | ||||
| @ -1115,6 +1315,7 @@ extrude001 = extrude(-12, sketch001) | ||||
|   }) | ||||
|  | ||||
|   await test.step(`Apply chamfer to the preselected edge`, async () => { | ||||
|     await page.waitForTimeout(100) | ||||
|     await toolbar.chamferButton.click() | ||||
|     await cmdBar.expectState({ | ||||
|       commandName: 'Chamfer', | ||||
| @ -1166,6 +1367,7 @@ extrude001 = extrude(-12, sketch001) | ||||
|  | ||||
|   // Test 2: Command bar flow without preselected edges | ||||
|   await test.step(`Open chamfer UI without selecting edges`, async () => { | ||||
|     await page.waitForTimeout(100) | ||||
|     await toolbar.chamferButton.click() | ||||
|     await cmdBar.expectState({ | ||||
|       stage: 'arguments', | ||||
|  | ||||
| @ -108,6 +108,8 @@ export class CameraControls { | ||||
|   interactionGuards: MouseGuard = cameraMouseDragGuards.Zoo | ||||
|   isFovAnimationInProgress = false | ||||
|   perspectiveFovBeforeOrtho = 45 | ||||
|   // NOTE: Duplicated state across Provider and singleton. Mapped from settingsMachine | ||||
|   _setting_allowOrbitInSketchMode = false | ||||
|   get isPerspective() { | ||||
|     return this.camera instanceof PerspectiveCamera | ||||
|   } | ||||
|  | ||||
| @ -22,6 +22,7 @@ export const CommandBar = () => { | ||||
|  | ||||
|   // Close the command bar when navigating | ||||
|   useEffect(() => { | ||||
|     if (commandBarState.matches('Closed')) return | ||||
|     commandBarSend({ type: 'Close' }) | ||||
|   }, [pathname]) | ||||
|  | ||||
|  | ||||
| @ -4,6 +4,8 @@ import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import { Command } from 'lib/commandTypes' | ||||
| import { useEffect, useState } from 'react' | ||||
| import { CustomIcon } from './CustomIcon' | ||||
| import { getActorNextEvents } from 'lib/utils' | ||||
| import { sortCommands } from 'lib/commandUtils' | ||||
|  | ||||
| function CommandComboBox({ | ||||
|   options, | ||||
| @ -18,8 +20,16 @@ function CommandComboBox({ | ||||
|  | ||||
|   const defaultOption = | ||||
|     options.find((o) => 'isCurrent' in o && o.isCurrent) || null | ||||
|   // sort disabled commands to the bottom | ||||
|   const sortedOptions = options | ||||
|     .map((command) => ({ | ||||
|       command, | ||||
|       disabled: optionIsDisabled(command), | ||||
|     })) | ||||
|     .sort(sortCommands) | ||||
|     .map(({ command }) => command) | ||||
|  | ||||
|   const fuse = new Fuse(options, { | ||||
|   const fuse = new Fuse(sortedOptions, { | ||||
|     keys: ['displayName', 'name', 'description'], | ||||
|     threshold: 0.3, | ||||
|     ignoreLocation: true, | ||||
| @ -27,7 +37,7 @@ function CommandComboBox({ | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const results = fuse.search(query).map((result) => result.item) | ||||
|     setFilteredOptions(query.length > 0 ? results : options) | ||||
|     setFilteredOptions(query.length > 0 ? results : sortedOptions) | ||||
|   }, [query]) | ||||
|  | ||||
|   function handleSelection(command: Command) { | ||||
| @ -73,7 +83,8 @@ function CommandComboBox({ | ||||
|           <Combobox.Option | ||||
|             key={option.groupId + option.name + (option.displayName || '')} | ||||
|             value={option} | ||||
|             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" | ||||
|             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 ui-disabled:!text-chalkboard-50" | ||||
|             disabled={optionIsDisabled(option)} | ||||
|           > | ||||
|             {'icon' in option && option.icon && ( | ||||
|               <CustomIcon name={option.icon} className="w-5 h-5" /> | ||||
| @ -96,3 +107,11 @@ function CommandComboBox({ | ||||
| } | ||||
|  | ||||
| export default CommandComboBox | ||||
|  | ||||
| function optionIsDisabled(option: Command): boolean { | ||||
|   return ( | ||||
|     'machineActor' in option && | ||||
|     option.machineActor !== undefined && | ||||
|     !getActorNextEvents(option.machineActor.getSnapshot()).includes(option.name) | ||||
|   ) | ||||
| } | ||||
|  | ||||
| @ -111,7 +111,7 @@ export const ModelingMachineProvider = ({ | ||||
|     auth, | ||||
|     settings: { | ||||
|       context: { | ||||
|         app: { theme, enableSSAO }, | ||||
|         app: { theme, enableSSAO, allowOrbitInSketchMode }, | ||||
|         modeling: { | ||||
|           defaultUnit, | ||||
|           cameraProjection, | ||||
| @ -121,6 +121,7 @@ export const ModelingMachineProvider = ({ | ||||
|       }, | ||||
|     }, | ||||
|   } = useSettingsAuthContext() | ||||
|   const previousAllowOrbitInSketchMode = useRef(allowOrbitInSketchMode.current) | ||||
|   const navigate = useNavigate() | ||||
|   const { context, send: fileMachineSend } = useFileContext() | ||||
|   const { file } = useLoaderData() as IndexLoaderData | ||||
| @ -634,7 +635,8 @@ export const ModelingMachineProvider = ({ | ||||
|             input.plane | ||||
|           ) | ||||
|           await kclManager.updateAst(modifiedAst, false) | ||||
|           sceneInfra.camControls.enableRotate = false | ||||
|           sceneInfra.camControls.enableRotate = | ||||
|             sceneInfra.camControls._setting_allowOrbitInSketchMode | ||||
|           sceneInfra.camControls.syncDirection = 'clientToEngine' | ||||
|  | ||||
|           await letEngineAnimateAndSyncCamAfter( | ||||
| @ -647,6 +649,7 @@ export const ModelingMachineProvider = ({ | ||||
|             zAxis: input.zAxis, | ||||
|             yAxis: input.yAxis, | ||||
|             origin: [0, 0, 0], | ||||
|             animateTargetId: input.planeId, | ||||
|           } | ||||
|         }), | ||||
|         'animate-to-sketch': fromPromise( | ||||
| @ -671,6 +674,7 @@ export const ModelingMachineProvider = ({ | ||||
|               origin: info.sketchDetails.origin.map( | ||||
|                 (a) => a / sceneInfra._baseUnitMultiplier | ||||
|               ) as [number, number, number], | ||||
|               animateTargetId: info?.sketchDetails?.faceId || '', | ||||
|             } | ||||
|           } | ||||
|         ), | ||||
| @ -1188,6 +1192,41 @@ export const ModelingMachineProvider = ({ | ||||
|     } | ||||
|   }, [engineCommandManager.engineConnection, modelingSend]) | ||||
|  | ||||
|   useEffect(() => { | ||||
|     // Only trigger this if the state actually changes, if it stays the same do not reload the camera | ||||
|     if ( | ||||
|       previousAllowOrbitInSketchMode.current === allowOrbitInSketchMode.current | ||||
|     ) { | ||||
|       //no op | ||||
|       previousAllowOrbitInSketchMode.current = allowOrbitInSketchMode.current | ||||
|       return | ||||
|     } | ||||
|     const inSketchMode = modelingState.matches('Sketch') | ||||
|  | ||||
|     // If you are in sketch mode and you disable the orbit, return back to the normal view to the target | ||||
|     if (!allowOrbitInSketchMode.current) { | ||||
|       const targetId = modelingState.context.sketchDetails?.animateTargetId | ||||
|       if (inSketchMode && targetId) { | ||||
|         letEngineAnimateAndSyncCamAfter(engineCommandManager, targetId) | ||||
|           .then(() => {}) | ||||
|           .catch((e) => { | ||||
|             console.error( | ||||
|               'failed to sync engine and client scene after disabling allow orbit in sketch mode' | ||||
|             ) | ||||
|             console.error(e) | ||||
|           }) | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // While you are in sketch mode you should be able to control the enable rotate | ||||
|     // Once you exit it goes back to normal | ||||
|     if (inSketchMode) { | ||||
|       sceneInfra.camControls.enableRotate = allowOrbitInSketchMode.current | ||||
|     } | ||||
|  | ||||
|     previousAllowOrbitInSketchMode.current = allowOrbitInSketchMode.current | ||||
|   }, [allowOrbitInSketchMode]) | ||||
|  | ||||
|   // Allow using the delete key to delete solids | ||||
|   useHotkeys(['backspace', 'delete', 'del'], () => { | ||||
|     modelingSend({ type: 'Delete selection' }) | ||||
|  | ||||
| @ -137,6 +137,11 @@ export const SettingsAuthProviderBase = ({ | ||||
|           sceneInfra.theme = opposingTheme | ||||
|           sceneEntitiesManager.updateSegmentBaseColor(opposingTheme) | ||||
|         }, | ||||
|         setAllowOrbitInSketchMode: ({ context }) => { | ||||
|           sceneInfra.camControls._setting_allowOrbitInSketchMode = | ||||
|             context.app.allowOrbitInSketchMode.current | ||||
|           // ModelingMachineProvider will do a use effect to trigger the camera engine sync | ||||
|         }, | ||||
|         toastSuccess: ({ event }) => { | ||||
|           if (!('data' in event)) return | ||||
|           const eventParts = event.type.replace(/^set./, '').split('.') as [ | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import { useEffect } from 'react' | ||||
| import { AnyStateMachine, Actor, StateFrom } from 'xstate' | ||||
| import { AnyStateMachine, Actor, StateFrom, EventFrom } from 'xstate' | ||||
| import { createMachineCommand } from '../lib/createMachineCommand' | ||||
| import { useCommandsContext } from './useCommandsContext' | ||||
| import { modelingMachine } from 'machines/modelingMachine' | ||||
| @ -15,7 +15,6 @@ import { useKclContext } from 'lang/KclProvider' | ||||
| import { useNetworkContext } from 'hooks/useNetworkContext' | ||||
| import { NetworkHealthState } from 'hooks/useNetworkStatus' | ||||
| import { useAppState } from 'AppState' | ||||
| import { getActorNextEvents } from 'lib/utils' | ||||
|  | ||||
| // This might not be necessary, AnyStateMachine from xstate is working | ||||
| export type AllMachines = | ||||
| @ -60,21 +59,21 @@ export default function useStateMachineCommands< | ||||
|         overallState !== NetworkHealthState.Weak) || | ||||
|       isExecuting || | ||||
|       !isStreamReady | ||||
|     const newCommands = getActorNextEvents(state) | ||||
|     const newCommands = Object.keys(commandBarConfig || {}) | ||||
|       .filter((_) => !allCommandsRequireNetwork || !disableAllButtons) | ||||
|       .filter((e) => !['done.', 'error.'].some((n) => e.includes(n))) | ||||
|       .flatMap((type) => | ||||
|         createMachineCommand<T, S>({ | ||||
|       .flatMap((type) => { | ||||
|         const typeWithProperType = type as EventFrom<T>['type'] | ||||
|         return createMachineCommand<T, S>({ | ||||
|           // The group is the owner machine's ID. | ||||
|           groupId: machineId, | ||||
|           type, | ||||
|           type: typeWithProperType, | ||||
|           state, | ||||
|           send, | ||||
|           actor, | ||||
|           commandBarConfig, | ||||
|           onCancel, | ||||
|         }) | ||||
|       ) | ||||
|       }) | ||||
|       .filter((c) => c !== null) as Command[] // TS isn't smart enough to know this filter removes nulls | ||||
|  | ||||
|     commandBarSend({ type: 'Add commands', data: { commands: newCommands } }) | ||||
| @ -85,5 +84,5 @@ export default function useStateMachineCommands< | ||||
|         data: { commands: newCommands }, | ||||
|       }) | ||||
|     } | ||||
|   }, [state, overallState, isExecuting, isStreamReady]) | ||||
|   }, [overallState, isExecuting, isStreamReady]) | ||||
| } | ||||
|  | ||||
| @ -329,7 +329,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< | ||||
|   Loft: { | ||||
|     description: 'Create a 3D body by blending between two or more sketches', | ||||
|     icon: 'loft', | ||||
|     needsReview: true, | ||||
|     needsReview: false, | ||||
|     args: { | ||||
|       selection: { | ||||
|         inputType: 'selection', | ||||
|  | ||||
| @ -63,12 +63,11 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig< | ||||
|       name: { | ||||
|         inputType: 'options', | ||||
|         required: true, | ||||
|         options: [], | ||||
|         optionsFromContext: (context) => | ||||
|           context.projects.map((p) => ({ | ||||
|         options: (_, context) => | ||||
|           context?.projects.map((p) => ({ | ||||
|             name: p.name!, | ||||
|             value: p.name!, | ||||
|           })), | ||||
|           })) || [], | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
| @ -80,12 +79,11 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig< | ||||
|       oldName: { | ||||
|         inputType: 'options', | ||||
|         required: true, | ||||
|         options: [], | ||||
|         optionsFromContext: (context) => | ||||
|           context.projects.map((p) => ({ | ||||
|         options: (_, context) => | ||||
|           context?.projects.map((p) => ({ | ||||
|             name: p.name!, | ||||
|             value: p.name!, | ||||
|           })), | ||||
|           })) || [], | ||||
|       }, | ||||
|       newName: { | ||||
|         inputType: 'string', | ||||
|  | ||||
| @ -76,6 +76,7 @@ export type Command< | ||||
|     | (( | ||||
|         commandBarContext: { argumentsToSubmit: Record<string, unknown> } // Should be the commandbarMachine's context, but it creates a circular dependency | ||||
|       ) => string | ReactNode) | ||||
|   machineActor?: Actor<T> | ||||
|   onSubmit: (data?: CommandSchema) => void | ||||
|   onCancel?: () => void | ||||
|   args?: { | ||||
| @ -95,7 +96,7 @@ export type CommandConfig< | ||||
|   Command<T, CommandName, CommandSchema>, | ||||
|   'name' | 'groupId' | 'onSubmit' | 'onCancel' | 'args' | 'needsReview' | ||||
| > & { | ||||
|   needsReview?: true | ||||
|   needsReview?: boolean | ||||
|   status?: 'active' | 'development' | 'inactive' | ||||
|   args?: { | ||||
|     [ArgName in keyof CommandSchema]: CommandArgumentConfig< | ||||
|  | ||||
							
								
								
									
										49
									
								
								src/lib/commandUtils.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,49 @@ | ||||
| import { CommandWithDisabledState, sortCommands } from './commandUtils' | ||||
|  | ||||
| function commandWithDisabled( | ||||
|   name: string, | ||||
|   disabled: boolean, | ||||
|   groupId = 'modeling' | ||||
| ): CommandWithDisabledState { | ||||
|   return { | ||||
|     command: { | ||||
|       name, | ||||
|       groupId, | ||||
|       needsReview: false, | ||||
|       onSubmit: () => {}, | ||||
|     }, | ||||
|     disabled, | ||||
|   } | ||||
| } | ||||
|  | ||||
| describe('Command sorting', () => { | ||||
|   it(`Puts modeling commands first`, () => { | ||||
|     const initial = [ | ||||
|       commandWithDisabled('a', false, 'settings'), | ||||
|       commandWithDisabled('b', false, 'modeling'), | ||||
|       commandWithDisabled('c', false, 'settings'), | ||||
|     ] | ||||
|     const sorted = initial.sort(sortCommands) | ||||
|     expect(sorted[0].command.groupId).toBe('modeling') | ||||
|   }) | ||||
|  | ||||
|   it(`Puts disabled commands last`, () => { | ||||
|     const initial = [ | ||||
|       commandWithDisabled('a', true, 'modeling'), | ||||
|       commandWithDisabled('z', false, 'modeling'), | ||||
|       commandWithDisabled('a', false, 'settings'), | ||||
|     ] | ||||
|     const sorted = initial.sort(sortCommands) | ||||
|     expect(sorted[sorted.length - 1].disabled).toBe(true) | ||||
|   }) | ||||
|  | ||||
|   it(`Puts settings commands second to last`, () => { | ||||
|     const initial = [ | ||||
|       commandWithDisabled('a', true, 'modeling'), | ||||
|       commandWithDisabled('z', false, 'modeling'), | ||||
|       commandWithDisabled('a', false, 'settings'), | ||||
|     ] | ||||
|     const sorted = initial.sort(sortCommands) | ||||
|     expect(sorted[1].command.groupId).toBe('settings') | ||||
|   }) | ||||
| }) | ||||
| @ -2,6 +2,9 @@ | ||||
| // That object also contains some metadata about what to do with the KCL expression, | ||||
| // such as whether we need to create a new variable for it. | ||||
| // This function extracts the value field from those arg payloads and returns | ||||
|  | ||||
| import { Command } from './commandTypes' | ||||
|  | ||||
| // The arg object with all its field as natural values that the command to be executed will expect. | ||||
| export function getCommandArgumentKclValuesOnly(args: Record<string, unknown>) { | ||||
|   return Object.fromEntries( | ||||
| @ -13,3 +16,42 @@ export function getCommandArgumentKclValuesOnly(args: Record<string, unknown>) { | ||||
|     }) | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export interface CommandWithDisabledState { | ||||
|   command: Command | ||||
|   disabled: boolean | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Sorting logic for commands in the command combo box. | ||||
|  */ | ||||
| export function sortCommands( | ||||
|   a: CommandWithDisabledState, | ||||
|   b: CommandWithDisabledState | ||||
| ) { | ||||
|   // Disabled commands should be at the bottom | ||||
|   if (a.disabled && !b.disabled) { | ||||
|     return 1 | ||||
|   } | ||||
|   if (b.disabled && !a.disabled) { | ||||
|     return -1 | ||||
|   } | ||||
|   // Settings commands should be next-to-last | ||||
|   if (a.command.groupId === 'settings' && b.command.groupId !== 'settings') { | ||||
|     return 1 | ||||
|   } | ||||
|   if (b.command.groupId === 'settings' && a.command.groupId !== 'settings') { | ||||
|     return -1 | ||||
|   } | ||||
|   // Modeling commands should be first | ||||
|   if (a.command.groupId === 'modeling' && b.command.groupId !== 'modeling') { | ||||
|     return -1 | ||||
|   } | ||||
|   if (b.command.groupId === 'modeling' && a.command.groupId !== 'modeling') { | ||||
|     return 1 | ||||
|   } | ||||
|   // Sort alphabetically | ||||
|   return (a.command.displayName || a.command.name).localeCompare( | ||||
|     b.command.displayName || b.command.name | ||||
|   ) | ||||
| } | ||||
|  | ||||
| @ -96,6 +96,7 @@ export function createMachineCommand< | ||||
|     icon, | ||||
|     description: commandConfig.description, | ||||
|     needsReview: commandConfig.needsReview || false, | ||||
|     machineActor: actor, | ||||
|     onSubmit: (data?: S[typeof type]) => { | ||||
|       if (data !== undefined && data !== null) { | ||||
|         send({ type, data }) | ||||
|  | ||||
| @ -190,6 +190,14 @@ export function createSettings() { | ||||
|           inputType: 'boolean', | ||||
|         }, | ||||
|       }), | ||||
|       allowOrbitInSketchMode: new Setting<boolean>({ | ||||
|         defaultValue: false, | ||||
|         description: 'Toggle free camera while in sketch mode', | ||||
|         validate: (v) => typeof v === 'boolean', | ||||
|         commandConfig: { | ||||
|           inputType: 'boolean', | ||||
|         }, | ||||
|       }), | ||||
|       onboardingStatus: new Setting<OnboardingStatus>({ | ||||
|         defaultValue: '', | ||||
|         // TODO: this could be better but we don't have a TS side real enum | ||||
|  | ||||
| @ -41,6 +41,8 @@ export function configurationToSettingsPayload( | ||||
|       onboardingStatus: configuration?.settings?.app?.onboarding_status, | ||||
|       dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner, | ||||
|       streamIdleMode: configuration?.settings?.app?.stream_idle_mode, | ||||
|       allowOrbitInSketchMode: | ||||
|         configuration?.settings?.app?.allow_orbit_in_sketch_mode, | ||||
|       projectDirectory: configuration?.settings?.project?.directory, | ||||
|       enableSSAO: configuration?.settings?.modeling?.enable_ssao, | ||||
|     }, | ||||
| @ -80,6 +82,8 @@ export function projectConfigurationToSettingsPayload( | ||||
|       onboardingStatus: configuration?.settings?.app?.onboarding_status, | ||||
|       dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner, | ||||
|       streamIdleMode: configuration?.settings?.app?.stream_idle_mode, | ||||
|       allowOrbitInSketchMode: | ||||
|         configuration?.settings?.app?.allow_orbit_in_sketch_mode, | ||||
|       enableSSAO: configuration?.settings?.modeling?.enable_ssao, | ||||
|     }, | ||||
|     modeling: { | ||||
|  | ||||
| @ -119,6 +119,9 @@ export const commandBarMachine = setup({ | ||||
|         selectedCommand?.onSubmit() | ||||
|       } | ||||
|     }, | ||||
|     'Clear selected command': assign({ | ||||
|       selectedCommand: undefined, | ||||
|     }), | ||||
|     'Set current argument to first non-skippable': assign({ | ||||
|       currentArgument: ({ context, event }) => { | ||||
|         const { selectedCommand } = context | ||||
| @ -246,6 +249,7 @@ export const commandBarMachine = setup({ | ||||
|       context.selectedCommand?.needsReview || false, | ||||
|     'Command has no arguments': () => false, | ||||
|     'All arguments are skippable': () => false, | ||||
|     'Has selected command': ({ context }) => !!context.selectedCommand, | ||||
|   }, | ||||
|   actors: { | ||||
|     'Validate argument': fromPromise( | ||||
| @ -394,7 +398,7 @@ export const commandBarMachine = setup({ | ||||
|     ), | ||||
|   }, | ||||
| }).createMachine({ | ||||
|   /** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswZJbPltM0U3eXLZAsRFeQcrerUHRbTFvfkmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUilkskqdiUWjWCAcDC0kjUqnElkc6lkoK0JzOT0CnWo1zIAEEIARvn48LA-uwuIC4qAEqIHJjJPJcYtxFoYdJFFjVsZEG5pqCquJxJoGvJxOUCT82sSrj0AEpgdCoABuYC+yog9OYIyZsQmYmhWzMmTqLnU0kyikRGVRbj5lg2SmUam5StpFxIkgAymBXh8HlADQGyKHw58aecGZEzUDWYgchY7CisWoSlpQQ5EVDLJJxKkdIpyypUop-ed2kHCW0Xu9uD1kwDzcCEJCtlUNgWhZZ1OlnaKEBpZJItHVzDZ5xlpPWiZdDc8w22O7JwozosyLUj3KiZZY85YtMUNgx5Ii81JuS5xBtPVCCyuVR0AOJYbgACzAEhI3wUgoAAV3QQZuFgCg-1wGAvjAkgSCgkCSHAyCcG4TtUxZYRED2TQZGSOw80qaQ7ARCc9mmZYSjPPFpDSQUP0DSQf3-QDgNAiCoJggAROBNw+aMkxNf5cMPHJSjPWRdhvZ9LGcF0b0kVQ8ycDRNkvNRWMbdjfwAoCcCjHjMOgyRyQAdywGITPwB42DA7hYzAgAjdAeDQjCoJw-du3TJE6mnWwoT0NIUTUAs71sMtdGsRYCyUtI9OJDijO49DeKw2BJAANSwShOAgX9IzICB+DASQHh1VAAGsqp1Qrit-MBySy8y-LGPCElSeoy2lZJcwcNRfXHQpBWmWQsRsPYdDPZJUu-QyuPssy+Py5qSt4EyyEAkhUCDNhKF-AAzQ70EkJqiu2tqOt88S926w9UhhLlykWXFHXcTJixsd60hHGxNj2Jag01HVODAKzXI8rzE1+R6U38tN8IQJSuR0OZ0gvHQXHGxBPWmUdppUWwFV0MHJAhqGYcpAh1qwrqDx7ZFajUmVpulEbqlqREsfBTQ0jcPM5P5KmaehshNW1PVvOy7Cka7VHeoYDkyjSPQtAvPMqkRQU1BnX1+UydFkQvCWwEhqWAFEIC8xnFd3ZHntZuTDZG4ob1nGxPTUfXrBnS8nDmEpT2ObxTnXVUALeOrgPanycvKyrqpwWqGqurbWsThXjWd5WesQZYtmBkplCceplIncK0SrXYqnccxxcj5s2OQWP4-s3PzJgiqcCqmr6sa7P2x7vi6AcAvJJ7XEy0dUFMWsFxlnKfn+S5CL3E9Jiuapjv3i7qNx+T-bDskY6zourObpz+6cuZgK0Y5Ua0TqEvXDkJR-YnR0s1xjkIMnDln3uuVsHwOxT1NCjIuR5nCSBUExJi1huRqyooUTYpYnzeyUFkKsy4Tg4FQBAOAgg26Nmga7QKohshSBtNYXGDonQumtPYaQfsSjLBHDefepJICUJZtQ4oMx8wXj6jebGt4JwbC2OiVwig9hh2hLpVu0cOhxjbMBBGeABFPwSA3ERRMlhKWCn9WRVQzZQirMo0BAYNzxn4RJGBh5MRbGXsHawGQFBmLrpUasOQorOAjs0OxaUVrGVMvfaCuiVYEV2KWTYg1yxyA0i6ZYaIPSL3lOkS8wSo6hOWpxCJ8te6WRsnZKMjlnIxNgSNIiiTF6vVSdI9JM1taXlxLzTwqiClBnSqtSJScLIFVvjtKANSXquENqoJwtgyZzRFIUQ4MhqgNACdNTQCpLbWyshMnsjpSwjXRJYHecgcmImsLIz0odNAaGqPiHpDYY6HwTlE+ATiqHPwohYewWQshmGhHrCcJz5Bck5toZwGhMheC8EAA */ | ||||
|   /** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAIwB2AHTiAHAE45AZjkAmdcoaSArCoA0IAJ6JxDOdJ2SF4gCySHaqZIa2Avm8NpMuAsXJUYKSMLEggHLA8fAJhIgiiOgBssokKTpJqiXK2OsqJaoYm8eJqytKJ9hqq+eJW2h5eGNh4RKRkAGKcLb74tJRgAMbc+ANNviGCEVH8gnEKubK5ympVrrlyhabm0rZ5CtYM4hVq83ININ7NfqTSVDSQZADybGA4E2FTvDOxiGoMDNJMjo9H9lLYGDpKpsEModOJpA5XOIwakwcidOdLj1-LdqLQIGQAIIQAijHx4WDvdhcL4xUBxURqSTw3LzfYKZSSOQZWzQ5S1crMuTAiElRKuTFjFo4u74sgAJTA6FQADcwCMpRBKcxJjTorMxEzkhouftuXCGGpIdCpADmZJxfzJLY5Cp5pLydcSLj7gSqeE9d96Yh+TppKoXfkGKdlHloUyFIDUvMXBzbFl3J4LprWt6AMpgfpDLpQDWesgFovDMlXf2ffU-BBZZKuczWWylRRwvlM6Q2LIMZQ2QdHZQeq65245vqDbgPOuBunCEP88MqPQchwVNLKaHptTSYUuRI6f4npyJcfYm5Ylozobz8ShamRWkGhBmeTSQeJX+JXQ6H88jQnCMiugoELCpypQKJeWa3l6U6er0hazvOajPgGr4NsGH6WhYsHOkycglLCEJ7iclgaIosKSLGGgKFe0o3AA4lg3AABZgCQJb4KQUAAK7oK83CwBQHG4DAIwCSQJAiXxJCCcJODcAu2FBsuH55GGJ7irYHYqHpGzGKYWiWB2nK2Kcv6wWO8E5jibGcdxvH8UJIliQAInAqFDGWtY6h8i7vlkZREbYZg6JuwEmQgfxhnkHbiHYJ7yHYTGIU5XE8TgpZucponSISADuWBRLl+BdGwAncBWAkAEboDwClKSJanTEucS1Bk36DicDCpOYLoKHu-x9tGuhgoozKpBlk5ZS5FX5R50gAGpYJQnAQOxJZkBA-BgNIXQqqgADWh0qhtW3sWAeYlv0hKKe5KntW+YRFGi5RyOKDrZEaUW8pppTguGuwDRFEO-nNjnsdlrlPQVsBrVd228LlZDcSQqDemwlDsQAZtj6DSJdm2o7d91gI9rUvYFL4dYIH0RV9P0Zv9CiA3EwMAmCWgVHYKVwY0yE4oqKqcGAxV1Y1zU1uMdNYQzjbMpYcgQlFxFq66u6xXRALbkyg7-Bz0bQzcYsS1LxIEMttOYfWGldYcCWwQmNiRrU0Jq2GRxJCbHZqC6ahm96FuSwqSqquqtuqQrDudVs4iJhUyZtn9qjQokYKHjk6hSLsZhciH0hh1LACiEDNTHr04ZpJT6bIEXxcKp50YDRT-mGrrJbUgFDp3xfIFxAynbx1PPaJe0HUdOAnedJMozd4+IzXjuIJCLIQqUWjJS4MVFBByS7BzyJq38WTB-ZIs3sPo8VcvHlTzgh3HWdF2L3OD8qZST66upCdxUTLBJOUUHB6GFPpSQXt1CWEGpaOiv4EyD1vmPBGj9MbY2kLjAmRMF5kyXmg7+q8AFJwbjkXQEVsj5FyO3RAiQjjfi5CReYPck7iA8FmHAqAIBwEEAhXMf8la4VEMsWwgJuTTXNGYK0tC4rwkDvsAaSQ-qlAhIPPEkBBFvWEVycoFQhywUHGaHWH04Q7FUNBdc+x2FXwnDiSss5eJyzwFo2ucQIplBWHrcEVhuoFFirCeEuxsj8mWEOFYmZhZ2JvNOXyc4ICuLXggaxCJwG70AiUHQfIzHBKHGYDMJFhTFwWjlPKhDRKJJIRFGQcIFBsiOIHJw8ZIQ7CUGrY+9g9AnmKbDRaZSaaFRKmVNGpYqo1Uqe+FKYZan1PyElbJYjrB6C5CUJQ9DL5ROvN6Ep8MBlI3WvgkZEzGzyAbnkZK-wjan38UzeEzZtBswdADYupdjm4XoTIOwuwHB5HyGkYyRRdBBLosCIE6ZvpnFsVs24KD77lPgEFf+kzxRH32EyFYlpOzQjAZYV2ehTkfI4W4IAA */ | ||||
|   context: { | ||||
|     commands: [], | ||||
|     selectedCommand: undefined, | ||||
| @ -421,14 +425,6 @@ export const commandBarMachine = setup({ | ||||
|           target: 'Selecting command', | ||||
|         }, | ||||
|  | ||||
|         'Find and select command': { | ||||
|           target: 'Command selected', | ||||
|           actions: [ | ||||
|             'Find and select command', | ||||
|             'Initialize arguments to submit', | ||||
|           ], | ||||
|         }, | ||||
|  | ||||
|         'Add commands': { | ||||
|           target: 'Closed', | ||||
|  | ||||
| @ -440,8 +436,6 @@ export const commandBarMachine = setup({ | ||||
|                 ), | ||||
|             }), | ||||
|           ], | ||||
|  | ||||
|           reenter: false, | ||||
|         }, | ||||
|  | ||||
|         'Remove commands': { | ||||
| @ -458,10 +452,13 @@ export const commandBarMachine = setup({ | ||||
|                 ), | ||||
|             }), | ||||
|           ], | ||||
|  | ||||
|           reenter: false, | ||||
|         }, | ||||
|       }, | ||||
|  | ||||
|       always: { | ||||
|         target: 'Command selected', | ||||
|         guard: 'Has selected command', | ||||
|       }, | ||||
|     }, | ||||
|  | ||||
|     'Selecting command': { | ||||
| @ -478,7 +475,7 @@ export const commandBarMachine = setup({ | ||||
|         { | ||||
|           target: 'Closed', | ||||
|           guard: 'Command has no arguments', | ||||
|           actions: ['Execute command'], | ||||
|           actions: ['Execute command', 'Clear selected command'], | ||||
|         }, | ||||
|         { | ||||
|           target: 'Checking Arguments', | ||||
| @ -548,7 +545,7 @@ export const commandBarMachine = setup({ | ||||
|       on: { | ||||
|         'Submit command': { | ||||
|           target: 'Closed', | ||||
|           actions: ['Execute command'], | ||||
|           actions: ['Execute command', 'Clear selected command'], | ||||
|         }, | ||||
|  | ||||
|         'Add argument': { | ||||
| @ -580,7 +577,7 @@ export const commandBarMachine = setup({ | ||||
|           }, | ||||
|           { | ||||
|             target: 'Closed', | ||||
|             actions: 'Execute command', | ||||
|             actions: ['Execute command', 'Clear selected command'], | ||||
|           }, | ||||
|         ], | ||||
|         onError: [ | ||||
| @ -600,6 +597,7 @@ export const commandBarMachine = setup({ | ||||
|  | ||||
|     Close: { | ||||
|       target: '.Closed', | ||||
|       actions: 'Clear selected command', | ||||
|     }, | ||||
|  | ||||
|     Clear: { | ||||
| @ -607,6 +605,11 @@ export const commandBarMachine = setup({ | ||||
|       reenter: false, | ||||
|       actions: ['Clear argument data'], | ||||
|     }, | ||||
|  | ||||
|     'Find and select command': { | ||||
|       target: '.Command selected', | ||||
|       actions: ['Find and select command', 'Initialize arguments to submit'], | ||||
|     }, | ||||
|   }, | ||||
| }) | ||||
|  | ||||
|  | ||||
| @ -133,6 +133,8 @@ export interface SketchDetails { | ||||
|   zAxis: [number, number, number] | ||||
|   yAxis: [number, number, number] | ||||
|   origin: [number, number, number] | ||||
|   // face id or plane id, both are strings | ||||
|   animateTargetId?: string | ||||
| } | ||||
|  | ||||
| export interface SegmentOverlay { | ||||
|  | ||||
| @ -43,6 +43,7 @@ export const settingsMachine = setup({ | ||||
|     'Execute AST': () => {}, | ||||
|     toastSuccess: () => {}, | ||||
|     setClientSideSceneUnits: () => {}, | ||||
|     setAllowOrbitInSketchMode: () => {}, | ||||
|     persistSettings: () => {}, | ||||
|     resetSettings: assign(({ context, event }) => { | ||||
|       if (!('level' in event)) return {} | ||||
| @ -157,6 +158,15 @@ export const settingsMachine = setup({ | ||||
|           actions: ['setSettingAtLevel', 'toastSuccess'], | ||||
|         }, | ||||
|  | ||||
|         'set.app.allowOrbitInSketchMode': { | ||||
|           target: 'persisting settings', | ||||
|           actions: [ | ||||
|             'setSettingAtLevel', | ||||
|             'toastSuccess', | ||||
|             'setAllowOrbitInSketchMode', | ||||
|           ], | ||||
|         }, | ||||
|  | ||||
|         'set.modeling.cameraProjection': { | ||||
|           target: 'persisting settings', | ||||
|  | ||||
| @ -183,6 +193,7 @@ export const settingsMachine = setup({ | ||||
|             'setClientSideSceneUnits', | ||||
|             'Execute AST', | ||||
|             'setClientTheme', | ||||
|             'setAllowOrbitInSketchMode', | ||||
|           ], | ||||
|         }, | ||||
|  | ||||
| @ -194,6 +205,7 @@ export const settingsMachine = setup({ | ||||
|             'setClientSideSceneUnits', | ||||
|             'Execute AST', | ||||
|             'setClientTheme', | ||||
|             'setAllowOrbitInSketchMode', | ||||
|           ], | ||||
|         }, | ||||
|  | ||||
|  | ||||
| @ -205,6 +205,7 @@ export function OnboardingButtons({ | ||||
|         </p> | ||||
|       )} | ||||
|       <ActionButton | ||||
|         autoFocus | ||||
|         Element="button" | ||||
|         onClick={next} | ||||
|         iconStart={{ icon: 'arrowRight', bgClassName: 'dark:bg-chalkboard-80' }} | ||||
|  | ||||
| @ -815,7 +815,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr | ||||
|                 context_type: crate::execution::ContextType::Mock, | ||||
|             }; | ||||
|  | ||||
|             if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await { | ||||
|             if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new(&ctx.settings)).await { | ||||
|                     return Err(miette::Report::new(crate::errors::Report { | ||||
|                         error: e, | ||||
|                         filename: format!("{}{}", #fn_name, #index), | ||||
|  | ||||
| @ -14,7 +14,10 @@ mod test_examples_someFn { | ||||
|             settings: Default::default(), | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await { | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|                 error: e, | ||||
|                 filename: format!("{}{}", "someFn", 0usize), | ||||
|  | ||||
| @ -14,7 +14,10 @@ mod test_examples_someFn { | ||||
|             settings: Default::default(), | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await { | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|                 error: e, | ||||
|                 filename: format!("{}{}", "someFn", 0usize), | ||||
|  | ||||
| @ -15,7 +15,10 @@ mod test_examples_show { | ||||
|             settings: Default::default(), | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await { | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|                 error: e, | ||||
|                 filename: format!("{}{}", "show", 0usize), | ||||
| @ -69,7 +72,10 @@ mod test_examples_show { | ||||
|             settings: Default::default(), | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await { | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|                 error: e, | ||||
|                 filename: format!("{}{}", "show", 1usize), | ||||
|  | ||||
| @ -15,7 +15,10 @@ mod test_examples_show { | ||||
|             settings: Default::default(), | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await { | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|                 error: e, | ||||
|                 filename: format!("{}{}", "show", 0usize), | ||||
|  | ||||
| @ -16,7 +16,10 @@ mod test_examples_my_func { | ||||
|             settings: Default::default(), | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await { | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|                 error: e, | ||||
|                 filename: format!("{}{}", "my_func", 0usize), | ||||
| @ -70,7 +73,10 @@ mod test_examples_my_func { | ||||
|             settings: Default::default(), | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await { | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|                 error: e, | ||||
|                 filename: format!("{}{}", "my_func", 1usize), | ||||
|  | ||||
| @ -16,7 +16,10 @@ mod test_examples_line_to { | ||||
|             settings: Default::default(), | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await { | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|                 error: e, | ||||
|                 filename: format!("{}{}", "line_to", 0usize), | ||||
| @ -70,7 +73,10 @@ mod test_examples_line_to { | ||||
|             settings: Default::default(), | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await { | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|                 error: e, | ||||
|                 filename: format!("{}{}", "line_to", 1usize), | ||||
|  | ||||
| @ -15,7 +15,10 @@ mod test_examples_min { | ||||
|             settings: Default::default(), | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await { | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|                 error: e, | ||||
|                 filename: format!("{}{}", "min", 0usize), | ||||
| @ -69,7 +72,10 @@ mod test_examples_min { | ||||
|             settings: Default::default(), | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await { | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|                 error: e, | ||||
|                 filename: format!("{}{}", "min", 1usize), | ||||
|  | ||||
| @ -15,7 +15,10 @@ mod test_examples_show { | ||||
|             settings: Default::default(), | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await { | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|                 error: e, | ||||
|                 filename: format!("{}{}", "show", 0usize), | ||||
|  | ||||
| @ -15,7 +15,10 @@ mod test_examples_import { | ||||
|             settings: Default::default(), | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await { | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|                 error: e, | ||||
|                 filename: format!("{}{}", "import", 0usize), | ||||
|  | ||||
| @ -15,7 +15,10 @@ mod test_examples_import { | ||||
|             settings: Default::default(), | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await { | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|                 error: e, | ||||
|                 filename: format!("{}{}", "import", 0usize), | ||||
|  | ||||
| @ -15,7 +15,10 @@ mod test_examples_import { | ||||
|             settings: Default::default(), | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await { | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|                 error: e, | ||||
|                 filename: format!("{}{}", "import", 0usize), | ||||
|  | ||||
| @ -15,7 +15,10 @@ mod test_examples_show { | ||||
|             settings: Default::default(), | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await { | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|                 error: e, | ||||
|                 filename: format!("{}{}", "show", 0usize), | ||||
|  | ||||
| @ -14,7 +14,10 @@ mod test_examples_some_function { | ||||
|             settings: Default::default(), | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await { | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|                 error: e, | ||||
|                 filename: format!("{}{}", "some_function", 0usize), | ||||
|  | ||||
| @ -164,7 +164,7 @@ async fn snapshot_endpoint(body: Bytes, state: ExecutorContext) -> Response<Body | ||||
|     }; | ||||
|  | ||||
|     eprintln!("Executing {test_name}"); | ||||
|     let mut exec_state = ExecState::new(); | ||||
|     let mut exec_state = ExecState::new(&state.settings); | ||||
|     // This is a shitty source range, I don't know what else to use for it though. | ||||
|     // There's no actual KCL associated with this reset_scene call. | ||||
|     if let Err(e) = state | ||||
|  | ||||
| @ -16,7 +16,7 @@ pub async fn kcl_to_engine_core(code: &str) -> Result<String> { | ||||
|     let ctx = ExecutorContext::new_forwarded_mock(Arc::new(Box::new( | ||||
|         crate::conn_mock_core::EngineConnection::new(ref_result).await?, | ||||
|     ))); | ||||
|     ctx.run(program.into(), &mut ExecState::new()).await?; | ||||
|     ctx.run(program.into(), &mut ExecState::new(&ctx.settings)).await?; | ||||
|  | ||||
|     let result = result.lock().expect("mutex lock").clone(); | ||||
|     Ok(result) | ||||
|  | ||||
| @ -30,13 +30,16 @@ impl From<KclErrorWithOutputs> for ExecError { | ||||
| #[derive(Debug)] | ||||
| pub struct ExecErrorWithState { | ||||
|     pub error: ExecError, | ||||
|     pub exec_state: crate::ExecState, | ||||
|     pub exec_state: Option<crate::ExecState>, | ||||
| } | ||||
|  | ||||
| impl ExecErrorWithState { | ||||
|     #[cfg_attr(target_arch = "wasm32", expect(dead_code))] | ||||
|     pub fn new(error: ExecError, exec_state: crate::ExecState) -> Self { | ||||
|         Self { error, exec_state } | ||||
|         Self { | ||||
|             error, | ||||
|             exec_state: Some(exec_state), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -44,7 +47,7 @@ impl From<ExecError> for ExecErrorWithState { | ||||
|     fn from(error: ExecError) -> Self { | ||||
|         Self { | ||||
|             error, | ||||
|             exec_state: Default::default(), | ||||
|             exec_state: None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -53,7 +56,7 @@ impl From<ConnectionError> for ExecErrorWithState { | ||||
|     fn from(error: ConnectionError) -> Self { | ||||
|         Self { | ||||
|             error: error.into(), | ||||
|             exec_state: Default::default(), | ||||
|             exec_state: None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -601,10 +601,24 @@ impl TryFrom<NumericSuffix> for UnitLen { | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)] | ||||
| impl From<crate::UnitLength> for UnitLen { | ||||
|     fn from(unit: crate::UnitLength) -> Self { | ||||
|         match unit { | ||||
|             crate::UnitLength::Cm => UnitLen::Cm, | ||||
|             crate::UnitLength::Ft => UnitLen::Feet, | ||||
|             crate::UnitLength::In => UnitLen::Inches, | ||||
|             crate::UnitLength::M => UnitLen::M, | ||||
|             crate::UnitLength::Mm => UnitLen::Mm, | ||||
|             crate::UnitLength::Yd => UnitLen::Yards, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)] | ||||
| #[ts(export)] | ||||
| #[serde(tag = "type")] | ||||
| pub enum UnitAngle { | ||||
|     #[default] | ||||
|     Degrees, | ||||
|     Radians, | ||||
| } | ||||
|  | ||||
| @ -21,7 +21,7 @@ type Point2D = kcmc::shared::Point2d<f64>; | ||||
| type Point3D = kcmc::shared::Point3d<f64>; | ||||
|  | ||||
| pub use function_param::FunctionParam; | ||||
| pub use kcl_value::{KclObjectFields, KclValue}; | ||||
| pub use kcl_value::{KclObjectFields, KclValue, UnitAngle, UnitLen}; | ||||
| use uuid::Uuid; | ||||
|  | ||||
| mod annotations; | ||||
| @ -78,7 +78,7 @@ pub struct GlobalState { | ||||
|     pub artifact_commands: Vec<ArtifactCommand>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq)] | ||||
| #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct ModuleState { | ||||
|     /// Program variable bindings. | ||||
| @ -116,21 +116,15 @@ pub struct ExecOutcome { | ||||
|     pub artifact_commands: Vec<ArtifactCommand>, | ||||
| } | ||||
|  | ||||
| impl Default for ExecState { | ||||
|     fn default() -> Self { | ||||
|         Self::new() | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl ExecState { | ||||
|     pub fn new() -> Self { | ||||
|     pub fn new(exec_settings: &ExecutorSettings) -> Self { | ||||
|         ExecState { | ||||
|             global: GlobalState::new(), | ||||
|             mod_local: ModuleState::default(), | ||||
|             mod_local: ModuleState::new(exec_settings), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn reset(&mut self) { | ||||
|     fn reset(&mut self, exec_settings: &ExecutorSettings) { | ||||
|         let mut id_generator = self.global.id_generator.clone(); | ||||
|         // We do not pop the ids, since we want to keep the same id generator. | ||||
|         // This is for the front end to keep track of the ids. | ||||
| @ -141,7 +135,7 @@ impl ExecState { | ||||
|  | ||||
|         *self = ExecState { | ||||
|             global, | ||||
|             mod_local: ModuleState::default(), | ||||
|             mod_local: ModuleState::new(exec_settings), | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| @ -205,6 +199,14 @@ impl ExecState { | ||||
|  | ||||
|         Ok(id) | ||||
|     } | ||||
|  | ||||
|     pub fn length_unit(&self) -> UnitLen { | ||||
|         self.mod_local.settings.default_length_units | ||||
|     } | ||||
|  | ||||
|     pub fn angle_unit(&self) -> UnitAngle { | ||||
|         self.mod_local.settings.default_angle_units | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl GlobalState { | ||||
| @ -233,6 +235,23 @@ impl GlobalState { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl ModuleState { | ||||
|     fn new(exec_settings: &ExecutorSettings) -> Self { | ||||
|         ModuleState { | ||||
|             memory: Default::default(), | ||||
|             dynamic_state: Default::default(), | ||||
|             pipe_value: Default::default(), | ||||
|             module_exports: Default::default(), | ||||
|             import_stack: Default::default(), | ||||
|             operations: Default::default(), | ||||
|             settings: MetaSettings { | ||||
|                 default_length_units: exec_settings.units.into(), | ||||
|                 default_angle_units: Default::default(), | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] | ||||
| #[ts(export)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| @ -241,15 +260,6 @@ pub struct MetaSettings { | ||||
|     pub default_angle_units: kcl_value::UnitAngle, | ||||
| } | ||||
|  | ||||
| impl Default for MetaSettings { | ||||
|     fn default() -> Self { | ||||
|         MetaSettings { | ||||
|             default_length_units: kcl_value::UnitLen::Mm, | ||||
|             default_angle_units: kcl_value::UnitAngle::Degrees, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl MetaSettings { | ||||
|     fn update_from_annotation(&mut self, annotation: &NonCodeValue, source_range: SourceRange) -> Result<(), KclError> { | ||||
|         let properties = annotations::expect_properties(annotations::SETTINGS, annotation, source_range)?; | ||||
| @ -1712,7 +1722,7 @@ pub struct ExecutorContext { | ||||
| #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] | ||||
| #[ts(export)] | ||||
| pub struct ExecutorSettings { | ||||
|     /// The unit to use in modeling dimensions. | ||||
|     /// The project-default unit to use in modeling dimensions. | ||||
|     pub units: UnitLength, | ||||
|     /// Highlight edges of 3D objects? | ||||
|     pub highlight_edges: bool, | ||||
| @ -2215,7 +2225,7 @@ impl ExecutorContext { | ||||
|  | ||||
|         if cache_result.clear_scene && !self.is_mock() { | ||||
|             // Pop the execution state, since we are starting fresh. | ||||
|             exec_state.reset(); | ||||
|             exec_state.reset(&self.settings); | ||||
|  | ||||
|             // We don't do this in mock mode since there is no engine connection | ||||
|             // anyways and from the TS side we override memory and don't want to clear it. | ||||
| @ -2458,7 +2468,7 @@ impl ExecutorContext { | ||||
|  | ||||
|         let mut local_state = ModuleState { | ||||
|             import_stack: exec_state.mod_local.import_stack.clone(), | ||||
|             ..Default::default() | ||||
|             ..ModuleState::new(&self.settings) | ||||
|         }; | ||||
|         local_state.import_stack.push(info.path.clone()); | ||||
|         std::mem::swap(&mut exec_state.mod_local, &mut local_state); | ||||
| @ -2831,7 +2841,7 @@ mod tests { | ||||
|             settings: Default::default(), | ||||
|             context_type: ContextType::Mock, | ||||
|         }; | ||||
|         let mut exec_state = ExecState::default(); | ||||
|         let mut exec_state = ExecState::new(&ctx.settings); | ||||
|         ctx.run(program.clone().into(), &mut exec_state).await?; | ||||
|  | ||||
|         Ok((program, ctx, exec_state)) | ||||
| @ -3303,6 +3313,25 @@ let shape = layer() |> patternTransform(10, transform, %) | ||||
|         assert_eq!(7.4, mem_get_json(exec_state.memory(), "thing").as_f64().unwrap()); | ||||
|     } | ||||
|  | ||||
|     #[tokio::test(flavor = "multi_thread")] | ||||
|     async fn test_unit_default() { | ||||
|         let ast = r#"const inMm = 25.4 * mm() | ||||
| const inInches = 1.0 * inch()"#; | ||||
|         let (_, _, exec_state) = parse_execute(ast).await.unwrap(); | ||||
|         assert_eq!(25.4, mem_get_json(exec_state.memory(), "inMm").as_f64().unwrap()); | ||||
|         assert_eq!(25.4, mem_get_json(exec_state.memory(), "inInches").as_f64().unwrap()); | ||||
|     } | ||||
|  | ||||
|     #[tokio::test(flavor = "multi_thread")] | ||||
|     async fn test_unit_overriden() { | ||||
|         let ast = r#"@settings(defaultLengthUnit = inch) | ||||
| const inMm = 25.4 * mm() | ||||
| const inInches = 1.0 * inch()"#; | ||||
|         let (_, _, exec_state) = parse_execute(ast).await.unwrap(); | ||||
|         assert_eq!(1.0, mem_get_json(exec_state.memory(), "inMm").as_f64().unwrap().round()); | ||||
|         assert_eq!(1.0, mem_get_json(exec_state.memory(), "inInches").as_f64().unwrap()); | ||||
|     } | ||||
|  | ||||
|     #[tokio::test(flavor = "multi_thread")] | ||||
|     async fn test_zero_param_fn() { | ||||
|         let ast = r#"const sigmaAllow = 35000 // psi | ||||
| @ -4046,7 +4075,7 @@ shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#; | ||||
|             .unwrap(); | ||||
|         let old_program = crate::Program::parse_no_errs(code).unwrap(); | ||||
|         // Execute the program. | ||||
|         let mut exec_state = Default::default(); | ||||
|         let mut exec_state = ExecState::new(&ctx.settings); | ||||
|         let cache_info = crate::CacheInformation { | ||||
|             old: None, | ||||
|             new_ast: old_program.ast.clone(), | ||||
|  | ||||
| @ -49,7 +49,7 @@ use crate::{ | ||||
|         token::TokenStream, | ||||
|         PIPE_OPERATOR, | ||||
|     }, | ||||
|     CacheInformation, ModuleId, OldAstState, Program, SourceRange, | ||||
|     CacheInformation, ExecState, ModuleId, OldAstState, Program, SourceRange, | ||||
| }; | ||||
| const SEMANTIC_TOKEN_TYPES: [SemanticTokenType; 10] = [ | ||||
|     SemanticTokenType::NUMBER, | ||||
| @ -693,7 +693,7 @@ impl Backend { | ||||
|         let mut exec_state = if let Some(last_successful_ast_state) = last_successful_ast_state.clone() { | ||||
|             last_successful_ast_state.exec_state | ||||
|         } else { | ||||
|             Default::default() | ||||
|             ExecState::new(&executor_ctx.settings) | ||||
|         }; | ||||
|  | ||||
|         if let Err(err) = executor_ctx | ||||
|  | ||||
| @ -121,6 +121,9 @@ pub struct AppSettings { | ||||
|     /// When the user is idle, and this is true, the stream will be torn down. | ||||
|     #[serde(default, alias = "streamIdleMode", skip_serializing_if = "is_default")] | ||||
|     stream_idle_mode: bool, | ||||
|     /// When the user is idle, and this is true, the stream will be torn down. | ||||
|     #[serde(default, alias = "allowOrbitInSketchMode", skip_serializing_if = "is_default")] | ||||
|     allow_orbit_in_sketch_mode: bool, | ||||
| } | ||||
|  | ||||
| // TODO: When we remove backwards compatibility with the old settings file, we can remove this. | ||||
| @ -586,6 +589,7 @@ textWrapping = true | ||||
|                         dismiss_web_banner: false, | ||||
|                         enable_ssao: None, | ||||
|                         stream_idle_mode: false, | ||||
|                         allow_orbit_in_sketch_mode: false, | ||||
|                     }, | ||||
|                     modeling: ModelingSettings { | ||||
|                         base_unit: UnitLength::In, | ||||
| @ -647,6 +651,7 @@ includeSettings = false | ||||
|                         dismiss_web_banner: false, | ||||
|                         enable_ssao: None, | ||||
|                         stream_idle_mode: false, | ||||
|                         allow_orbit_in_sketch_mode: false, | ||||
|                     }, | ||||
|                     modeling: ModelingSettings { | ||||
|                         base_unit: UnitLength::Yd, | ||||
| @ -713,6 +718,7 @@ defaultProjectName = "projects-$nnn" | ||||
|                         dismiss_web_banner: false, | ||||
|                         enable_ssao: None, | ||||
|                         stream_idle_mode: false, | ||||
|                         allow_orbit_in_sketch_mode: false, | ||||
|                     }, | ||||
|                     modeling: ModelingSettings { | ||||
|                         base_unit: UnitLength::Yd, | ||||
| @ -791,6 +797,7 @@ projectDirectory = "/Users/macinatormax/Documents/kittycad-modeling-projects""#; | ||||
|                         dismiss_web_banner: false, | ||||
|                         enable_ssao: None, | ||||
|                         stream_idle_mode: false, | ||||
|                         allow_orbit_in_sketch_mode: false, | ||||
|                     }, | ||||
|                     modeling: ModelingSettings { | ||||
|                         base_unit: UnitLength::Mm, | ||||
|  | ||||
| @ -124,6 +124,7 @@ includeSettings = false | ||||
|                         dismiss_web_banner: false, | ||||
|                         enable_ssao: None, | ||||
|                         stream_idle_mode: false, | ||||
|                         allow_orbit_in_sketch_mode: false, | ||||
|                     }, | ||||
|                     modeling: ModelingSettings { | ||||
|                         base_unit: UnitLength::Yd, | ||||
|  | ||||
| @ -96,25 +96,6 @@ impl Args { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[cfg(test)] | ||||
|     pub(crate) async fn new_test_args() -> Result<Self> { | ||||
|         use std::sync::Arc; | ||||
|  | ||||
|         Ok(Self { | ||||
|             args: Vec::new(), | ||||
|             kw_args: Default::default(), | ||||
|             source_range: SourceRange::default(), | ||||
|             ctx: ExecutorContext { | ||||
|                 engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await?)), | ||||
|                 fs: Arc::new(crate::fs::FileManager::new()), | ||||
|                 stdlib: Arc::new(crate::std::StdLib::new()), | ||||
|                 settings: Default::default(), | ||||
|                 context_type: crate::execution::ContextType::Mock, | ||||
|             }, | ||||
|             pipe_value: None, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     /// Get a keyword argument. If not set, returns None. | ||||
|     pub(crate) fn get_kw_arg_opt<'a, T>(&'a self, label: &str) -> Option<T> | ||||
|     where | ||||
|  | ||||
| @ -50,7 +50,7 @@ pub async fn helix(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K | ||||
| /// helixPath = helix({ | ||||
| ///     angleStart = 0, | ||||
| ///     ccw = true, | ||||
| ///     revolutions = 16, | ||||
| ///     revolutions = 5, | ||||
| ///     length = 10, | ||||
| ///     radius = 5, | ||||
| ///     axis = 'Z', | ||||
| @ -59,8 +59,8 @@ pub async fn helix(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K | ||||
| /// | ||||
| /// // Create a spring by sweeping around the helix path. | ||||
| /// springSketch = startSketchOn('YZ') | ||||
| ///     |> circle({ center = [0, 0], radius = 1 }, %) | ||||
| ///     //|> sweep({ path = helixPath }, %) | ||||
| ///     |> circle({ center = [0, 0], radius = 0.5 }, %) | ||||
| ///     |> sweep({ path = helixPath }, %) | ||||
| /// ``` | ||||
| /// | ||||
| /// ```no_run | ||||
| @ -72,7 +72,7 @@ pub async fn helix(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K | ||||
| /// helixPath = helix({ | ||||
| ///     angleStart = 0, | ||||
| ///     ccw = true, | ||||
| ///     revolutions = 16, | ||||
| ///     revolutions = 5, | ||||
| ///     length = 10, | ||||
| ///     radius = 5, | ||||
| ///     axis = edge001, | ||||
| @ -80,8 +80,8 @@ pub async fn helix(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K | ||||
| /// | ||||
| /// // Create a spring by sweeping around the helix path. | ||||
| /// springSketch = startSketchOn('XY') | ||||
| ///     |> circle({ center = [0, 0], radius = 1 }, %) | ||||
| ///     //|> sweep({ path = helixPath }, %) | ||||
| ///     |> circle({ center = [0, 0], radius = 0.5 }, %) | ||||
| ///     |> sweep({ path = helixPath }, %) | ||||
| /// ``` | ||||
| /// | ||||
| /// ```no_run | ||||
| @ -89,7 +89,7 @@ pub async fn helix(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K | ||||
| /// helixPath = helix({ | ||||
| ///     angleStart = 0, | ||||
| ///     ccw = true, | ||||
| ///     revolutions = 16, | ||||
| ///     revolutions = 5, | ||||
| ///     length = 10, | ||||
| ///     radius = 5, | ||||
| ///     axis = { | ||||
| @ -103,7 +103,7 @@ pub async fn helix(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K | ||||
| /// // Create a spring by sweeping around the helix path. | ||||
| /// springSketch = startSketchOn('XY') | ||||
| ///     |> circle({ center = [0, 0], radius = 1 }, %) | ||||
| ///     //|> sweep({ path = helixPath }, %) | ||||
| ///     |> sweep({ path = helixPath }, %) | ||||
| /// ``` | ||||
| #[stdlib { | ||||
|     name = "helix", | ||||
| @ -137,7 +137,7 @@ async fn inner_helix(data: HelixData, exec_state: &mut ExecState, args: Args) -> | ||||
|             }; | ||||
|  | ||||
|             args.batch_modeling_cmd( | ||||
|                 exec_state.next_uuid(), | ||||
|                 id, | ||||
|                 ModelingCmd::from(mcmd::EntityMakeHelixFromParams { | ||||
|                     radius: data.radius, | ||||
|                     is_clockwise: !data.ccw, | ||||
| @ -154,7 +154,7 @@ async fn inner_helix(data: HelixData, exec_state: &mut ExecState, args: Args) -> | ||||
|             let edge_id = edge.get_engine_id(exec_state, &args)?; | ||||
|  | ||||
|             args.batch_modeling_cmd( | ||||
|                 exec_state.next_uuid(), | ||||
|                 id, | ||||
|                 ModelingCmd::from(mcmd::EntityMakeHelixFromEdge { | ||||
|                     radius: data.radius, | ||||
|                     is_clockwise: !data.ccw, | ||||
|  | ||||
| @ -94,7 +94,7 @@ pub async fn sweep(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K | ||||
| /// helixPath = helix({ | ||||
| ///     angleStart = 0, | ||||
| ///     ccw = true, | ||||
| ///     revolutions = 16, | ||||
| ///     revolutions = 4, | ||||
| ///     length = 10, | ||||
| ///     radius = 5, | ||||
| ///     axis = 'Z', | ||||
| @ -104,7 +104,7 @@ pub async fn sweep(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K | ||||
| /// // Create a spring by sweeping around the helix path. | ||||
| /// springSketch = startSketchOn('YZ') | ||||
| ///     |> circle({ center = [0, 0], radius = 1 }, %) | ||||
| ///     //|> sweep({ path = helixPath }, %) | ||||
| ///     |> sweep({ path = helixPath }, %) | ||||
| /// ``` | ||||
| #[stdlib { | ||||
|     name = "sweep", | ||||
|  | ||||
| @ -5,14 +5,13 @@ use derive_docs::stdlib; | ||||
|  | ||||
| use crate::{ | ||||
|     errors::KclError, | ||||
|     execution::{ExecState, KclValue}, | ||||
|     settings::types::UnitLength, | ||||
|     execution::{ExecState, KclValue, UnitLen}, | ||||
|     std::Args, | ||||
| }; | ||||
|  | ||||
| /// Millimeters conversion factor for current projects units. | ||||
| pub async fn mm(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { | ||||
|     let result = inner_mm(&args)?; | ||||
| pub async fn mm(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { | ||||
|     let result = inner_mm(exec_state)?; | ||||
|  | ||||
|     Ok(args.make_user_val_from_f64(result)) | ||||
| } | ||||
| @ -40,20 +39,20 @@ pub async fn mm(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl | ||||
|     name = "mm", | ||||
|     tags = ["units"], | ||||
| }] | ||||
| fn inner_mm(args: &Args) -> Result<f64, KclError> { | ||||
|     match args.ctx.settings.units { | ||||
|         UnitLength::Mm => Ok(1.0), | ||||
|         UnitLength::In => Ok(measurements::Length::from_millimeters(1.0).as_inches()), | ||||
|         UnitLength::Ft => Ok(measurements::Length::from_millimeters(1.0).as_feet()), | ||||
|         UnitLength::M => Ok(measurements::Length::from_millimeters(1.0).as_meters()), | ||||
|         UnitLength::Cm => Ok(measurements::Length::from_millimeters(1.0).as_centimeters()), | ||||
|         UnitLength::Yd => Ok(measurements::Length::from_millimeters(1.0).as_yards()), | ||||
| fn inner_mm(exec_state: &ExecState) -> Result<f64, KclError> { | ||||
|     match exec_state.length_unit() { | ||||
|         UnitLen::Mm => Ok(1.0), | ||||
|         UnitLen::Inches => Ok(measurements::Length::from_millimeters(1.0).as_inches()), | ||||
|         UnitLen::Feet => Ok(measurements::Length::from_millimeters(1.0).as_feet()), | ||||
|         UnitLen::M => Ok(measurements::Length::from_millimeters(1.0).as_meters()), | ||||
|         UnitLen::Cm => Ok(measurements::Length::from_millimeters(1.0).as_centimeters()), | ||||
|         UnitLen::Yards => Ok(measurements::Length::from_millimeters(1.0).as_yards()), | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Inches conversion factor for current projects units. | ||||
| pub async fn inch(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { | ||||
|     let result = inner_inch(&args)?; | ||||
| pub async fn inch(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { | ||||
|     let result = inner_inch(exec_state)?; | ||||
|  | ||||
|     Ok(args.make_user_val_from_f64(result)) | ||||
| } | ||||
| @ -81,20 +80,20 @@ pub async fn inch(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K | ||||
|     name = "inch", | ||||
|     tags = ["units"], | ||||
| }] | ||||
| fn inner_inch(args: &Args) -> Result<f64, KclError> { | ||||
|     match args.ctx.settings.units { | ||||
|         UnitLength::Mm => Ok(measurements::Length::from_inches(1.0).as_millimeters()), | ||||
|         UnitLength::In => Ok(1.0), | ||||
|         UnitLength::Ft => Ok(measurements::Length::from_inches(1.0).as_feet()), | ||||
|         UnitLength::M => Ok(measurements::Length::from_inches(1.0).as_meters()), | ||||
|         UnitLength::Cm => Ok(measurements::Length::from_inches(1.0).as_centimeters()), | ||||
|         UnitLength::Yd => Ok(measurements::Length::from_inches(1.0).as_yards()), | ||||
| fn inner_inch(exec_state: &ExecState) -> Result<f64, KclError> { | ||||
|     match exec_state.length_unit() { | ||||
|         UnitLen::Mm => Ok(measurements::Length::from_inches(1.0).as_millimeters()), | ||||
|         UnitLen::Inches => Ok(1.0), | ||||
|         UnitLen::Feet => Ok(measurements::Length::from_inches(1.0).as_feet()), | ||||
|         UnitLen::M => Ok(measurements::Length::from_inches(1.0).as_meters()), | ||||
|         UnitLen::Cm => Ok(measurements::Length::from_inches(1.0).as_centimeters()), | ||||
|         UnitLen::Yards => Ok(measurements::Length::from_inches(1.0).as_yards()), | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Feet conversion factor for current projects units. | ||||
| pub async fn ft(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { | ||||
|     let result = inner_ft(&args)?; | ||||
| pub async fn ft(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { | ||||
|     let result = inner_ft(exec_state)?; | ||||
|  | ||||
|     Ok(args.make_user_val_from_f64(result)) | ||||
| } | ||||
| @ -123,20 +122,20 @@ pub async fn ft(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl | ||||
|     name = "ft", | ||||
|     tags = ["units"], | ||||
| }] | ||||
| fn inner_ft(args: &Args) -> Result<f64, KclError> { | ||||
|     match args.ctx.settings.units { | ||||
|         UnitLength::Mm => Ok(measurements::Length::from_feet(1.0).as_millimeters()), | ||||
|         UnitLength::In => Ok(measurements::Length::from_feet(1.0).as_inches()), | ||||
|         UnitLength::Ft => Ok(1.0), | ||||
|         UnitLength::M => Ok(measurements::Length::from_feet(1.0).as_meters()), | ||||
|         UnitLength::Cm => Ok(measurements::Length::from_feet(1.0).as_centimeters()), | ||||
|         UnitLength::Yd => Ok(measurements::Length::from_feet(1.0).as_yards()), | ||||
| fn inner_ft(exec_state: &ExecState) -> Result<f64, KclError> { | ||||
|     match exec_state.length_unit() { | ||||
|         UnitLen::Mm => Ok(measurements::Length::from_feet(1.0).as_millimeters()), | ||||
|         UnitLen::Inches => Ok(measurements::Length::from_feet(1.0).as_inches()), | ||||
|         UnitLen::Feet => Ok(1.0), | ||||
|         UnitLen::M => Ok(measurements::Length::from_feet(1.0).as_meters()), | ||||
|         UnitLen::Cm => Ok(measurements::Length::from_feet(1.0).as_centimeters()), | ||||
|         UnitLen::Yards => Ok(measurements::Length::from_feet(1.0).as_yards()), | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Meters conversion factor for current projects units. | ||||
| pub async fn m(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { | ||||
|     let result = inner_m(&args)?; | ||||
| pub async fn m(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { | ||||
|     let result = inner_m(exec_state)?; | ||||
|  | ||||
|     Ok(args.make_user_val_from_f64(result)) | ||||
| } | ||||
| @ -165,20 +164,20 @@ pub async fn m(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclE | ||||
|     name = "m", | ||||
|     tags = ["units"], | ||||
| }] | ||||
| fn inner_m(args: &Args) -> Result<f64, KclError> { | ||||
|     match args.ctx.settings.units { | ||||
|         UnitLength::Mm => Ok(measurements::Length::from_meters(1.0).as_millimeters()), | ||||
|         UnitLength::In => Ok(measurements::Length::from_meters(1.0).as_inches()), | ||||
|         UnitLength::Ft => Ok(measurements::Length::from_meters(1.0).as_feet()), | ||||
|         UnitLength::M => Ok(1.0), | ||||
|         UnitLength::Cm => Ok(measurements::Length::from_meters(1.0).as_centimeters()), | ||||
|         UnitLength::Yd => Ok(measurements::Length::from_meters(1.0).as_yards()), | ||||
| fn inner_m(exec_state: &ExecState) -> Result<f64, KclError> { | ||||
|     match exec_state.length_unit() { | ||||
|         UnitLen::Mm => Ok(measurements::Length::from_meters(1.0).as_millimeters()), | ||||
|         UnitLen::Inches => Ok(measurements::Length::from_meters(1.0).as_inches()), | ||||
|         UnitLen::Feet => Ok(measurements::Length::from_meters(1.0).as_feet()), | ||||
|         UnitLen::M => Ok(1.0), | ||||
|         UnitLen::Cm => Ok(measurements::Length::from_meters(1.0).as_centimeters()), | ||||
|         UnitLen::Yards => Ok(measurements::Length::from_meters(1.0).as_yards()), | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Centimeters conversion factor for current projects units. | ||||
| pub async fn cm(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { | ||||
|     let result = inner_cm(&args)?; | ||||
| pub async fn cm(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { | ||||
|     let result = inner_cm(exec_state)?; | ||||
|  | ||||
|     Ok(args.make_user_val_from_f64(result)) | ||||
| } | ||||
| @ -207,20 +206,20 @@ pub async fn cm(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl | ||||
|     name = "cm", | ||||
|     tags = ["units"], | ||||
| }] | ||||
| fn inner_cm(args: &Args) -> Result<f64, KclError> { | ||||
|     match args.ctx.settings.units { | ||||
|         UnitLength::Mm => Ok(measurements::Length::from_centimeters(1.0).as_millimeters()), | ||||
|         UnitLength::In => Ok(measurements::Length::from_centimeters(1.0).as_inches()), | ||||
|         UnitLength::Ft => Ok(measurements::Length::from_centimeters(1.0).as_feet()), | ||||
|         UnitLength::M => Ok(measurements::Length::from_centimeters(1.0).as_meters()), | ||||
|         UnitLength::Cm => Ok(1.0), | ||||
|         UnitLength::Yd => Ok(measurements::Length::from_centimeters(1.0).as_yards()), | ||||
| fn inner_cm(exec_state: &ExecState) -> Result<f64, KclError> { | ||||
|     match exec_state.length_unit() { | ||||
|         UnitLen::Mm => Ok(measurements::Length::from_centimeters(1.0).as_millimeters()), | ||||
|         UnitLen::Inches => Ok(measurements::Length::from_centimeters(1.0).as_inches()), | ||||
|         UnitLen::Feet => Ok(measurements::Length::from_centimeters(1.0).as_feet()), | ||||
|         UnitLen::M => Ok(measurements::Length::from_centimeters(1.0).as_meters()), | ||||
|         UnitLen::Cm => Ok(1.0), | ||||
|         UnitLen::Yards => Ok(measurements::Length::from_centimeters(1.0).as_yards()), | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Yards conversion factor for current projects units. | ||||
| pub async fn yd(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { | ||||
|     let result = inner_yd(&args)?; | ||||
| pub async fn yd(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { | ||||
|     let result = inner_yd(exec_state)?; | ||||
|  | ||||
|     Ok(args.make_user_val_from_f64(result)) | ||||
| } | ||||
| @ -249,92 +248,13 @@ pub async fn yd(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl | ||||
|     name = "yd", | ||||
|     tags = ["units"], | ||||
| }] | ||||
| fn inner_yd(args: &Args) -> Result<f64, KclError> { | ||||
|     match args.ctx.settings.units { | ||||
|         UnitLength::Mm => Ok(measurements::Length::from_yards(1.0).as_millimeters()), | ||||
|         UnitLength::In => Ok(measurements::Length::from_yards(1.0).as_inches()), | ||||
|         UnitLength::Ft => Ok(measurements::Length::from_yards(1.0).as_feet()), | ||||
|         UnitLength::M => Ok(measurements::Length::from_yards(1.0).as_meters()), | ||||
|         UnitLength::Cm => Ok(measurements::Length::from_yards(1.0).as_centimeters()), | ||||
|         UnitLength::Yd => Ok(1.0), | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use pretty_assertions::assert_eq; | ||||
|  | ||||
|     use super::*; | ||||
|  | ||||
|     #[tokio::test(flavor = "multi_thread")] | ||||
|     async fn test_units_inner_mm() { | ||||
|         let mut args = Args::new_test_args().await.unwrap(); | ||||
|         args.ctx.settings.units = UnitLength::Mm; | ||||
|         let result = inner_mm(&args).unwrap(); | ||||
|         assert_eq!(result, 1.0); | ||||
|  | ||||
|         args.ctx.settings.units = UnitLength::In; | ||||
|         let result = inner_mm(&args).unwrap(); | ||||
|         assert_eq!(result, 1.0 / 25.4); | ||||
|     } | ||||
|  | ||||
|     #[tokio::test(flavor = "multi_thread")] | ||||
|     async fn test_units_inner_inch() { | ||||
|         let mut args = Args::new_test_args().await.unwrap(); | ||||
|         args.ctx.settings.units = UnitLength::In; | ||||
|         let result = inner_inch(&args).unwrap(); | ||||
|         assert_eq!(result, 1.0); | ||||
|  | ||||
|         args.ctx.settings.units = UnitLength::Mm; | ||||
|         let result = inner_inch(&args).unwrap(); | ||||
|         assert_eq!(result, 25.4); | ||||
|     } | ||||
|  | ||||
|     #[tokio::test(flavor = "multi_thread")] | ||||
|     async fn test_units_inner_ft() { | ||||
|         let mut args = Args::new_test_args().await.unwrap(); | ||||
|         args.ctx.settings.units = UnitLength::Ft; | ||||
|         let result = inner_ft(&args).unwrap(); | ||||
|         assert_eq!(result, 1.0); | ||||
|  | ||||
|         args.ctx.settings.units = UnitLength::Mm; | ||||
|         let result = inner_ft(&args).unwrap(); | ||||
|         assert_eq!(result, 304.8); | ||||
|     } | ||||
|  | ||||
|     #[tokio::test(flavor = "multi_thread")] | ||||
|     async fn test_units_inner_m() { | ||||
|         let mut args = Args::new_test_args().await.unwrap(); | ||||
|         args.ctx.settings.units = UnitLength::M; | ||||
|         let result = inner_m(&args).unwrap(); | ||||
|         assert_eq!(result, 1.0); | ||||
|  | ||||
|         args.ctx.settings.units = UnitLength::Mm; | ||||
|         let result = inner_m(&args).unwrap(); | ||||
|         assert_eq!(result, 1000.0); | ||||
|     } | ||||
|  | ||||
|     #[tokio::test(flavor = "multi_thread")] | ||||
|     async fn test_units_inner_cm() { | ||||
|         let mut args = Args::new_test_args().await.unwrap(); | ||||
|         args.ctx.settings.units = UnitLength::Cm; | ||||
|         let result = inner_cm(&args).unwrap(); | ||||
|         assert_eq!(result, 1.0); | ||||
|  | ||||
|         args.ctx.settings.units = UnitLength::Mm; | ||||
|         let result = inner_cm(&args).unwrap(); | ||||
|         assert_eq!(result, 10.0); | ||||
|     } | ||||
|  | ||||
|     #[tokio::test(flavor = "multi_thread")] | ||||
|     async fn test_units_inner_yd() { | ||||
|         let mut args = Args::new_test_args().await.unwrap(); | ||||
|         args.ctx.settings.units = UnitLength::Yd; | ||||
|         let result = inner_yd(&args).unwrap(); | ||||
|         assert_eq!(result, 1.0); | ||||
|  | ||||
|         args.ctx.settings.units = UnitLength::Mm; | ||||
|         let result = inner_yd(&args).unwrap(); | ||||
|         assert_eq!(result, 914.4); | ||||
| fn inner_yd(exec_state: &ExecState) -> Result<f64, KclError> { | ||||
|     match exec_state.length_unit() { | ||||
|         UnitLen::Mm => Ok(measurements::Length::from_yards(1.0).as_millimeters()), | ||||
|         UnitLen::Inches => Ok(measurements::Length::from_yards(1.0).as_inches()), | ||||
|         UnitLen::Feet => Ok(measurements::Length::from_yards(1.0).as_feet()), | ||||
|         UnitLen::M => Ok(measurements::Length::from_yards(1.0).as_meters()), | ||||
|         UnitLen::Cm => Ok(measurements::Length::from_yards(1.0).as_centimeters()), | ||||
|         UnitLen::Yards => Ok(1.0), | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -6,7 +6,7 @@ use crate::{ | ||||
|     errors::ExecErrorWithState, | ||||
|     execution::{new_zoo_client, ArtifactCommand, ExecutorContext, ExecutorSettings, Operation, ProgramMemory}, | ||||
|     settings::types::UnitLength, | ||||
|     ConnectionError, ExecError, KclErrorWithOutputs, Program, | ||||
|     ConnectionError, ExecError, ExecState, KclErrorWithOutputs, Program, | ||||
| }; | ||||
|  | ||||
| #[derive(serde::Deserialize, serde::Serialize)] | ||||
| @ -72,7 +72,7 @@ async fn do_execute_and_snapshot( | ||||
|     ctx: &ExecutorContext, | ||||
|     program: Program, | ||||
| ) -> Result<(crate::execution::ExecState, image::DynamicImage), ExecErrorWithState> { | ||||
|     let mut exec_state = Default::default(); | ||||
|     let mut exec_state = ExecState::new(&ctx.settings); | ||||
|     let snapshot_png_bytes = ctx | ||||
|         .execute_and_prepare_snapshot(&program, &mut exec_state) | ||||
|         .await | ||||
|  | ||||
| Before Width: | Height: | Size: 182 KiB After Width: | Height: | Size: 188 KiB | 
| Before Width: | Height: | Size: 198 KiB After Width: | Height: | Size: 184 KiB | 
| Before Width: | Height: | Size: 191 KiB After Width: | Height: | Size: 175 KiB | 
| Before Width: | Height: | Size: 182 KiB After Width: | Height: | Size: 168 KiB | 
| @ -80,7 +80,7 @@ pub async fn execute( | ||||
|         kcl_lib::ExecutorContext::new(engine_manager, fs_manager, settings.into()).await? | ||||
|     }; | ||||
|  | ||||
|     let mut exec_state = ExecState::default(); | ||||
|     let mut exec_state = ExecState::new(&ctx.settings); | ||||
|     let mut old_ast_memory = None; | ||||
|  | ||||
|     // Populate from the old exec state if it exists. | ||||
|  | ||||
| @ -18,7 +18,7 @@ async fn cache_test( | ||||
|         .ok_or_else(|| anyhow::anyhow!("No variations provided for test '{}'", test_name))?; | ||||
|  | ||||
|     let mut ctx = kcl_lib::ExecutorContext::new_with_client(first.settings.clone(), None, None).await?; | ||||
|     let mut exec_state = kcl_lib::ExecState::default(); | ||||
|     let mut exec_state = kcl_lib::ExecState::new(&ctx.settings); | ||||
|  | ||||
|     let mut old_ast_state = None; | ||||
|     let mut img_results = Vec::new(); | ||||
|  | ||||
| @ -10,7 +10,7 @@ use pretty_assertions::assert_eq; | ||||
| async fn setup(code: &str, name: &str) -> Result<(ExecutorContext, Program, ModuleId, uuid::Uuid)> { | ||||
|     let program = Program::parse_no_errs(code)?; | ||||
|     let ctx = kcl_lib::ExecutorContext::new_with_default_client(Default::default()).await?; | ||||
|     let mut exec_state = ExecState::default(); | ||||
|     let mut exec_state = ExecState::new(&ctx.settings); | ||||
|     ctx.run(program.clone().into(), &mut exec_state).await?; | ||||
|  | ||||
|     // We need to get the sketch ID. | ||||
|  | ||||