From 1fbaaa6e7793c6dd284df83a7548c2abd4e10dc5 Mon Sep 17 00:00:00 2001 From: Ivan Fontosh Date: Fri, 7 Aug 2026 08:23:08 +0800 Subject: [PATCH] feat(scene): video map editor parity and help updates Enable scene editor, overlays, effects, and darkness on video scenes; brighten GM trap markers; document snap, NPC types, materials, and control controls in RU/EN help. Co-authored-by: Cursor --- app/main/index.ts | 4 +- app/main/project/sharpRuntime.mjs | 22 ++- app/renderer/control/ControlApp.tsx | 56 +++---- .../control/ControlScenePreview.module.css | 2 + app/renderer/control/ControlScenePreview.tsx | 14 +- .../control/controlApp.effectsPanel.test.ts | 2 +- app/renderer/editor/EditorApp.tsx | 4 + app/renderer/editor/i18n/editorMessages.ts | 40 ++--- app/renderer/sceneEditor/SceneEditorApp.tsx | 33 +++- app/renderer/shared/ContainedVideo.module.css | 16 ++ app/renderer/shared/ContainedVideo.tsx | 158 ++++++++++++++++++ app/renderer/shared/PresentationView.tsx | 51 +++--- .../shared/traps/SceneTrapsOverlay.module.css | 18 +- .../shared/videoSceneMapParity.test.ts | 48 ++++++ app/shared/types/containMediaRect.test.ts | 45 +++++ app/shared/types/containMediaRect.ts | 26 +++ package.json | 2 +- 17 files changed, 443 insertions(+), 98 deletions(-) create mode 100644 app/renderer/shared/ContainedVideo.module.css create mode 100644 app/renderer/shared/ContainedVideo.tsx create mode 100644 app/renderer/shared/videoSceneMapParity.test.ts create mode 100644 app/shared/types/containMediaRect.test.ts create mode 100644 app/shared/types/containMediaRect.ts diff --git a/app/main/index.ts b/app/main/index.ts index 1da613e..05856a7 100644 --- a/app/main/index.ts +++ b/app/main/index.ts @@ -267,7 +267,9 @@ function emitScenePlayerTokensSessionState(): void { function syncSceneDarknessForProject(project: Project): void { const cacheKey = project.currentGraphNodeId ?? project.currentSceneId ?? null; const scene = project.currentSceneId ? project.scenes[project.currentSceneId] : undefined; - const enabled = Boolean(scene?.darkenScene) && scene?.previewAssetType === 'image'; + const enabled = + Boolean(scene?.darkenScene) && + (scene?.previewAssetType === 'image' || scene?.previewAssetType === 'video'); sceneDarknessStore.switchScene(cacheKey, enabled); } diff --git a/app/main/project/sharpRuntime.mjs b/app/main/project/sharpRuntime.mjs index cd5e9a0..036630b 100644 --- a/app/main/project/sharpRuntime.mjs +++ b/app/main/project/sharpRuntime.mjs @@ -1,10 +1,30 @@ /** * Lazy `sharp` load so a corrupt/missing native install does not crash Electron at import time. * Call only from image-processing paths; errors are recoverable for the rest of the app. + * + * Note: main is bundled to CJS (esbuild). `import.meta.url` is empty there — prefer `__filename`. */ import { createRequire } from 'node:module'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; -const require = createRequire(import.meta.url); +/** + * @returns {string} + */ +function requireBaseFilename() { + // CJS bundle / Electron main + if (typeof __filename === 'string' && __filename.length > 0) { + return __filename; + } + // Direct ESM (unit tests) + const metaUrl = import.meta.url; + if (typeof metaUrl === 'string' && metaUrl.startsWith('file:')) { + return fileURLToPath(metaUrl); + } + return path.join(process.cwd(), 'package.json'); +} + +const require = createRequire(requireBaseFilename()); /** @type {typeof import('sharp') | null} */ let cached = null; diff --git a/app/renderer/control/ControlApp.tsx b/app/renderer/control/ControlApp.tsx index fef8866..860eb3d 100644 --- a/app/renderer/control/ControlApp.tsx +++ b/app/renderer/control/ControlApp.tsx @@ -373,8 +373,7 @@ export function ControlApp() { const currentHistoryIdx = currentGraphNodeId != null ? history.lastIndexOf(currentGraphNodeId) : -1; const currentScene = project && session?.currentSceneId ? project.scenes[session.currentSceneId] : undefined; - const isVideoPreviewScene = currentScene?.previewAssetType === 'video'; - const isDarkenScene = Boolean(currentScene?.darkenScene) && !isVideoPreviewScene; + const isDarkenScene = Boolean(currentScene?.darkenScene); const snapNormActive = useCallback( (nx: number, ny: number) => { @@ -868,7 +867,7 @@ export function ControlApp() { useEffect(() => { const frame = previewFrameRef.current; - if (!frame || isVideoPreviewScene) return; + if (!frame) return; const onWheel = (e: WheelEvent) => { e.preventDefault(); const host = previewHostRef.current; @@ -892,7 +891,7 @@ export function ControlApp() { }; frame.addEventListener('wheel', onWheel, { passive: false }); return () => frame.removeEventListener('wheel', onWheel); - }, [isVideoPreviewScene]); + }, []); useEffect(() => { return () => { @@ -1256,11 +1255,6 @@ export function ControlApp() { } async function commitStroke(): Promise { - if (isVideoPreviewScene) { - brushRef.current = null; - clearDraftFromPixi(); - return; - } if (!fxState) return; const b = brushRef.current; if (!b) return; @@ -1608,8 +1602,7 @@ export function ControlApp() {
- {!isVideoPreviewScene ? ( - <> + <>
{t('control.effects')}
@@ -1815,7 +1808,6 @@ export function ControlApp() {
- ) : null}
{t('control.storyLine')}
@@ -1960,14 +1952,11 @@ export function ControlApp() {
- {isVideoPreviewScene ?
{t('control.videoBrushHint')}
: null}
- {!isVideoPreviewScene ? ( - <> + <> { + {...(markersInteractive + ? { + onContextMenu: ( + e: React.MouseEvent, + placement: { id: string }, + ) => { setNpcSessionCtxMenu({ x: e.clientX, y: e.clientY, placementId: String(placement.id), }); - } - : undefined - } + }, + } + : {})} /> ) : null} {USERS_BRANCH_FEATURES_ENABLED && previewContentRect ? ( @@ -2280,6 +2271,13 @@ export function ControlApp() { /> ) : null} + {previewContentRect && currentScene?.darkenScene ? ( + ) : null} {(() => { const project = session?.project; @@ -2322,7 +2320,7 @@ export function ControlApp() { const showMaterial = materialItems.length > 0; const showNpcs = npcItems.length > 0; const screenRect = presentationScreenRect; - const showGuide = Boolean(screenRect) && !isVideoPreviewScene; + const showGuide = Boolean(screenRect); if (!showMaterial && !showNpcs && !showGuide) return null; const closes = [ ...(showMaterial @@ -2357,14 +2355,6 @@ export function ControlApp() { }; return ( <> - {previewContentRect && currentScene?.darkenScene ? ( - - ) : null} ) : url && isVideo ? ( - + ) : (
)} diff --git a/app/renderer/control/controlApp.effectsPanel.test.ts b/app/renderer/control/controlApp.effectsPanel.test.ts index a2654e6..d3e976b 100644 --- a/app/renderer/control/controlApp.effectsPanel.test.ts +++ b/app/renderer/control/controlApp.effectsPanel.test.ts @@ -58,7 +58,7 @@ void test('ControlApp: эффект «взрыв» + ловушка исполь const appSrc = readControlApp(); const sfxSrc = fs.readFileSync(path.join(here, 'explosionSfx.ts'), 'utf8'); assert.ok(appSrc.includes("title={t('control.explosion')}")); - assert.ok(appSrc.includes("tool: 'explosion'")); + assert.ok(appSrc.includes("selectEffectTool('explosion')")); assert.ok(appSrc.includes("type: 'explosion'")); assert.ok(appSrc.includes('getExplosionEffectLifeMs')); assert.ok(appSrc.includes('playExplosionEffectSound')); diff --git a/app/renderer/editor/EditorApp.tsx b/app/renderer/editor/EditorApp.tsx index 3eecaad..dff33aa 100644 --- a/app/renderer/editor/EditorApp.tsx +++ b/app/renderer/editor/EditorApp.tsx @@ -2755,6 +2755,10 @@ function SceneInspector({ > {t('scene.rotate')} + + ) : null} + {previewAssetId && (previewAssetType === 'image' || previewAssetType === 'video') ? ( + <>