diff --git a/e2e/playwright/testing-samples-loading.spec.ts b/e2e/playwright/testing-samples-loading.spec.ts index 4fa18f879..a3a01d290 100644 --- a/e2e/playwright/testing-samples-loading.spec.ts +++ b/e2e/playwright/testing-samples-loading.spec.ts @@ -103,6 +103,8 @@ test.describe('Testing loading external models', () => { file: 'ball-bearing' + FILE_EXT, title: 'Ball Bearing', file1: 'ball-bearing-1' + FILE_EXT, + folderName: 'ball-bearing', + folderName1: 'ball-bearing-1', } const projectCard = page.getByRole('link', { name: 'bracket' }) const overwriteWarning = page.getByText( @@ -154,8 +156,10 @@ test.describe('Testing loading external models', () => { await test.step(`Ensure we made and opened a new file`, async () => { await editor.expectEditor.toContain('// ' + sampleOne.title) - await expect(newlyCreatedFile(sampleOne.file)).toBeVisible() - await expect(projectMenuButton).toContainText(sampleOne.file) + await expect( + page.getByTestId('file-tree-item').getByText(sampleOne.folderName) + ).toBeVisible() + await expect(projectMenuButton).toContainText('main.kcl') }) await test.step(`Load a KCL sample with the command palette`, async () => { @@ -169,8 +173,10 @@ test.describe('Testing loading external models', () => { await test.step(`Ensure we made and opened a new file with a unique name`, async () => { await editor.expectEditor.toContain('// ' + sampleOne.title) - await expect(newlyCreatedFile(sampleOne.file1)).toBeVisible() - await expect(projectMenuButton).toContainText(sampleOne.file1) + await expect( + page.getByTestId('file-tree-item').getByText(sampleOne.folderName1) + ).toBeVisible() + await expect(projectMenuButton).toContainText('main.kcl') }) } ) diff --git a/e2e/playwright/text-to-cad-tests.spec.ts b/e2e/playwright/text-to-cad-tests.spec.ts index f3576c16a..78a090797 100644 --- a/e2e/playwright/text-to-cad-tests.spec.ts +++ b/e2e/playwright/text-to-cad-tests.spec.ts @@ -984,12 +984,12 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => { ) await expect(page.getByTestId('app-header-file-name')).toBeVisible() await expect(page.getByTestId('app-header-file-name')).toContainText( - '2x2x2-cube.kcl' + 'main.kcl' ) await u.openFilePanel() await expect( - page.getByTestId('file-tree-item').getByText('2x2x2-cube.kcl') + page.getByTestId('file-tree-item').getByText('2x2x2-cube') ).toBeVisible() } ) @@ -1184,13 +1184,13 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => { ) await expect(page.getByTestId('app-header-file-name')).toBeVisible() await expect(page.getByTestId('app-header-file-name')).toContainText( - '2x2x2-cube.kcl' + 'main.kcl' ) // Check file is created await u.openFilePanel() await expect( - page.getByTestId('file-tree-item').getByText('2x2x2-cube.kcl') + page.getByTestId('file-tree-item').getByText('2x2x2-cube') ).toBeVisible() } ) @@ -1476,13 +1476,13 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => { ) await expect(page.getByTestId('app-header-file-name')).toBeVisible() await expect(page.getByTestId('app-header-file-name')).toContainText( - '2x2x2-cube.kcl' + 'main.kcl' ) // Check file is created await u.openFilePanel() await expect( - page.getByTestId('file-tree-item').getByText('2x2x2-cube.kcl') + page.getByTestId('file-tree-item').getByText('2x2x2-cube') ).toBeVisible() await expect( page.getByTestId('file-tree-item').getByText('main.kcl') diff --git a/src/components/ToastTextToCad.tsx b/src/components/ToastTextToCad.tsx index 6dbc1c9c9..4086b1b9e 100644 --- a/src/components/ToastTextToCad.tsx +++ b/src/components/ToastTextToCad.tsx @@ -159,6 +159,7 @@ export function ToastTextToCadSuccess({ projectName, fileName, isProjectNew, + rootProjectName, }: { toastId: string data: TextToCad_type & { fileName: string } @@ -170,6 +171,7 @@ export function ToastTextToCadSuccess({ projectName: string fileName: string isProjectNew: boolean + rootProjectName: string }) { const wrapperRef = useRef(null) const canvasRef = useRef(null) @@ -361,26 +363,23 @@ export function ToastTextToCadSuccess({ if (isDesktop()) { // Delete the file from the project - if (projectName && fileName) { - // You are in the new workflow for text to cad at the global application level - if (isProjectNew) { - // Delete the entire project if it was newly created from text to CAD - systemIOActor.send({ - type: SystemIOMachineEvents.deleteProject, - data: { - requestedProjectName: projectName, - }, - }) - } else { - // Only delete the file if the project was preexisting - systemIOActor.send({ - type: SystemIOMachineEvents.deleteKCLFile, - data: { - requestedProjectName: projectName, - requestedFileName: fileName, - }, - }) - } + if (isProjectNew) { + // Delete the entire project if it was newly created from text to CAD + systemIOActor.send({ + type: SystemIOMachineEvents.deleteProject, + data: { + requestedProjectName: rootProjectName, + }, + }) + } else if (projectName && fileName) { + // deletes the folder when inside the modeling page + // The TTC Create will make a subdir, delete that dir with the main.kcl as well + systemIOActor.send({ + type: SystemIOMachineEvents.deleteProject, + data: { + requestedProjectName: projectName, + }, + }) } } diff --git a/src/lib/commandBarConfigs/applicationCommandConfig.ts b/src/lib/commandBarConfigs/applicationCommandConfig.ts index a84822fee..cc86b08ce 100644 --- a/src/lib/commandBarConfigs/applicationCommandConfig.ts +++ b/src/lib/commandBarConfigs/applicationCommandConfig.ts @@ -10,12 +10,16 @@ import { kclSamplesManifestWithNoMultipleFiles, } from '@src/lib/kclSamples' import { getUniqueProjectName } from '@src/lib/desktopFS' -import { IS_ML_EXPERIMENTAL, PROJECT_ENTRYPOINT } from '@src/lib/constants' +import { IS_ML_EXPERIMENTAL } from '@src/lib/constants' import toast from 'react-hot-toast' import { reportRejection } from '@src/lib/trap' import { relevantFileExtensions } from '@src/lang/wasmUtils' -import { getStringAfterLastSeparator, webSafePathSplit } from '@src/lib/paths' -import { FILE_EXT } from '@src/lib/constants' +import { + getStringAfterLastSeparator, + joinOSPaths, + webSafePathSplit, +} from '@src/lib/paths' +import { getAllSubDirectoriesAtProjectRoot } from '@src/machines/systemIO/snapshotContext' function onSubmitKCLSampleCreation({ sample, @@ -69,20 +73,32 @@ function onSubmitKCLSampleCreation({ }) } + /** + * When adding assemblies to an existing project create the assembly into a unique sub directory + */ + if (!isProjectNew) { + requestedFiles.forEach((requestedFile) => { + const subDirectoryName = projectPathPart + const firstLevelDirectories = getAllSubDirectoriesAtProjectRoot({ + projectFolderName: requestedFile.requestedProjectName, + }) + const uniqueSubDirectoryName = getUniqueProjectName( + subDirectoryName, + firstLevelDirectories + ) + requestedFile.requestedProjectName = joinOSPaths( + requestedFile.requestedProjectName, + uniqueSubDirectoryName + ) + }) + } + if (requestedFiles.length === 1) { - /** - * Navigates to the single file that could be renamed on disk for duplicates - */ - const folderNameBecomesKCLFileName = projectPathPart + FILE_EXT - // If the project is new create the single file as main.kcl - const requestedFileNameWithExtension = isProjectNew - ? PROJECT_ENTRYPOINT - : folderNameBecomesKCLFileName systemIOActor.send({ type: SystemIOMachineEvents.importFileFromURL, data: { requestedProjectName: requestedFiles[0].requestedProjectName, - requestedFileNameWithExtension: requestedFileNameWithExtension, + requestedFileNameWithExtension: requestedFiles[0].requestedFileName, requestedCode: requestedFiles[0].requestedCode, }, }) @@ -278,10 +294,9 @@ export function createApplicationCommands({ return value }, options: ({ argumentsToSubmit }) => { - const samples = - isDesktop() && argumentsToSubmit.method !== 'existingProject' - ? everyKclSample - : kclSamplesManifestWithNoMultipleFiles + const samples = isDesktop() + ? everyKclSample + : kclSamplesManifestWithNoMultipleFiles return samples.map((sample) => { return { value: sample.pathFromProjectDirectoryToFirstFile, @@ -296,17 +311,10 @@ export function createApplicationCommands({ skip: true, options: ({ argumentsToSubmit }, _) => { if (isDesktop() && typeof argumentsToSubmit.sample === 'string') { - const kclSample = findKclSample(argumentsToSubmit.sample) - if (kclSample && kclSample.files.length > 1) { - return [ - { name: 'New project', value: 'newProject', isCurrent: true }, - ] - } else { - return [ - { name: 'New project', value: 'newProject', isCurrent: true }, - { name: 'Existing project', value: 'existingProject' }, - ] - } + return [ + { name: 'New project', value: 'newProject', isCurrent: true }, + { name: 'Existing project', value: 'existingProject' }, + ] } else { return [{ name: 'Overwrite', value: 'existingProject' }] } diff --git a/src/lib/textToCad.ts b/src/lib/textToCad.ts index e813d29fb..6e3cba6c2 100644 --- a/src/lib/textToCad.ts +++ b/src/lib/textToCad.ts @@ -6,9 +6,9 @@ import { ToastTextToCadError, ToastTextToCadSuccess, } from '@src/components/ToastTextToCad' -import { FILE_EXT, PROJECT_ENTRYPOINT } from '@src/lib/constants' +import { PROJECT_ENTRYPOINT } from '@src/lib/constants' import crossPlatformFetch from '@src/lib/crossPlatformFetch' -import { getNextFileName } from '@src/lib/desktopFS' +import { getUniqueProjectName } from '@src/lib/desktopFS' import { isDesktop } from '@src/lib/isDesktop' import { kclManager, systemIOActor } from '@src/lib/singletons' import { @@ -17,6 +17,8 @@ import { } from '@src/machines/systemIO/utils' import { reportRejection } from '@src/lib/trap' import { toSync } from '@src/lib/utils' +import { getAllSubDirectoriesAtProjectRoot } from '@src/machines/systemIO/snapshotContext' +import { joinOSPaths } from '@src/lib/paths' export async function submitTextToCadPrompt( prompt: string, @@ -173,7 +175,8 @@ export async function submitAndAwaitTextToKclSystemIO({ } ) - let newFileName = '' + let newFileName = PROJECT_ENTRYPOINT + let uniqueProjectName = projectName const textToCadOutputCreated = await textToCadComplete .catch((e) => { @@ -197,30 +200,28 @@ export async function submitAndAwaitTextToKclSystemIO({ const TRUNCATED_PROMPT_LENGTH = 24 // Only add the prompt name if it is a preexisting project - newFileName = `${value.prompt + const subDirectoryAsPromptName = `${value.prompt .slice(0, TRUNCATED_PROMPT_LENGTH) .replace(/\s/gi, '-') .replace(/\W/gi, '-') - .toLowerCase()}${FILE_EXT}` - - // If the project is new generate a main.kcl - if (isProjectNew) { - newFileName = PROJECT_ENTRYPOINT - } + .toLowerCase()}` if (isDesktop()) { - // We have to preemptively run our unique file name logic, - // so that we can pass the unique file name to the toast, - // and by extension the file-deletion-on-reject logic. - newFileName = getNextFileName({ - entryName: newFileName, - baseDir: projectName, - }).name - + if (!isProjectNew) { + // If the project is new, use a sub dir + const firstLevelDirectories = getAllSubDirectoriesAtProjectRoot({ + projectFolderName: projectName, + }) + const uniqueSubDirectoryName = getUniqueProjectName( + subDirectoryAsPromptName, + firstLevelDirectories + ) + uniqueProjectName = joinOSPaths(projectName, uniqueSubDirectoryName) + } systemIOActor.send({ type: SystemIOMachineEvents.createKCLFile, data: { - requestedProjectName: projectName, + requestedProjectName: uniqueProjectName, requestedCode: value.code, requestedFileNameWithExtension: newFileName, }, @@ -251,11 +252,14 @@ export async function submitAndAwaitTextToKclSystemIO({ toastId, data: textToCadOutputCreated, token, - projectName: projectName, + // This can be a subdir within the rootProjectName + projectName: uniqueProjectName, fileName: newFileName, navigate, isProjectNew, settings, + // This is always the root project name, no subdir + rootProjectName: projectName, }), { id: toastId, diff --git a/src/machines/systemIO/snapshotContext.ts b/src/machines/systemIO/snapshotContext.ts index b227bed92..c64ff4cf2 100644 --- a/src/machines/systemIO/snapshotContext.ts +++ b/src/machines/systemIO/snapshotContext.ts @@ -1,4 +1,6 @@ +import type { FileEntry } from '@src/lib/project' import { systemIOActor } from '@src/lib/singletons' +import { isArray } from '@src/lib/utils' export const folderSnapshot = () => { const { folders } = systemIOActor.getSnapshot().context @@ -9,3 +11,48 @@ export const defaultProjectFolderNameSnapshot = () => { const { defaultProjectFolderName } = systemIOActor.getSnapshot().context return defaultProjectFolderName } + +/** + * From the application project directory go down to a project folder and list all the folders at that directory level + * application project directory: /home/documents/zoo-modeling-app-projects/ + * + * /home/documents/zoo-modeling-app-projects/car-door/ + * ├── handle + * ├── main.kcl + * └── window + * + * The two folders are handle and window + * + * @param {Object} params + * @param {string} params.projectFolderName - The name with no path information. + * @returns {FileEntry[]} An array of subdirectory names found at the root level of the specified project folder. + */ +export const getAllSubDirectoriesAtProjectRoot = ({ + projectFolderName, +}: { projectFolderName: string }): FileEntry[] => { + const subDirectories: FileEntry[] = [] + const { folders } = systemIOActor.getSnapshot().context + + const projectFolder = folders.find((folder) => { + return folder.name === projectFolderName + }) + + // Find the subdirectories + if (projectFolder) { + // 1st level + const children = projectFolder.children + if (children) { + children.forEach((childFileOrDirectory) => { + // 2nd level + const secondLevelChild = childFileOrDirectory.children + // if secondLevelChild is null then it is a file + if (secondLevelChild && isArray(secondLevelChild)) { + // this is a directory! + subDirectories.push(childFileOrDirectory) + } + }) + } + } + + return subDirectories +}