diff --git a/e2e/playwright/import-ui.spec.ts b/e2e/playwright/import-ui.spec.ts new file mode 100644 index 00000000000..47f55e4252e --- /dev/null +++ b/e2e/playwright/import-ui.spec.ts @@ -0,0 +1,120 @@ +import { expect, test } from '@e2e/playwright/zoo-test' +import * as fsp from 'fs/promises' +import path from 'path' + +test.describe('Import UI tests', () => { + test('shows toast when trying to sketch on imported face', async ({ + context, + page, + homePage, + toolbar, + scene, + editor, + }) => { + await context.folderSetupFn(async (dir) => { + const projectDir = path.join(dir, 'import-test') + await fsp.mkdir(projectDir, { recursive: true }) + + // Create the imported file + await fsp.writeFile( + path.join(projectDir, 'toBeImported.kcl'), + `sketch001 = startSketchOn(XZ) +profile001 = startProfileAt([281.54, 305.81], sketch001) + |> angledLine([0, 123.43], %, $rectangleSegmentA001) + |> angledLine([ + segAng(rectangleSegmentA001) - 90, + 85.99 + ], %) + |> angledLine([ + segAng(rectangleSegmentA001), + -segLen(rectangleSegmentA001) + ], %) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude(profile001, length = 100)` + ) + + // Create the main file that imports + await fsp.writeFile( + path.join(projectDir, 'main.kcl'), + `import "toBeImported.kcl" as importedCube + +importedCube + +sketch001 = startSketchOn(XZ) +profile001 = startProfileAt([-134.53, -56.17], sketch001) + |> angledLine([0, 79.05], %, $rectangleSegmentA001) + |> angledLine([ + segAng(rectangleSegmentA001) - 90, + 76.28 + ], %) + |> angledLine([ + segAng(rectangleSegmentA001), + -segLen(rectangleSegmentA001) + ], %, $seg01) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02) + |> close() +extrude001 = extrude(profile001, length = 100) +sketch003 = startSketchOn(extrude001, seg02) +sketch002 = startSketchOn(extrude001, seg01)` + ) + }) + + await homePage.openProject('import-test') + await scene.waitForExecutionDone() + + await scene.moveCameraTo( + { + x: -114, + y: -897, + z: 475, + }, + { + x: -114, + y: -51, + z: 83, + } + ) + const [_, hoverOverNonImport] = scene.makeMouseHelpers(611, 364) + const [importedFaceClick, hoverOverImported] = scene.makeMouseHelpers( + 940, + 150 + ) + + await test.step('check code highlight works for code define in the file being edited', async () => { + await hoverOverNonImport() + await editor.expectState({ + highlightedCode: 'startProfileAt([-134.53,-56.17],sketch001)', + diagnostics: [], + activeLines: ['import"toBeImported.kcl"asimportedCube'], + }) + }) + + await test.step('check code does nothing when geometry is defined in an import', async () => { + await hoverOverImported() + await editor.expectState({ + highlightedCode: '', + diagnostics: [], + activeLines: ['import"toBeImported.kcl"asimportedCube'], + }) + }) + + await test.step('check the user is warned when sketching on a imported face', async () => { + // Start sketch mode + await toolbar.startSketchPlaneSelection() + + // Click on a face from the imported model + // await new Promise(() => {}) + await importedFaceClick() + + // Verify toast appears with correct content + await expect(page.getByText('This face is from an import')).toBeVisible() + await expect( + page.locator('.font-mono').getByText('toBeImported.kcl') + ).toBeVisible() + await expect( + page.getByText('Please select this from the files pane to edit') + ).toBeVisible() + }) + }) +}) diff --git a/src/components/SketchOnImportToast.tsx b/src/components/SketchOnImportToast.tsx new file mode 100644 index 00000000000..fd51dc0e1d7 --- /dev/null +++ b/src/components/SketchOnImportToast.tsx @@ -0,0 +1,21 @@ +import toast from 'react-hot-toast' + +interface SketchOnImportToastProps { + fileName: string +} + +export function SketchOnImportToast({ fileName }: SketchOnImportToastProps) { + return ( +
+ This face is from an import + + {fileName} + + Please select this from the files pane to edit +
+ ) +} + +export function showSketchOnImportToast(fileName: string) { + toast.error() +} diff --git a/src/editor/manager.ts b/src/editor/manager.ts index 0991b4e53d1..14e6400a97a 100644 --- a/src/editor/manager.ts +++ b/src/editor/manager.ts @@ -13,6 +13,7 @@ import { } from '@src/editor/highlightextension' import type { KclManager } from '@src/lang/KclSingleton' import type { EngineCommandManager } from '@src/lang/std/engineConnection' +import { isTopLevelModule } from '@src/lang/util' import { markOnce } from '@src/lib/performance' import type { Selection, Selections } from '@src/lib/selections' import { processCodeMirrorRanges } from '@src/lib/selections' @@ -151,12 +152,12 @@ export default class EditorManager { selection: Array ): Array<[number, number]> { if (!this._editorView) { - return selection.map((s): [number, number] => { + return selection.filter(isTopLevelModule).map((s): [number, number] => { return [s[0], s[1]] }) } - return selection.map((s): [number, number] => { + return selection.filter(isTopLevelModule).map((s): [number, number] => { const safeEnd = Math.min(s[1], this._editorView?.state.doc.length || s[1]) return [s[0], safeEnd] }) diff --git a/src/hooks/useEngineConnectionSubscriptions.ts b/src/hooks/useEngineConnectionSubscriptions.ts index ee11b649143..d4aeeb35e5e 100644 --- a/src/hooks/useEngineConnectionSubscriptions.ts +++ b/src/hooks/useEngineConnectionSubscriptions.ts @@ -1,5 +1,6 @@ import { useEffect, useRef } from 'react' +import { showSketchOnImportToast } from '@src/components/SketchOnImportToast' import { useModelingContext } from '@src/hooks/useModelingContext' import { getNodeFromPath } from '@src/lang/queryAst' import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' @@ -24,10 +25,12 @@ import { sceneInfra, } from '@src/lib/singletons' import { err, reportRejection } from '@src/lib/trap' +import { getModuleId } from '@src/lib/utils' import type { EdgeCutInfo, ExtrudeFacePlane, } from '@src/machines/modelingMachine' +import toast from 'react-hot-toast' export function useEngineConnectionSubscriptions() { const { send, context, state } = useModelingContext() @@ -186,6 +189,29 @@ export function useEngineConnectionSubscriptions() { faceId, kclManager.artifactGraph ) + if (!err(extrusion)) { + const fileIndex = getModuleId(extrusion.codeRef.range) + if (fileIndex !== 0) { + const importDetails = + kclManager.execState.filenames[fileIndex] + if (!importDetails) { + toast.error("can't sketch on this face") + return + } + if (importDetails?.type === 'Local') { + const paths = importDetails.value.split('/') + const fileName = paths[paths.length - 1] + showSketchOnImportToast(fileName) + } else if ( + importDetails?.type === 'Main' || + importDetails?.type === 'Std' + ) { + toast.error("can't sketch on this face") + } else { + const _exhaustiveCheck: never = importDetails + } + } + } if ( artifact?.type !== 'cap' && diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 30ff5576a50..30dec53865b 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -469,3 +469,7 @@ export function binaryToUuid( hexValues.slice(10, 16).join(''), ].join('-') } + +export function getModuleId(sourceRange: SourceRange) { + return sourceRange[2] +}