Merge branch 'main' into coredump-clientstate
This commit is contained in:
		| @ -2440,6 +2440,293 @@ test('Extrude from command bar selects extrude line after', async ({ | ||||
| }) | ||||
|  | ||||
| test.describe('Testing constraints', () => { | ||||
|   test.describe('Test ABS distance constraint', () => { | ||||
|     const cases = [ | ||||
|       { | ||||
|         testName: 'Add variable', | ||||
|         addVariable: true, | ||||
|         constraint: 'ABS X', | ||||
|         value: 'xDis001, 61.34', | ||||
|       }, | ||||
|       { | ||||
|         testName: 'No variable', | ||||
|         addVariable: false, | ||||
|         constraint: 'ABS X', | ||||
|         value: '154.9, 61.34', | ||||
|       }, | ||||
|       { | ||||
|         testName: 'Add variable', | ||||
|         addVariable: true, | ||||
|         constraint: 'ABS Y', | ||||
|         value: '154.9, yDis001', | ||||
|       }, | ||||
|       { | ||||
|         testName: 'No variable', | ||||
|         addVariable: false, | ||||
|         constraint: 'ABS Y', | ||||
|         value: '154.9, 61.34', | ||||
|       }, | ||||
|     ] as const | ||||
|     for (const { testName, addVariable, value, constraint } of cases) { | ||||
|       test(`${constraint} - ${testName}`, async ({ page }) => { | ||||
|         await page.addInitScript(async () => { | ||||
|           localStorage.setItem( | ||||
|             'persistCode', | ||||
|             `const yo = 5 | ||||
| const part001 = startSketchOn('XZ') | ||||
|   |> startProfileAt([-7.54, -26.74], %) | ||||
|   |> line([74.36, 130.4], %) | ||||
|   |> line([78.92, -120.11], %) | ||||
|   |> line([9.16, 77.79], %) | ||||
|   |> line([41.19, 28.97], %) | ||||
| const part002 = startSketchOn('XZ') | ||||
|   |> startProfileAt([299.05, 231.45], %) | ||||
|   |> xLine(-425.34, %, 'seg-what') | ||||
|   |> yLine(-264.06, %) | ||||
|   |> xLine(segLen('seg-what', %), %) | ||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %)` | ||||
|           ) | ||||
|         }) | ||||
|         const u = await getUtils(page) | ||||
|         await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|         await page.goto('/') | ||||
|         await u.waitForAuthSkipAppStart() | ||||
|  | ||||
|         await page.getByText('line([74.36, 130.4], %)').click() | ||||
|         await page.getByRole('button', { name: 'Edit Sketch' }).click() | ||||
|  | ||||
|         const [line3] = await Promise.all([ | ||||
|           u.getSegmentBodyCoords(`[data-overlay-index="${2}"]`), | ||||
|         ]) | ||||
|  | ||||
|         if (constraint === 'ABS X') { | ||||
|           await page.mouse.click(600, 130) | ||||
|         } else { | ||||
|           await page.mouse.click(900, 250) | ||||
|         } | ||||
|         await page.keyboard.down('Shift') | ||||
|         await page.mouse.click(line3.x, line3.y) | ||||
|         await page.waitForTimeout(100) // this wait is needed for webkit - not sure why | ||||
|         await page.keyboard.up('Shift') | ||||
|         await page | ||||
|           .getByRole('button', { | ||||
|             name: 'Constrain', | ||||
|           }) | ||||
|           .click() | ||||
|         await page | ||||
|           .getByRole('button', { name: constraint, exact: true }) | ||||
|           .click() | ||||
|  | ||||
|         const createNewVariableCheckbox = page.getByTestId( | ||||
|           'create-new-variable-checkbox' | ||||
|         ) | ||||
|         const isChecked = await createNewVariableCheckbox.isChecked() | ||||
|         ;((isChecked && !addVariable) || (!isChecked && addVariable)) && | ||||
|           (await createNewVariableCheckbox.click()) | ||||
|  | ||||
|         await page | ||||
|           .getByRole('button', { name: 'Add constraining value' }) | ||||
|           .click() | ||||
|  | ||||
|         // checking activeLines assures the cursors are where they should be | ||||
|         const codeAfter = [`|> lineTo([${value}], %)`] | ||||
|  | ||||
|         const activeLinesContent = await page.locator('.cm-activeLine').all() | ||||
|         await Promise.all( | ||||
|           activeLinesContent.map(async (line, i) => { | ||||
|             await expect(page.locator('.cm-content')).toContainText( | ||||
|               codeAfter[i] | ||||
|             ) | ||||
|             // if the code is an active line then the cursor should be on that line | ||||
|             await expect(line).toHaveText(codeAfter[i]) | ||||
|           }) | ||||
|         ) | ||||
|  | ||||
|         // checking the count of the overlays is a good proxy check that the client sketch scene is in a good state | ||||
|         await expect(page.getByTestId('segment-overlay')).toHaveCount(4) | ||||
|       }) | ||||
|     } | ||||
|   }) | ||||
|   test.describe('Test Angle constraint double segment selection', () => { | ||||
|     const cases = [ | ||||
|       { | ||||
|         testName: 'Add variable', | ||||
|         addVariable: true, | ||||
|         axisSelect: false, | ||||
|         value: "segAng('seg01', %) + angle001", | ||||
|       }, | ||||
|       { | ||||
|         testName: 'No variable', | ||||
|         addVariable: false, | ||||
|         axisSelect: false, | ||||
|         value: "segAng('seg01', %) + 22.69", | ||||
|       }, | ||||
|       { | ||||
|         testName: 'Add variable, selecting axis', | ||||
|         addVariable: true, | ||||
|         axisSelect: true, | ||||
|         value: 'QUARTER_TURN - angle001', | ||||
|       }, | ||||
|       { | ||||
|         testName: 'No variable, selecting axis', | ||||
|         addVariable: false, | ||||
|         axisSelect: true, | ||||
|         value: 'QUARTER_TURN - 7', | ||||
|       }, | ||||
|     ] as const | ||||
|     for (const { testName, addVariable, value, axisSelect } of cases) { | ||||
|       test(`${testName}`, async ({ page }) => { | ||||
|         await page.addInitScript(async () => { | ||||
|           localStorage.setItem( | ||||
|             'persistCode', | ||||
|             `const yo = 5 | ||||
| const part001 = startSketchOn('XZ') | ||||
|   |> startProfileAt([-7.54, -26.74], %) | ||||
|   |> line([74.36, 130.4], %) | ||||
|   |> line([78.92, -120.11], %) | ||||
|   |> line([9.16, 77.79], %) | ||||
|   |> line([41.19, 28.97], %) | ||||
| const part002 = startSketchOn('XZ') | ||||
|   |> startProfileAt([299.05, 231.45], %) | ||||
|   |> xLine(-425.34, %, 'seg-what') | ||||
|   |> yLine(-264.06, %) | ||||
|   |> xLine(segLen('seg-what', %), %) | ||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %)` | ||||
|           ) | ||||
|         }) | ||||
|         const u = await getUtils(page) | ||||
|         await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|         await page.goto('/') | ||||
|         await u.waitForAuthSkipAppStart() | ||||
|  | ||||
|         await page.getByText('line([74.36, 130.4], %)').click() | ||||
|         await page.getByRole('button', { name: 'Edit Sketch' }).click() | ||||
|  | ||||
|         const [line1, line3] = await Promise.all([ | ||||
|           u.getSegmentBodyCoords(`[data-overlay-index="${0}"]`), | ||||
|           u.getSegmentBodyCoords(`[data-overlay-index="${2}"]`), | ||||
|         ]) | ||||
|  | ||||
|         if (axisSelect) { | ||||
|           await page.mouse.click(600, 130) | ||||
|         } else { | ||||
|           await page.mouse.click(line1.x, line1.y) | ||||
|         } | ||||
|         await page.keyboard.down('Shift') | ||||
|         await page.mouse.click(line3.x, line3.y) | ||||
|         await page.waitForTimeout(100) // this wait is needed for webkit - not sure why | ||||
|         await page.keyboard.up('Shift') | ||||
|         await page | ||||
|           .getByRole('button', { | ||||
|             name: 'Constrain', | ||||
|           }) | ||||
|           .click() | ||||
|         await page.getByTestId('angle').click() | ||||
|  | ||||
|         const createNewVariableCheckbox = page.getByTestId( | ||||
|           'create-new-variable-checkbox' | ||||
|         ) | ||||
|         const isChecked = await createNewVariableCheckbox.isChecked() | ||||
|         ;((isChecked && !addVariable) || (!isChecked && addVariable)) && | ||||
|           (await createNewVariableCheckbox.click()) | ||||
|  | ||||
|         await page | ||||
|           .getByRole('button', { name: 'Add constraining value' }) | ||||
|           .click() | ||||
|  | ||||
|         // checking activeLines assures the cursors are where they should be | ||||
|         const codeAfter = [ | ||||
|           "|> line([74.36, 130.4], %, 'seg01')", | ||||
|           `|> angledLine([${value}, 78.33], %)`, | ||||
|         ] | ||||
|         if (axisSelect) codeAfter.shift() | ||||
|  | ||||
|         const activeLinesContent = await page.locator('.cm-activeLine').all() | ||||
|         await Promise.all( | ||||
|           activeLinesContent.map(async (line, i) => { | ||||
|             await expect(page.locator('.cm-content')).toContainText( | ||||
|               codeAfter[i] | ||||
|             ) | ||||
|             // if the code is an active line then the cursor should be on that line | ||||
|             await expect(line).toHaveText(codeAfter[i]) | ||||
|           }) | ||||
|         ) | ||||
|  | ||||
|         // checking the count of the overlays is a good proxy check that the client sketch scene is in a good state | ||||
|         await expect(page.getByTestId('segment-overlay')).toHaveCount(4) | ||||
|       }) | ||||
|     } | ||||
|   }) | ||||
|   test.describe('Test Angle constraint single selection', () => { | ||||
|     const cases = [ | ||||
|       { | ||||
|         testName: 'Add variable', | ||||
|         addVariable: true, | ||||
|         value: 'angle001', | ||||
|       }, | ||||
|       { | ||||
|         testName: 'No variable', | ||||
|         addVariable: false, | ||||
|         value: '83', | ||||
|       }, | ||||
|     ] as const | ||||
|     for (const { testName, addVariable, value } of cases) { | ||||
|       test(`${testName}`, async ({ page }) => { | ||||
|         await page.addInitScript(async () => { | ||||
|           localStorage.setItem( | ||||
|             'persistCode', | ||||
|             `const yo = 5 | ||||
|     const part001 = startSketchOn('XZ') | ||||
|       |> startProfileAt([-7.54, -26.74], %) | ||||
|       |> line([74.36, 130.4], %) | ||||
|       |> line([78.92, -120.11], %) | ||||
|       |> line([9.16, 77.79], %) | ||||
|       |> line([41.19, 28.97], %) | ||||
|     const part002 = startSketchOn('XZ') | ||||
|       |> startProfileAt([299.05, 231.45], %) | ||||
|       |> xLine(-425.34, %, 'seg-what') | ||||
|       |> yLine(-264.06, %) | ||||
|       |> xLine(segLen('seg-what', %), %) | ||||
|       |> lineTo([profileStartX(%), profileStartY(%)], %)` | ||||
|           ) | ||||
|         }) | ||||
|         const u = await getUtils(page) | ||||
|         await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|         await page.goto('/') | ||||
|         await u.waitForAuthSkipAppStart() | ||||
|  | ||||
|         await page.getByText('line([74.36, 130.4], %)').click() | ||||
|         await page.getByRole('button', { name: 'Edit Sketch' }).click() | ||||
|  | ||||
|         const line3 = await u.getSegmentBodyCoords( | ||||
|           `[data-overlay-index="${2}"]` | ||||
|         ) | ||||
|  | ||||
|         await page.mouse.click(line3.x, line3.y) | ||||
|         await page | ||||
|           .getByRole('button', { | ||||
|             name: 'Constrain', | ||||
|           }) | ||||
|           .click() | ||||
|         await page.getByTestId('angle').click() | ||||
|  | ||||
|         if (!addVariable) { | ||||
|           await page.getByTestId('create-new-variable-checkbox').click() | ||||
|         } | ||||
|         await page | ||||
|           .getByRole('button', { name: 'Add constraining value' }) | ||||
|           .click() | ||||
|  | ||||
|         const changedCode = `|> angledLine([${value}, 78.33], %)` | ||||
|         await expect(page.locator('.cm-content')).toContainText(changedCode) | ||||
|         // checking active assures the cursor is where it should be | ||||
|         await expect(page.locator('.cm-activeLine')).toHaveText(changedCode) | ||||
|  | ||||
|         // checking the count of the overlays is a good proxy check that the client sketch scene is in a good state | ||||
|         await expect(page.getByTestId('segment-overlay')).toHaveCount(4) | ||||
|       }) | ||||
|     } | ||||
|   }) | ||||
|   test.describe('Many segments - no modal constraints', () => { | ||||
|     const cases = [ | ||||
|       { | ||||
|  | ||||
| @ -132,6 +132,19 @@ export async function getUtils(page: Page) { | ||||
|     }, | ||||
|     waitForCmdReceive: (commandType: string) => | ||||
|       waitForCmdReceive(page, commandType), | ||||
|     getSegmentBodyCoords: async (locator: string, px = 30) => { | ||||
|       const overlay = page.locator(locator) | ||||
|       const bbox = await overlay | ||||
|         .boundingBox() | ||||
|         .then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })) | ||||
|       const angle = Number(await overlay.getAttribute('data-overlay-angle')) | ||||
|       const angleXOffset = Math.cos(((angle - 180) * Math.PI) / 180) * px | ||||
|       const angleYOffset = Math.sin(((angle - 180) * Math.PI) / 180) * px | ||||
|       return { | ||||
|         x: bbox.x + angleXOffset, | ||||
|         y: bbox.y - angleYOffset, | ||||
|       } | ||||
|     }, | ||||
|     getBoundingBox: async (locator: string) => | ||||
|       page | ||||
|         .locator(locator) | ||||
|  | ||||
| @ -531,8 +531,7 @@ const ConstraintSymbol = ({ | ||||
|     varNameMap[_type as LineInputsType]?.implicitConstraintDesc | ||||
|  | ||||
|   const node = useMemo( | ||||
|     () => | ||||
|       getNodeFromPath<Value>(parse(recast(kclManager.ast)), pathToNode).node, | ||||
|     () => getNodeFromPath<Value>(kclManager.ast, pathToNode).node, | ||||
|     [kclManager.ast, pathToNode] | ||||
|   ) | ||||
|   const range: SourceRange = node ? [node.start, node.end] : [0, 0] | ||||
|  | ||||
| @ -39,6 +39,7 @@ export function ActionButtonDropdown({ | ||||
|               onClick={item.onClick} | ||||
|               className="block px-3 py-1 hover:bg-primary/10 dark:hover:bg-chalkboard-80 border-0 m-0 text-sm w-full rounded-none text-left disabled:!bg-transparent dark:disabled:text-chalkboard-60" | ||||
|               disabled={item.disabled} | ||||
|               data-testid={item.label} | ||||
|             > | ||||
|               <span className="capitalize">{item.label}</span> | ||||
|               {item.shortcut && ( | ||||
|  | ||||
| @ -214,13 +214,17 @@ export const CreateNewVariable = ({ | ||||
| }) => { | ||||
|   return ( | ||||
|     <> | ||||
|       <label htmlFor="create-new-variable" className="block mt-3 font-mono"> | ||||
|       <label | ||||
|         htmlFor="create-new-variable" | ||||
|         className="block mt-3 font-mono text-chalkboard-90" | ||||
|       > | ||||
|         Create new variable | ||||
|       </label> | ||||
|       <div className="mt-1 flex gap-2 items-center"> | ||||
|         {showCheckbox && ( | ||||
|           <input | ||||
|             type="checkbox" | ||||
|             data-testid="create-new-variable-checkbox" | ||||
|             checked={shouldCreateVariable} | ||||
|             onChange={(e) => { | ||||
|               setShouldCreateVariable(e.target.checked) | ||||
|  | ||||
| @ -11,7 +11,10 @@ import { | ||||
| import { SetSelections, modelingMachine } from 'machines/modelingMachine' | ||||
| import { useSetupEngineManager } from 'hooks/useSetupEngineManager' | ||||
| import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' | ||||
| import { isCursorInSketchCommandRange } from 'lang/util' | ||||
| import { | ||||
|   isCursorInSketchCommandRange, | ||||
|   updatePathToNodeFromMap, | ||||
| } from 'lang/util' | ||||
| import { | ||||
|   kclManager, | ||||
|   sceneInfra, | ||||
| @ -34,7 +37,6 @@ import { | ||||
|   handleSelectionBatch, | ||||
|   isSelectionLastLine, | ||||
|   isSketchPipe, | ||||
|   updateSelections, | ||||
| } from 'lib/selections' | ||||
| import { applyConstraintIntersect } from './Toolbar/Intersect' | ||||
| import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance' | ||||
| @ -54,7 +56,6 @@ import { | ||||
| } from 'lang/modifyAst' | ||||
| import { | ||||
|   Program, | ||||
|   Value, | ||||
|   VariableDeclaration, | ||||
|   coreDump, | ||||
|   parse, | ||||
| @ -75,7 +76,6 @@ import { useSearchParams } from 'react-router-dom' | ||||
| import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls' | ||||
| import { getVarNameModal } from 'hooks/useToolbarGuards' | ||||
| import useHotkeyWrapper from 'lib/hotkeyWrapper' | ||||
| import { applyConstraintEqualAngle } from './Toolbar/EqualAngle' | ||||
|  | ||||
| type MachineContext<T extends AnyStateMachine> = { | ||||
|   state: StateFrom<T> | ||||
| @ -221,7 +221,7 @@ export const ModelingMachineProvider = ({ | ||||
|               } | ||||
|             : {} | ||||
|         ), | ||||
|         'Set selection': assign(({ selectionRanges }, event) => { | ||||
|         'Set selection': assign(({ selectionRanges, sketchDetails }, event) => { | ||||
|           const setSelections = event.data as SetSelections // this was needed for ts after adding 'Set selection' action to on done modal events | ||||
|           if (!editorManager.editorView) return {} | ||||
|           const dispatchSelection = (selection?: EditorSelection) => { | ||||
| @ -311,8 +311,19 @@ export const ModelingMachineProvider = ({ | ||||
|           } | ||||
|           if (setSelections.selectionType === 'completeSelection') { | ||||
|             editorManager.selectRange(setSelections.selection) | ||||
|             if (!sketchDetails) | ||||
|               return { | ||||
|                 selectionRanges: setSelections.selection, | ||||
|               } | ||||
|             return { | ||||
|               selectionRanges: setSelections.selection, | ||||
|               sketchDetails: { | ||||
|                 ...sketchDetails, | ||||
|                 sketchPathToNode: | ||||
|                   setSelections.updatedPathToNode || | ||||
|                   sketchDetails?.sketchPathToNode || | ||||
|                   [], | ||||
|               }, | ||||
|             } | ||||
|           } | ||||
|  | ||||
| @ -533,6 +544,7 @@ export const ModelingMachineProvider = ({ | ||||
|         }, | ||||
|         'Get angle info': async ({ | ||||
|           selectionRanges, | ||||
|           sketchDetails, | ||||
|         }): Promise<SetSelections> => { | ||||
|           const { modifiedAst, pathToNodeMap } = await (angleBetweenInfo({ | ||||
|             selectionRanges, | ||||
| @ -544,14 +556,27 @@ export const ModelingMachineProvider = ({ | ||||
|                 selectionRanges, | ||||
|                 angleOrLength: 'setAngle', | ||||
|               })) | ||||
|           await kclManager.updateAst(modifiedAst, true) | ||||
|           const _modifiedAst = parse(recast(modifiedAst)) | ||||
|           if (!sketchDetails) throw new Error('No sketch details') | ||||
|           const updatedPathToNode = updatePathToNodeFromMap( | ||||
|             sketchDetails.sketchPathToNode, | ||||
|             pathToNodeMap | ||||
|           ) | ||||
|           await sceneEntitiesManager.updateAstAndRejigSketch( | ||||
|             updatedPathToNode, | ||||
|             _modifiedAst, | ||||
|             sketchDetails.zAxis, | ||||
|             sketchDetails.yAxis, | ||||
|             sketchDetails.origin | ||||
|           ) | ||||
|           return { | ||||
|             selectionType: 'completeSelection', | ||||
|             selection: pathMapToSelections( | ||||
|               kclManager.ast, | ||||
|               _modifiedAst, | ||||
|               selectionRanges, | ||||
|               pathToNodeMap | ||||
|             ), | ||||
|             updatedPathToNode, | ||||
|           } | ||||
|         }, | ||||
|         'Get length info': async ({ | ||||
| @ -589,13 +614,26 @@ export const ModelingMachineProvider = ({ | ||||
|         }, | ||||
|         'Get ABS X info': async ({ | ||||
|           selectionRanges, | ||||
|           sketchDetails, | ||||
|         }): Promise<SetSelections> => { | ||||
|           const { modifiedAst, pathToNodeMap } = | ||||
|             await applyConstraintAbsDistance({ | ||||
|               constraint: 'xAbs', | ||||
|               selectionRanges, | ||||
|             }) | ||||
|           await kclManager.updateAst(modifiedAst, true) | ||||
|           const _modifiedAst = parse(recast(modifiedAst)) | ||||
|           if (!sketchDetails) throw new Error('No sketch details') | ||||
|           const updatedPathToNode = updatePathToNodeFromMap( | ||||
|             sketchDetails.sketchPathToNode, | ||||
|             pathToNodeMap | ||||
|           ) | ||||
|           await sceneEntitiesManager.updateAstAndRejigSketch( | ||||
|             updatedPathToNode, | ||||
|             _modifiedAst, | ||||
|             sketchDetails.zAxis, | ||||
|             sketchDetails.yAxis, | ||||
|             sketchDetails.origin | ||||
|           ) | ||||
|           return { | ||||
|             selectionType: 'completeSelection', | ||||
|             selection: pathMapToSelections( | ||||
| @ -603,17 +641,31 @@ export const ModelingMachineProvider = ({ | ||||
|               selectionRanges, | ||||
|               pathToNodeMap | ||||
|             ), | ||||
|             updatedPathToNode, | ||||
|           } | ||||
|         }, | ||||
|         'Get ABS Y info': async ({ | ||||
|           selectionRanges, | ||||
|           sketchDetails, | ||||
|         }): Promise<SetSelections> => { | ||||
|           const { modifiedAst, pathToNodeMap } = | ||||
|             await applyConstraintAbsDistance({ | ||||
|               constraint: 'yAbs', | ||||
|               selectionRanges, | ||||
|             }) | ||||
|           await kclManager.updateAst(modifiedAst, true) | ||||
|           const _modifiedAst = parse(recast(modifiedAst)) | ||||
|           if (!sketchDetails) throw new Error('No sketch details') | ||||
|           const updatedPathToNode = updatePathToNodeFromMap( | ||||
|             sketchDetails.sketchPathToNode, | ||||
|             pathToNodeMap | ||||
|           ) | ||||
|           await sceneEntitiesManager.updateAstAndRejigSketch( | ||||
|             updatedPathToNode, | ||||
|             _modifiedAst, | ||||
|             sketchDetails.zAxis, | ||||
|             sketchDetails.yAxis, | ||||
|             sketchDetails.origin | ||||
|           ) | ||||
|           return { | ||||
|             selectionType: 'completeSelection', | ||||
|             selection: pathMapToSelections( | ||||
| @ -621,6 +673,7 @@ export const ModelingMachineProvider = ({ | ||||
|               selectionRanges, | ||||
|               pathToNodeMap | ||||
|             ), | ||||
|             updatedPathToNode, | ||||
|           } | ||||
|         }, | ||||
|         'Get convert to variable info': async ({ sketchDetails }, { data }) => { | ||||
|  | ||||
| @ -31,43 +31,6 @@ const projectWellFormed = { | ||||
| } satisfies Project | ||||
|  | ||||
| describe('ProjectSidebarMenu tests', () => { | ||||
|   test('Renders the project name', () => { | ||||
|     render( | ||||
|       <BrowserRouter> | ||||
|         <CommandBarProvider> | ||||
|           <SettingsAuthProviderJest> | ||||
|             <ProjectSidebarMenu project={projectWellFormed} enableMenu={true} /> | ||||
|           </SettingsAuthProviderJest> | ||||
|         </CommandBarProvider> | ||||
|       </BrowserRouter> | ||||
|     ) | ||||
|  | ||||
|     fireEvent.click(screen.getByTestId('project-sidebar-toggle')) | ||||
|  | ||||
|     expect(screen.getByTestId('projectName')).toHaveTextContent( | ||||
|       projectWellFormed.name | ||||
|     ) | ||||
|     expect(screen.getByTestId('createdAt')).toHaveTextContent( | ||||
|       `Created ${now.toLocaleDateString()}` | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   test('Renders app name if given no project', () => { | ||||
|     render( | ||||
|       <BrowserRouter> | ||||
|         <CommandBarProvider> | ||||
|           <SettingsAuthProviderJest> | ||||
|             <ProjectSidebarMenu enableMenu={true} /> | ||||
|           </SettingsAuthProviderJest> | ||||
|         </CommandBarProvider> | ||||
|       </BrowserRouter> | ||||
|     ) | ||||
|  | ||||
|     fireEvent.click(screen.getByTestId('project-sidebar-toggle')) | ||||
|  | ||||
|     expect(screen.getByTestId('projectName')).toHaveTextContent(APP_NAME) | ||||
|   }) | ||||
|  | ||||
|   test('Disables popover menu by default', () => { | ||||
|     render( | ||||
|       <BrowserRouter> | ||||
|  | ||||
| @ -138,41 +138,7 @@ function ProjectMenuPopover({ | ||||
|         > | ||||
|           {({ close }) => ( | ||||
|             <> | ||||
|               <div className="flex items-center gap-4 px-4 py-3"> | ||||
|                 <div> | ||||
|                   <p className="m-0 text-mono" data-testid="projectName"> | ||||
|                     {project?.name ? project.name : APP_NAME} | ||||
|                   </p> | ||||
|                   {project?.metadata && project.metadata.created && ( | ||||
|                     <p | ||||
|                       className="m-0 text-xs text-chalkboard-80 dark:text-chalkboard-40" | ||||
|                       data-testid="createdAt" | ||||
|                     > | ||||
|                       Created{' '} | ||||
|                       {new Date(project.metadata.created).toLocaleDateString()} | ||||
|                     </p> | ||||
|                   )} | ||||
|                 </div> | ||||
|               </div> | ||||
|               {isTauri() ? ( | ||||
|                 <FileTree | ||||
|                   file={file} | ||||
|                   className="overflow-hidden border-0 border-y border-chalkboard-30 dark:border-chalkboard-80" | ||||
|                   onNavigateToFile={close} | ||||
|                 /> | ||||
|               ) : ( | ||||
|                 <div className="flex-1 p-4 text-sm overflow-hidden"> | ||||
|                   <p> | ||||
|                     In the browser version of Modeling App you can only have one | ||||
|                     part, and the code is stored in your browser's storage. | ||||
|                   </p> | ||||
|                   <p className="my-6"> | ||||
|                     Please save any code you want to keep more permanently, as | ||||
|                     your browser's storage is not guaranteed to be permanent. | ||||
|                   </p> | ||||
|                 </div> | ||||
|               )} | ||||
|               <div className="flex flex-col gap-2 p-4 dark:bg-chalkboard-90"> | ||||
|               <div className="flex flex-col gap-2 p-4"> | ||||
|                 <ActionButton | ||||
|                   Element="button" | ||||
|                   iconStart={{ icon: 'exportFile', className: 'p-1' }} | ||||
|  | ||||
| @ -120,6 +120,10 @@ export async function applyConstraintAbsDistance({ | ||||
|       createVariableDeclaration(variableName, valueNode) | ||||
|     ) | ||||
|     _modifiedAst.body = newBody | ||||
|     Object.values(pathToNodeMap).forEach((pathToNode) => { | ||||
|       const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 | ||||
|       pathToNode[index][0] = Number(pathToNode[index][0]) + 1 | ||||
|     }) | ||||
|   } | ||||
|   return { modifiedAst: _modifiedAst, pathToNodeMap } | ||||
| } | ||||
|  | ||||
| @ -98,7 +98,11 @@ export async function applyConstraintAngleBetween({ | ||||
|     value: valueUsedInTransform, | ||||
|     initialVariableName: 'angle', | ||||
|   } as any) | ||||
|   if (segName === tagInfo?.tag && Number(value) === valueUsedInTransform) { | ||||
|   if ( | ||||
|     segName === tagInfo?.tag && | ||||
|     Number(value) === valueUsedInTransform && | ||||
|     !variableName | ||||
|   ) { | ||||
|     return { | ||||
|       modifiedAst, | ||||
|       pathToNodeMap, | ||||
| @ -128,6 +132,10 @@ export async function applyConstraintAngleBetween({ | ||||
|       createVariableDeclaration(variableName, valueNode) | ||||
|     ) | ||||
|     _modifiedAst.body = newBody | ||||
|     Object.values(_pathToNodeMap).forEach((pathToNode) => { | ||||
|       const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 | ||||
|       pathToNode[index][0] = Number(pathToNode[index][0]) + 1 | ||||
|     }) | ||||
|   } | ||||
|   return { | ||||
|     modifiedAst: _modifiedAst, | ||||
|  | ||||
| @ -138,6 +138,10 @@ export async function applyConstraintAngleLength({ | ||||
|         createVariableDeclaration(variableName, valueNode) | ||||
|       ) | ||||
|       _modifiedAst.body = newBody | ||||
|       Object.values(pathToNodeMap).forEach((pathToNode) => { | ||||
|         const index = pathToNode.findIndex((a) => a[0] === 'body') + 1 | ||||
|         pathToNode[index][0] = Number(pathToNode[index][0]) + 1 | ||||
|       }) | ||||
|     } | ||||
|     return { | ||||
|       modifiedAst: _modifiedAst, | ||||
|  | ||||
| @ -341,36 +341,29 @@ const setAbsDistanceCreateNode = | ||||
|     isXOrYLine = false, | ||||
|     index = xOrY === 'x' ? 0 : 1 | ||||
|   ): TransformInfo['createNode'] => | ||||
|   ({ tag, forceValueUsedInTransform }) => { | ||||
|     return (args, _, referencedSegment) => { | ||||
|       const valueUsedInTransform = roundOff( | ||||
|         getArgLiteralVal(args?.[index]) - (referencedSegment?.to?.[index] || 0), | ||||
|         2 | ||||
|       ) | ||||
|       const val = | ||||
|         (forceValueUsedInTransform as BinaryPart) || | ||||
|         createLiteral(valueUsedInTransform) | ||||
|       if (isXOrYLine) { | ||||
|         return createCallWrapper( | ||||
|           xOrY === 'x' ? 'xLineTo' : 'yLineTo', | ||||
|           val, | ||||
|           tag, | ||||
|           valueUsedInTransform | ||||
|         ) | ||||
|       } | ||||
|   ({ tag, forceValueUsedInTransform }) => | ||||
|   (args) => { | ||||
|     const valueUsedInTransform = roundOff(getArgLiteralVal(args?.[index]), 2) | ||||
|     const val = | ||||
|       (forceValueUsedInTransform as BinaryPart) || | ||||
|       createLiteral(valueUsedInTransform) | ||||
|     if (isXOrYLine) { | ||||
|       return createCallWrapper( | ||||
|         'lineTo', | ||||
|         !index ? [val, args[1]] : [args[0], val], | ||||
|         xOrY === 'x' ? 'xLineTo' : 'yLineTo', | ||||
|         val, | ||||
|         tag, | ||||
|         valueUsedInTransform | ||||
|       ) | ||||
|     } | ||||
|     return createCallWrapper( | ||||
|       'lineTo', | ||||
|       !index ? [val, args[1]] : [args[0], val], | ||||
|       tag, | ||||
|       valueUsedInTransform | ||||
|     ) | ||||
|   } | ||||
| const setAbsDistanceForAngleLineCreateNode = | ||||
|   ( | ||||
|     xOrY: 'x' | 'y', | ||||
|     index = xOrY === 'x' ? 0 : 1 | ||||
|   ): TransformInfo['createNode'] => | ||||
|   (xOrY: 'x' | 'y'): TransformInfo['createNode'] => | ||||
|   ({ tag, forceValueUsedInTransform, varValA }) => { | ||||
|     return (args) => { | ||||
|       const valueUsedInTransform = roundOff(getArgLiteralVal(args?.[1]), 2) | ||||
|  | ||||
| @ -26,6 +26,22 @@ export function pathMapToSelections( | ||||
|   return newSelections | ||||
| } | ||||
|  | ||||
| export function updatePathToNodeFromMap( | ||||
|   oldPath: PathToNode, | ||||
|   pathToNodeMap: { [key: number]: PathToNode } | ||||
| ): PathToNode { | ||||
|   const updatedPathToNode = JSON.parse(JSON.stringify(oldPath)) | ||||
|   let max = 0 | ||||
|   Object.values(pathToNodeMap).forEach((path) => { | ||||
|     const index = Number(path[1][0]) | ||||
|     if (index > max) { | ||||
|       max = index | ||||
|     } | ||||
|   }) | ||||
|   updatedPathToNode[1][0] = max | ||||
|   return updatedPathToNode | ||||
| } | ||||
|  | ||||
| export function isCursorInSketchCommandRange( | ||||
|   artifactMap: ArtifactMap, | ||||
|   selectionRanges: Selections | ||||
|  | ||||
| @ -64,6 +64,7 @@ export type SetSelections = | ||||
|   | { | ||||
|       selectionType: 'completeSelection' | ||||
|       selection: Selections | ||||
|       updatedPathToNode?: PathToNode | ||||
|     } | ||||
|   | { | ||||
|       selectionType: 'mirrorCodeMirrorSelections' | ||||
|  | ||||
| @ -504,51 +504,45 @@ impl SketchGroup { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn get_coords_from_paths(&self) -> Result<Point2d, KclError> { | ||||
|         if self.value.is_empty() { | ||||
|             return Ok(self.start.to.into()); | ||||
|         } | ||||
|     /// Get the path most recently sketched. | ||||
|     pub fn latest_path(&self) -> Option<&Path> { | ||||
|         self.value.last() | ||||
|     } | ||||
|  | ||||
|         let index = self.value.len() - 1; | ||||
|         if let Some(path) = self.value.get(index) { | ||||
|             let base = path.get_base(); | ||||
|             Ok(base.to.into()) | ||||
|         } else { | ||||
|             Ok(self.start.to.into()) | ||||
|         } | ||||
|     /// The "pen" is an imaginary pen drawing the path. | ||||
|     /// This gets the current point the pen is hovering over, i.e. the point | ||||
|     /// where the last path segment ends, and the next path segment will begin. | ||||
|     pub fn current_pen_position(&self) -> Result<Point2d, KclError> { | ||||
|         let Some(path) = self.latest_path() else { | ||||
|             return Ok(self.start.to.into()); | ||||
|         }; | ||||
|  | ||||
|         let base = path.get_base(); | ||||
|         Ok(base.to.into()) | ||||
|     } | ||||
|  | ||||
|     pub fn get_tangential_info_from_paths(&self) -> GetTangentialInfoFromPathsResult { | ||||
|         if self.value.is_empty() { | ||||
|         let Some(path) = self.latest_path() else { | ||||
|             return GetTangentialInfoFromPathsResult { | ||||
|                 center_or_tangent_point: self.start.to, | ||||
|                 is_center: false, | ||||
|                 ccw: false, | ||||
|             }; | ||||
|         } | ||||
|         let index = self.value.len() - 1; | ||||
|         if let Some(path) = self.value.get(index) { | ||||
|             match path { | ||||
|                 Path::TangentialArcTo { center, ccw, .. } => GetTangentialInfoFromPathsResult { | ||||
|                     center_or_tangent_point: *center, | ||||
|                     is_center: true, | ||||
|                     ccw: *ccw, | ||||
|                 }, | ||||
|                 _ => { | ||||
|                     let base = path.get_base(); | ||||
|                     GetTangentialInfoFromPathsResult { | ||||
|                         center_or_tangent_point: base.from, | ||||
|                         is_center: false, | ||||
|                         ccw: false, | ||||
|                     } | ||||
|         }; | ||||
|         match path { | ||||
|             Path::TangentialArcTo { center, ccw, .. } => GetTangentialInfoFromPathsResult { | ||||
|                 center_or_tangent_point: *center, | ||||
|                 is_center: true, | ||||
|                 ccw: *ccw, | ||||
|             }, | ||||
|             _ => { | ||||
|                 let base = path.get_base(); | ||||
|                 GetTangentialInfoFromPathsResult { | ||||
|                     center_or_tangent_point: base.from, | ||||
|                     is_center: false, | ||||
|                     ccw: false, | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             GetTangentialInfoFromPathsResult { | ||||
|                 center_or_tangent_point: self.start.to, | ||||
|                 is_center: false, | ||||
|                 ccw: false, | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -53,7 +53,7 @@ async fn inner_line_to( | ||||
|     tag: Option<String>, | ||||
|     args: Args, | ||||
| ) -> Result<Box<SketchGroup>, KclError> { | ||||
|     let from = sketch_group.get_coords_from_paths()?; | ||||
|     let from = sketch_group.current_pen_position()?; | ||||
|     let id = uuid::Uuid::new_v4(); | ||||
|  | ||||
|     args.send_modeling_cmd( | ||||
| @ -128,7 +128,7 @@ async fn inner_x_line_to( | ||||
|     tag: Option<String>, | ||||
|     args: Args, | ||||
| ) -> Result<Box<SketchGroup>, KclError> { | ||||
|     let from = sketch_group.get_coords_from_paths()?; | ||||
|     let from = sketch_group.current_pen_position()?; | ||||
|  | ||||
|     let new_sketch_group = inner_line_to([to, from.y], sketch_group, tag, args).await?; | ||||
|  | ||||
| @ -166,7 +166,7 @@ async fn inner_y_line_to( | ||||
|     tag: Option<String>, | ||||
|     args: Args, | ||||
| ) -> Result<Box<SketchGroup>, KclError> { | ||||
|     let from = sketch_group.get_coords_from_paths()?; | ||||
|     let from = sketch_group.current_pen_position()?; | ||||
|  | ||||
|     let new_sketch_group = inner_line_to([from.x, to], sketch_group, tag, args).await?; | ||||
|     Ok(new_sketch_group) | ||||
| @ -213,7 +213,7 @@ async fn inner_line( | ||||
|     tag: Option<String>, | ||||
|     args: Args, | ||||
| ) -> Result<Box<SketchGroup>, KclError> { | ||||
|     let from = sketch_group.get_coords_from_paths()?; | ||||
|     let from = sketch_group.current_pen_position()?; | ||||
|     let to = [from.x + delta[0], from.y + delta[1]]; | ||||
|  | ||||
|     let id = uuid::Uuid::new_v4(); | ||||
| @ -381,7 +381,7 @@ async fn inner_angled_line( | ||||
|     tag: Option<String>, | ||||
|     args: Args, | ||||
| ) -> Result<Box<SketchGroup>, KclError> { | ||||
|     let from = sketch_group.get_coords_from_paths()?; | ||||
|     let from = sketch_group.current_pen_position()?; | ||||
|     let (angle, length) = match data { | ||||
|         AngledLineData::AngleAndLengthNamed { angle, length } => (angle, length), | ||||
|         AngledLineData::AngleAndLengthPair(pair) => (pair[0], pair[1]), | ||||
| @ -514,7 +514,7 @@ async fn inner_angled_line_to_x( | ||||
|     tag: Option<String>, | ||||
|     args: Args, | ||||
| ) -> Result<Box<SketchGroup>, KclError> { | ||||
|     let from = sketch_group.get_coords_from_paths()?; | ||||
|     let from = sketch_group.current_pen_position()?; | ||||
|     let AngledLineToData { angle, to: x_to } = data; | ||||
|  | ||||
|     let x_component = x_to - from.x; | ||||
| @ -600,7 +600,7 @@ async fn inner_angled_line_to_y( | ||||
|     tag: Option<String>, | ||||
|     args: Args, | ||||
| ) -> Result<Box<SketchGroup>, KclError> { | ||||
|     let from = sketch_group.get_coords_from_paths()?; | ||||
|     let from = sketch_group.current_pen_position()?; | ||||
|     let AngledLineToData { angle, to: y_to } = data; | ||||
|  | ||||
|     let y_component = y_to - from.y; | ||||
| @ -672,7 +672,7 @@ async fn inner_angled_line_that_intersects( | ||||
|         })? | ||||
|         .get_base(); | ||||
|  | ||||
|     let from = sketch_group.get_coords_from_paths()?; | ||||
|     let from = sketch_group.current_pen_position()?; | ||||
|     let to = intersection_with_parallel_line( | ||||
|         &[intersect_path.from.into(), intersect_path.to.into()], | ||||
|         data.offset.unwrap_or_default(), | ||||
| @ -1355,7 +1355,7 @@ pub(crate) async fn inner_close( | ||||
|     tag: Option<String>, | ||||
|     args: Args, | ||||
| ) -> Result<Box<SketchGroup>, KclError> { | ||||
|     let from = sketch_group.get_coords_from_paths()?; | ||||
|     let from = sketch_group.current_pen_position()?; | ||||
|     let to: Point2d = sketch_group.start.from.into(); | ||||
|  | ||||
|     let id = uuid::Uuid::new_v4(); | ||||
| @ -1449,7 +1449,7 @@ pub(crate) async fn inner_arc( | ||||
|     tag: Option<String>, | ||||
|     args: Args, | ||||
| ) -> Result<Box<SketchGroup>, KclError> { | ||||
|     let from: Point2d = sketch_group.get_coords_from_paths()?; | ||||
|     let from: Point2d = sketch_group.current_pen_position()?; | ||||
|  | ||||
|     let (center, angle_start, angle_end, radius, end) = match &data { | ||||
|         ArcData::AnglesAndRadius { | ||||
| @ -1555,7 +1555,7 @@ async fn inner_tangential_arc( | ||||
|     tag: Option<String>, | ||||
|     args: Args, | ||||
| ) -> Result<Box<SketchGroup>, KclError> { | ||||
|     let from: Point2d = sketch_group.get_coords_from_paths()?; | ||||
|     let from: Point2d = sketch_group.current_pen_position()?; | ||||
|  | ||||
|     let id = uuid::Uuid::new_v4(); | ||||
|  | ||||
| @ -1676,7 +1676,7 @@ async fn inner_tangential_arc_to( | ||||
|     tag: Option<String>, | ||||
|     args: Args, | ||||
| ) -> Result<Box<SketchGroup>, KclError> { | ||||
|     let from: Point2d = sketch_group.get_coords_from_paths()?; | ||||
|     let from: Point2d = sketch_group.current_pen_position()?; | ||||
|     let tangent_info = sketch_group.get_tangential_info_from_paths(); | ||||
|     let tan_previous_point = if tangent_info.is_center { | ||||
|         get_tangent_point_from_previous_arc(tangent_info.center_or_tangent_point, tangent_info.ccw, from.into()) | ||||
| @ -1762,7 +1762,7 @@ async fn inner_bezier_curve( | ||||
|     tag: Option<String>, | ||||
|     args: Args, | ||||
| ) -> Result<Box<SketchGroup>, KclError> { | ||||
|     let from = sketch_group.get_coords_from_paths()?; | ||||
|     let from = sketch_group.current_pen_position()?; | ||||
|  | ||||
|     let relative = true; | ||||
|     let delta = data.to; | ||||
|  | ||||
		Reference in New Issue
	
	Block a user