diff --git a/.github/workflows/publish-unsigned-preview.yml b/.github/workflows/publish-unsigned-preview.yml index ac0bccd..81a1aac 100644 --- a/.github/workflows/publish-unsigned-preview.yml +++ b/.github/workflows/publish-unsigned-preview.yml @@ -12,12 +12,12 @@ on: description: New prerelease tag; an existing release is never overwritten required: true type: string - default: v0.5.0-beta.5 + default: v0.5.0-beta.6 preview_title: description: Public prerelease title required: true type: string - default: Parlyn Engine v0.5.0 Beta 5 (unsigned) + default: Parlyn Engine v0.5.0 Beta 6 (unsigned) permissions: contents: write @@ -29,8 +29,8 @@ jobs: env: CSC_IDENTITY_AUTO_DISCOVERY: false GH_TOKEN: ${{ github.token }} - PREVIEW_TAG: ${{ inputs.preview_tag || 'v0.5.0-beta.5' }} - PREVIEW_TITLE: ${{ inputs.preview_title || 'Parlyn Engine v0.5.0 Beta 5 (unsigned)' }} + PREVIEW_TAG: ${{ inputs.preview_tag || 'v0.5.0-beta.6' }} + PREVIEW_TITLE: ${{ inputs.preview_title || 'Parlyn Engine v0.5.0 Beta 6 (unsigned)' }} steps: - name: Checkout @@ -68,14 +68,13 @@ jobs: shell: pwsh run: | @" - # Parlyn Engine v0.5.0 Beta 5 + # Parlyn Engine v0.5.0 Beta 6 - This unsigned Windows beta begins Phase 2 of the Parlyn editor. It adds - deep node duplication, safe hierarchy reparenting, recursive hierarchy - display and durable validated Scene History. Undo/Redo now survives - project close and a complete application restart when the saved history - matches the loaded Scene. Beta 4 project persistence, safe Recycle Bin - deletion and hardened desktop boundaries remain intact. + This unsigned Windows beta expands Phase 2 with hierarchy and viewport + multi-selection, safe one-step bulk deletion and the first complete + Multi-Scene Project Browser workflow. Project scenes can be validated, + opened, created, renamed and moved within the scenes directory while + preserving unsaved-change and project-path protections. **Important:** This beta is unsigned and may be blocked by Windows Smart App Control. It is published to document and validate the packaged @@ -88,7 +87,7 @@ jobs: remain pending. This release is not SignPath-signed. Maintainer acceptance instructions: - https://github.com/WebCrew/Parlyn-Engine/blob/main/docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.5.md + https://github.com/WebCrew/Parlyn-Engine/blob/main/docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.6.md "@ | Set-Content -LiteralPath release/PREVIEW-NOTES.md -Encoding utf8 - name: Publish GitHub pre-release diff --git a/CHANGES.md b/CHANGES.md index da08366..f139570 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,9 @@ ## Unreleased — Foundation Stabilization +- Added hierarchy and viewport multi-selection with Undo/Redo-safe bulk deletion. +- Added a validated Multi-Scene Project Browser with scene creation, switching, rename and move operations. +- Added guarded creation of scene subfolders without allowing path or symbolic-link escapes. - Started Phase 2 with editor commands for deep node duplication and safe reparenting. - Added recursive hierarchy rendering and fixed subtree deletion in the viewport. - Added a bounded, validated local Scene History that preserves Undo/Redo through project close and application restart. diff --git a/README.md b/README.md index 3b7102d..f5d71aa 100644 --- a/README.md +++ b/README.md @@ -223,7 +223,7 @@ See [`docs/WINDOWS-DISTRIBUTION.md`](docs/WINDOWS-DISTRIBUTION.md) for signing, verification and the maintainer acceptance test. Phase 1 acceptance is complete. The current Phase 2 editor candidate uses the -repeatable [`v0.5.0-beta.5 maintainer checklist`](docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.5.md). +repeatable [`v0.5.0-beta.6 maintainer checklist`](docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.6.md). ### Code signing policy diff --git a/docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.6.md b/docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.6.md new file mode 100644 index 0000000..4f8a778 --- /dev/null +++ b/docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.6.md @@ -0,0 +1,40 @@ +# Parlyn Engine v0.5.0 Beta 6 — Maintainer Acceptance + +This pass verifies the Phase 2 multi-selection and Multi-Scene Project Browser +against the published `v0.5.0-beta.6` Windows artifact. + +## 1. Install and launch + +1. Download the Beta 6 installer and `SHA256SUMS.txt`. +2. Verify the installer checksum. +3. Install normally and launch Parlyn without weakening Windows security. + +## 2. Multi-selection + +1. Open a project with several scene nodes. +2. Use **Ctrl+click** to select separate nodes in the Hierarchy and viewport. +3. Use **Shift+click** in the Hierarchy to select a contiguous range. +4. Confirm every selected node is highlighted. +5. Delete several selected nodes and confirm one Undo restores the complete set. +6. Select a parent and child together, delete them and confirm no orphan remains. + +## 3. Multi-Scene Project Browser + +1. Confirm the startup scene appears in the **Scenes** panel. +2. Create `Village` at `scenes/Village.parlyn-scene.json`. +3. Create `Forest Path` at `scenes/travel/Forest-Path.parlyn-scene.json`. +4. Modify and save both scenes, then switch between them from the panel. +5. Make an unsaved edit and confirm switching offers Cancel, Discard and Save. +6. Rename `Village` to `Oak Village` and move it to + `scenes/locations/Oak-Village.parlyn-scene.json`. +7. Restart Parlyn, reopen the project and confirm all scenes and contents remain. +8. Attempt to move a scene onto an existing scene path and confirm Parlyn refuses + without changing either file. + +## 4. Regression + +Confirm project creation, Save, Close, Open, Recycle Bin deletion, Undo/Redo, +duplication and hierarchy reparenting still work. + +Beta 6 passes only when all required checks succeed or a defect is corrected and +the affected check is repeated against a new artifact. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index cca8224..620d7f5 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -50,7 +50,7 @@ Completion evidence: - duplication and reparenting — initial editor commands complete; - durable, validated local Scene History — initial foundation complete; - multi-selection where appropriate — initial hierarchy and viewport selection complete; -- multi-scene project browser — initial validated scene listing and switching complete; +- multi-scene project browser — validated listing, switching, creation, rename and move complete; - rename and move project assets; - unsaved-change protection; - editor preferences; diff --git a/docs/TESTING-v0.5.0.md b/docs/TESTING-v0.5.0.md index a81ba59..83acfd6 100644 --- a/docs/TESTING-v0.5.0.md +++ b/docs/TESTING-v0.5.0.md @@ -1,7 +1,7 @@ # Parlyn Engine v0.5.0 Test Guide For the current Phase 2 editor acceptance pass, use the shorter artifact-specific -[`MAINTAINER-ACCEPTANCE-v0.5.0-beta.5.md`](MAINTAINER-ACCEPTANCE-v0.5.0-beta.5.md) +[`MAINTAINER-ACCEPTANCE-v0.5.0-beta.6.md`](MAINTAINER-ACCEPTANCE-v0.5.0-beta.6.md) and record the result in Issue #29. The guide below remains the detailed feature reference. This test focuses on the two new foundations introduced in v0.5.0: viewport transform gizmos and the module lifecycle. diff --git a/docs/WINDOWS-DISTRIBUTION.md b/docs/WINDOWS-DISTRIBUTION.md index a6d1cbf..19f111f 100644 --- a/docs/WINDOWS-DISTRIBUTION.md +++ b/docs/WINDOWS-DISTRIBUTION.md @@ -133,5 +133,5 @@ pending separately from the resolved development-binary blocker. Phase 1 functional acceptance completed with the unsigned `v0.5.0-beta.4` prerelease and Issue #23. The current Phase 2 editor candidate is -`v0.5.0-beta.5`; its repeatable checklist is documented in -[`MAINTAINER-ACCEPTANCE-v0.5.0-beta.5.md`](MAINTAINER-ACCEPTANCE-v0.5.0-beta.5.md). +`v0.5.0-beta.6`; its repeatable checklist is documented in +[`MAINTAINER-ACCEPTANCE-v0.5.0-beta.6.md`](MAINTAINER-ACCEPTANCE-v0.5.0-beta.6.md). diff --git a/package-lock.json b/package-lock.json index 2752775..1a95fe0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parlyn-engine", - "version": "0.5.0-beta.5", + "version": "0.5.0-beta.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "parlyn-engine", - "version": "0.5.0-beta.5", + "version": "0.5.0-beta.6", "license": "MIT", "dependencies": { "three": "0.185.1" diff --git a/package.json b/package.json index e7aaf37..c66b718 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parlyn-engine", - "version": "0.5.0-beta.5", + "version": "0.5.0-beta.6", "private": true, "description": "Parlyn Engine - open-source 2.5D-first game engine editor", "author": "Parlyn Engine contributors", diff --git a/scripts/check-desktop-boundaries.cjs b/scripts/check-desktop-boundaries.cjs index a2b83c1..f72f07d 100644 --- a/scripts/check-desktop-boundaries.cjs +++ b/scripts/check-desktop-boundaries.cjs @@ -4,7 +4,7 @@ const os = require('os'); const path = require('path'); const vm = require('vm'); const { assertTrustedIpcEvent, assertIpcPayload } = require('../src/main/ipcSecurity'); -const { resolveExistingProjectPath, resolveWritableProjectPath } = require('../src/main/projectPaths'); +const { resolveExistingProjectPath, resolveWritableProjectPath, resolveWritableProjectPathCreatingParents } = require('../src/main/projectPaths'); (async () => { const editorUrl = 'file:///parlyn/src/renderer/index.html'; @@ -24,6 +24,9 @@ const { resolveExistingProjectPath, resolveWritableProjectPath } = require('../s const realScenePath = await fs.realpath(scenePath); assert.equal(await resolveExistingProjectPath(temporaryRoot, 'scenes/Main.parlyn-scene.json'), realScenePath); assert.equal(await resolveWritableProjectPath(temporaryRoot, 'scenes/Main.parlyn-scene.json'), realScenePath); + const nestedScenePath = await resolveWritableProjectPathCreatingParents(temporaryRoot, 'scenes/chapters/Intro.parlyn-scene.json'); + const realNestedParent = await fs.realpath(path.join(temporaryRoot, 'scenes', 'chapters')); + assert.equal(nestedScenePath, path.join(realNestedParent, 'Intro.parlyn-scene.json')); try { await fs.symlink(outsideRoot, path.join(temporaryRoot, 'escaped'), 'dir'); @@ -58,6 +61,8 @@ const { resolveExistingProjectPath, resolveWritableProjectPath } = require('../s assert.match(main, /secureHandle\('parlyn:app:get-info'/); assert.match(main, /secureHandle\('parlyn:project:open'/); assert.match(main, /secureHandle\('parlyn:project:open-scene'/); + assert.match(main, /secureHandle\('parlyn:project:create-scene'/); + assert.match(main, /secureHandle\('parlyn:project:move-scene'/); assert.match(main, /secureHandle\('parlyn:project:close'/); assert.match(main, /secureHandle\('parlyn:project:delete'/); assert.match(main, /parlyn-scene-history/); @@ -75,7 +80,7 @@ const { resolveExistingProjectPath, resolveWritableProjectPath } = require('../s } }, { filename:'src/main/preload.js' }); assert.ok(exposedHost, 'Preload must expose window.parlynHost.'); - for (const method of ['getAppInfo','createProject','openProject','openProjectScene','closeProject','deleteProject','saveProjectScene','saveProjectWorld','saveSceneAs','openScene','importAssets']) { + for (const method of ['getAppInfo','createProject','openProject','openProjectScene','createProjectScene','moveProjectScene','closeProject','deleteProject','saveProjectScene','saveProjectWorld','saveSceneAs','openScene','importAssets']) { assert.equal(typeof exposedHost[method], 'function', `Preload host is missing ${method}().`); } diff --git a/scripts/check-electron-preload.cjs b/scripts/check-electron-preload.cjs index c4766d9..f34cbe0 100644 --- a/scripts/check-electron-preload.cjs +++ b/scripts/check-electron-preload.cjs @@ -7,6 +7,8 @@ const methods = [ 'createProject', 'openProject', 'openProjectScene', + 'createProjectScene', + 'moveProjectScene', 'closeProject', 'deleteProject', 'saveProjectScene', diff --git a/scripts/check.cjs b/scripts/check.cjs index 64424fc..ae34858 100644 --- a/scripts/check.cjs +++ b/scripts/check.cjs @@ -50,7 +50,7 @@ for (const rel of required) { const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); if (pkg.name !== 'parlyn-engine') throw new Error('Unexpected package name.'); -if (pkg.version !== '0.5.0-beta.5') throw new Error(`Unexpected package version: ${pkg.version}`); +if (pkg.version !== '0.5.0-beta.6') throw new Error(`Unexpected package version: ${pkg.version}`); function walk(dir) { return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { diff --git a/src/main/main.js b/src/main/main.js index c89b09d..3dc4225 100644 --- a/src/main/main.js +++ b/src/main/main.js @@ -2,7 +2,7 @@ const { app, BrowserWindow, dialog, ipcMain, shell } = require('electron'); const fs = require('fs/promises'); const path = require('path'); const { pathToFileURL } = require('url'); -const { resolveExistingProjectPath, resolveWritableProjectPath } = require('./projectPaths'); +const { resolveExistingProjectPath, resolveWritableProjectPath, resolveWritableProjectPathCreatingParents } = require('./projectPaths'); const { assertTrustedIpcEvent, assertIpcPayload } = require('./ipcSecurity'); const { ProjectSession } = require('./ProjectSession'); @@ -103,18 +103,29 @@ async function listAssets(projectRoot) { async function listProjectScenes(projectRoot) { if (!projectRoot) return []; const scenesRoot = await resolveExistingProjectPath(projectRoot, 'scenes', 'Scenes directory'); - const entries = await fs.readdir(scenesRoot, { withFileTypes:true }); const scenes = []; - for (const entry of entries) { - if (!entry.isFile() || !entry.name.endsWith('.parlyn-scene.json')) continue; - const relativePath = `scenes/${entry.name}`; - const filePath = await resolveExistingProjectPath(projectRoot, relativePath, 'Project scene'); - const scene = await readDocument(filePath, 'parlyn-scene', 'Project scene'); - scenes.push({ name:scene.name, relativePath }); + async function walk(directory) { + for (const entry of await fs.readdir(directory, { withFileTypes:true })) { + if (entry.isSymbolicLink()) continue; + const fullPath=path.join(directory,entry.name); + if (entry.isDirectory()) await walk(fullPath); + else if (entry.isFile() && entry.name.endsWith('.parlyn-scene.json')) { + const relativePath=path.relative(projectRoot,fullPath).replace(/\\/g,'/'); + const filePath=await resolveExistingProjectPath(projectRoot,relativePath,'Project scene'); + const scene=await readDocument(filePath,'parlyn-scene','Project scene'); + scenes.push({ name:scene.name, relativePath }); + } + } } + await walk(scenesRoot); return scenes.sort((a,b) => a.relativePath.localeCompare(b.relativePath)); } +function requireScenePath(value) { + if (typeof value !== 'string' || !/^scenes\/.+\.parlyn-scene\.json$/.test(value)) throw new Error('Scene paths must stay inside scenes/ and end with .parlyn-scene.json.'); + return value; +} + function createWindow() { const win = new BrowserWindow({ width:1500, @@ -222,6 +233,39 @@ secureHandle('parlyn:project:open-scene', async (payload) => { return { ok:true, relativePath, scene, history:sceneHistory.history, historyWarning:sceneHistory.warning }; }, { payload:true }); +secureHandle('parlyn:project:create-scene', async (payload) => { + const activeProjectRoot=projectSession.activeProjectRoot; + if (!activeProjectRoot) throw new Error('Open a project before creating a project scene.'); + const relativePath=requireScenePath(payload?.relativePath); + const target=await resolveWritableProjectPathCreatingParents(activeProjectRoot,relativePath,'New project scene'); + try { await fs.access(target); throw new Error('A scene already exists at that project path.'); } + catch (error) { if (error.code !== 'ENOENT') throw error; } + await writeDocumentAtomic(target,payload?.scene,'parlyn-scene','New project scene'); + return { ok:true, relativePath, scenes:await listProjectScenes(activeProjectRoot) }; +}, { payload:true }); + +secureHandle('parlyn:project:move-scene', async (payload) => { + const activeProjectRoot=projectSession.activeProjectRoot; + if (!activeProjectRoot) throw new Error('Open a project before moving a project scene.'); + const sourcePath=requireScenePath(payload?.sourcePath); + const targetPath=requireScenePath(payload?.targetPath); + if (sourcePath === targetPath) return { ok:true, relativePath:sourcePath, scenes:await listProjectScenes(activeProjectRoot) }; + const source=await resolveExistingProjectPath(activeProjectRoot,sourcePath,'Existing project scene'); + const target=await resolveWritableProjectPathCreatingParents(activeProjectRoot,targetPath,'New project scene path'); + try { await fs.access(target); throw new Error('A scene already exists at that project path.'); } + catch (error) { if (error.code !== 'ENOENT') throw error; } + const scene=await readDocument(source,'parlyn-scene','Existing project scene'); + if (typeof payload?.name === 'string' && payload.name.trim()) scene.name=payload.name.trim(); + await writeDocumentAtomic(target,scene,'parlyn-scene','Moved project scene'); + const projectFile=await resolveWritableProjectPath(activeProjectRoot,'parlyn.project.json','Parlyn project file'); + const project=await readDocument(projectFile,'parlyn-project','Parlyn project file'); + if (project.startupScene === sourcePath) project.startupScene=targetPath; + project.updatedAt=new Date().toISOString(); + await writeDocumentAtomic(projectFile,project,'parlyn-project','Parlyn project file'); + await fs.unlink(source); + return { ok:true, relativePath:targetPath, project, scene, scenes:await listProjectScenes(activeProjectRoot) }; +}, { payload:true }); + secureHandle('parlyn:project:close', async () => projectSession.close()); secureHandle('parlyn:project:delete', async (payload) => projectSession.moveToTrash(payload?.confirmationName), { payload:true }); diff --git a/src/main/preload.js b/src/main/preload.js index 3ca0b43..007d4fd 100644 --- a/src/main/preload.js +++ b/src/main/preload.js @@ -7,6 +7,8 @@ contextBridge.exposeInMainWorld('parlynHost', { createProject:(payload)=>ipcRenderer.invoke('parlyn:project:create',payload), openProject:()=>ipcRenderer.invoke('parlyn:project:open'), openProjectScene:(payload)=>ipcRenderer.invoke('parlyn:project:open-scene',payload), + createProjectScene:(payload)=>ipcRenderer.invoke('parlyn:project:create-scene',payload), + moveProjectScene:(payload)=>ipcRenderer.invoke('parlyn:project:move-scene',payload), closeProject:()=>ipcRenderer.invoke('parlyn:project:close'), deleteProject:(payload)=>ipcRenderer.invoke('parlyn:project:delete',payload), saveProjectScene:(payload)=>ipcRenderer.invoke('parlyn:project:save-scene',payload), diff --git a/src/main/projectPaths.js b/src/main/projectPaths.js index 2cf11cd..fb7dd73 100644 --- a/src/main/projectPaths.js +++ b/src/main/projectPaths.js @@ -46,4 +46,24 @@ async function resolveWritableProjectPath(projectRoot, relativePath, label = 'Pr return path.join(realParent, path.basename(target)); } -module.exports = { validateRelativeProjectPath, resolveProjectPath, resolveExistingProjectPath, resolveWritableProjectPath }; +async function resolveWritableProjectPathCreatingParents(projectRoot, relativePath, label = 'Project path') { + const target = resolveProjectPath(projectRoot, relativePath, label); + const realRoot = await fs.realpath(projectRoot); + const parentParts = path.relative(path.resolve(projectRoot), path.dirname(target)).split(path.sep).filter(Boolean); + let current = realRoot; + for (const part of parentParts) { + const next = path.join(current, part); + try { + const info = await fs.lstat(next); + if (info.isSymbolicLink() || !info.isDirectory()) throw new Error(`${label} contains an unsafe parent.`); + } catch (error) { + if (error.code !== 'ENOENT') throw error; + await fs.mkdir(next); + } + current = await fs.realpath(next); + if (!isInsideOrEqual(realRoot, current)) throw new Error(`${label} escapes the project root.`); + } + return resolveWritableProjectPath(projectRoot, relativePath, label); +} + +module.exports = { validateRelativeProjectPath, resolveProjectPath, resolveExistingProjectPath, resolveWritableProjectPath, resolveWritableProjectPathCreatingParents }; diff --git a/src/renderer/app.mjs b/src/renderer/app.mjs index 0fcde38..786494e 100644 --- a/src/renderer/app.mjs +++ b/src/renderer/app.mjs @@ -625,6 +625,8 @@ async function bootstrap() { $("project-name").title = currentProjectRoot ?? ""; $("close-project").disabled = !currentProject; $("delete-project").disabled = !currentProject; + $("create-scene").disabled = !currentProject; + $("move-scene").disabled = !currentProject || !currentSceneRelativePath; } function assetIcon(ext) { if ([".png", ".jpg", ".jpeg", ".webp", ".svg"].includes(ext)) return "\u25A7"; @@ -692,6 +694,52 @@ async function bootstrap() { showError("Project scene open failed", error); } } + async function createProjectScene() { + if (!currentProject || !await mayCloseProject()) return; + const name = $("create-scene-name").value.trim() || "New Scene"; + const relativePath = $("create-scene-path").value.trim(); + try { + const newScene = new SceneDocument(name); + const result = await host.createProjectScene({ relativePath, scene:newScene.toJSON() }); + projectScenes = result.scenes ?? projectScenes; + scene = newScene; + currentSceneRelativePath = result.relativePath; + currentFilePath = null; + history.clear(); + renderer.rebuild(scene); + clearSelection(); + updateHistoryButtons(); + updateProjectUI(); + renderProjectScenes(); + setDirty(false); + $("create-scene-dialog").close(); + status.textContent = `Scene created: ${result.relativePath}`; + } catch (error) { + showError("Project scene creation failed", error); + } + } + async function moveProjectScene() { + if (!currentProject || !currentSceneRelativePath || !await mayCloseProject()) return; + const sourcePath = currentSceneRelativePath; + try { + const result = await host.moveProjectScene({ sourcePath, targetPath:$("move-scene-path").value.trim(), name:$("move-scene-name").value.trim() }); + currentProject = ProjectDocument.fromJSON(result.project); + scene = SceneDocument.fromJSON(result.scene); + currentSceneRelativePath = result.relativePath; + projectScenes = result.scenes ?? []; + history.clear(); + renderer.rebuild(scene); + clearSelection(); + updateHistoryButtons(); + updateProjectUI(); + renderProjectScenes(); + setDirty(false); + $("move-scene-dialog").close(); + status.textContent = `Scene moved: ${result.relativePath}`; + } catch (error) { + showError("Project scene move failed", error); + } + } async function importAssets() { if (!currentProject) { status.textContent = "Create or open a Parlyn project before importing assets."; @@ -774,6 +822,17 @@ async function bootstrap() { $("new-scene").addEventListener("click", newScene); $("new-project").addEventListener("click", () => $("project-dialog").showModal()); $("open-project").addEventListener("click", openProject); + $("create-scene").addEventListener("click", () => $("create-scene-dialog").showModal()); + $("cancel-create-scene").addEventListener("click", () => $("create-scene-dialog").close()); + $("confirm-create-scene").addEventListener("click", createProjectScene); + $("move-scene").addEventListener("click", () => { + const entry = projectScenes.find((item) => item.relativePath === currentSceneRelativePath); + $("move-scene-name").value = entry?.name ?? scene.name; + $("move-scene-path").value = currentSceneRelativePath ?? ""; + $("move-scene-dialog").showModal(); + }); + $("cancel-move-scene").addEventListener("click", () => $("move-scene-dialog").close()); + $("confirm-move-scene").addEventListener("click", moveProjectScene); $("confirm-project").addEventListener("click", createProject); $("cancel-project").addEventListener("click", () => $("project-dialog").close()); $("add-node").addEventListener("click", () => $("add-dialog").showModal()); diff --git a/src/renderer/index.html b/src/renderer/index.html index 8ef6f64..961982e 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -37,7 +37,7 @@ -