Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions e2e/playwright/import-ui.spec.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
})
21 changes: 21 additions & 0 deletions src/components/SketchOnImportToast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import toast from 'react-hot-toast'

interface SketchOnImportToastProps {
fileName: string
}

export function SketchOnImportToast({ fileName }: SketchOnImportToastProps) {
return (
<div className="flex flex-col gap-2">
<span>This face is from an import</span>
<span className="font-mono text-sm bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded">
{fileName}
</span>
<span>Please select this from the files pane to edit</span>
</div>
)
}

export function showSketchOnImportToast(fileName: string) {
toast.error(<SketchOnImportToast fileName={fileName} />)
}
5 changes: 3 additions & 2 deletions src/editor/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -151,12 +152,12 @@ export default class EditorManager {
selection: Array<Selection['codeRef']['range']>
): 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]
})
Expand Down
26 changes: 26 additions & 0 deletions src/hooks/useEngineConnectionSubscriptions.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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()
Expand Down Expand Up @@ -186,6 +189,29 @@ export function useEngineConnectionSubscriptions() {
faceId,
kclManager.artifactGraph
)
if (!err(extrusion)) {
const fileIndex = getModuleId(extrusion.codeRef.range)
if (fileIndex !== 0) {
Comment on lines +193 to +194
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One nitpick. We can use isTopLevelModule() here too.

Suggested change
const fileIndex = getModuleId(extrusion.codeRef.range)
if (fileIndex !== 0) {
if (!isTopLevelModule(extrusion.codeRef.range)) {
const fileIndex = getModuleId(extrusion.codeRef.range)

const importDetails =
kclManager.execState.filenames[fileIndex]
if (!importDetails) {
toast.error("can't sketch on this face")
return
}
if (importDetails?.type === 'Local') {
Copy link
Contributor Author

@Irev-Dev Irev-Dev Apr 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to check with the kcl oracles if I should checking anything else here, there's main and std but main is the happy path, and we std is not ever geometry right?

there might one day be url or similar, but can probably update at the time?

Copy link
Contributor

@jtran jtran Apr 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct. I don't think anything restricts std from having geometry, but we don't currently.

If you'd like the addition of a new case to fail tsc, you can make an exhaustive check like this:

const _exhaustiveCheck: never = op.group

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

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' &&
Expand Down
4 changes: 4 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,3 +469,7 @@ export function binaryToUuid(
hexValues.slice(10, 16).join(''),
].join('-')
}

export function getModuleId(sourceRange: SourceRange) {
return sourceRange[2]
}
Loading