From 9d82e742721d9681a56d5904ce334552edfb443c Mon Sep 17 00:00:00 2001 From: Ivan Fontosh Date: Sat, 25 Jul 2026 14:41:27 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20control=20UX=20=E2=80=94=20no=20default?= =?UTF-8?q?=20tool,=20launch=20loader,=20NPC=20layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deselect effect tools by default and on re-click, block the editor during session launch, and fix NPC overlay/window empty-state layout. Co-authored-by: Cursor --- app/main/effects/effectsStore.test.ts | 5 ++ app/main/effects/effectsStore.ts | 7 ++- app/main/index.ts | 4 +- app/renderer/control/ControlApp.tsx | 47 +++++++++---------- .../control/controlApp.effectsPanel.test.ts | 2 + app/renderer/editor/EditorApp.tsx | 33 ++++++++++--- app/renderer/editor/i18n/editorMessages.ts | 6 ++- app/renderer/npcs/NpcsApp.tsx | 35 +++++++------- app/renderer/shared/npcs/NpcsSceneOverlay.tsx | 5 -- app/shared/types/effects.ts | 1 + 10 files changed, 88 insertions(+), 57 deletions(-) diff --git a/app/main/effects/effectsStore.test.ts b/app/main/effects/effectsStore.test.ts index 61b8464..5ff3558 100644 --- a/app/main/effects/effectsStore.test.ts +++ b/app/main/effects/effectsStore.test.ts @@ -3,6 +3,11 @@ import test from 'node:test'; import { EffectsStore } from './effectsStore'; +void test('defaultTool: при старте инструмент не выбран', () => { + const store = new EffectsStore(); + assert.equal(store.getState().tool.tool, 'none'); +}); + void test('pruneExpired: лёд не удаляется по времени', () => { const store = new EffectsStore(); const createdAtMs = Date.now() - 365 * 24 * 60 * 60 * 1000; diff --git a/app/main/effects/effectsStore.ts b/app/main/effects/effectsStore.ts index cb469f0..60d30fa 100644 --- a/app/main/effects/effectsStore.ts +++ b/app/main/effects/effectsStore.ts @@ -8,7 +8,12 @@ function nowMs(): number { } function defaultTool(): EffectToolState { - return { tool: 'fog', radiusN: 0.08, intensity: 0.6 }; + return { tool: 'none', radiusN: 0.08, intensity: 0.6 }; +} + +/** Сброс выбранного инструмента (при старте сессии). */ +export function effectsDefaultTool(): EffectToolState { + return defaultTool(); } export class EffectsStore { diff --git a/app/main/index.ts b/app/main/index.ts index c94000b..06278c4 100644 --- a/app/main/index.ts +++ b/app/main/index.ts @@ -19,7 +19,7 @@ import { } from '../shared/project/projectZipExtension'; import type { Project } from '../shared/types'; -import { EffectsStore } from './effects/effectsStore'; +import { EffectsStore, effectsDefaultTool } from './effects/effectsStore'; import { SceneDarknessStore } from './effects/sceneDarknessStore'; import { SceneTrapsStore } from './sceneTraps/sceneTrapsStore'; import { installIpcRouter, registerHandler, setLicenseAssert } from './ipc/router'; @@ -378,6 +378,7 @@ async function main() { registerHandler(ipcChannels.windows.openMultiWindow, () => { sceneDarknessStore.resetSession(); sceneTrapsStore.resetSession(); + effectsStore.dispatch({ kind: 'tool.set', tool: effectsDefaultTool() }); openMultiWindow(); const project = projectStore.getOpenProject(); if (project) { @@ -386,6 +387,7 @@ async function main() { } emitSceneDarknessState(); emitSceneTrapsState(); + emitEffectsState(); return { ok: true }; }); registerHandler(ipcChannels.windows.closeMultiWindow, () => { diff --git a/app/renderer/control/ControlApp.tsx b/app/renderer/control/ControlApp.tsx index f395ee4..c6cf1fe 100644 --- a/app/renderer/control/ControlApp.tsx +++ b/app/renderer/control/ControlApp.tsx @@ -23,7 +23,7 @@ import { PixiEffectsOverlay, type PixiEffectsOverlayHandle, } from '../shared/effects/PxiEffectsOverlay'; -import type { EffectInstance, ExplosionInstance } from '../../shared/types/effects'; +import type { EffectInstance, EffectToolType, ExplosionInstance } from '../../shared/types/effects'; import { SceneDarknessOverlay } from '../shared/effects/SceneDarknessOverlay'; import { useEffectsState } from '../shared/effects/useEffectsState'; import { useSceneDarknessState } from '../shared/effects/useSceneDarknessState'; @@ -839,10 +839,16 @@ export function ControlApp() { const showReturnToMain = isInSideStoryline && mainStoryReturnGraphNodeId !== null; const branchOptionOffset = showReturnToMain ? 1 : 0; - const tool = fxState?.tool ?? { tool: 'fog', radiusN: 0.08, intensity: 0.6 }; + const tool = fxState?.tool ?? { tool: 'none' as const, radiusN: 0.08, intensity: 0.6 }; const toolRef = useRef(tool); toolRef.current = tool; + /** Повторный клик по активному инструменту снимает выбор. */ + function selectEffectTool(next: Exclude): void { + const toolName: EffectToolType = tool.tool === next ? 'none' : next; + void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: toolName } }); + } + function layoutBrushCursor(): void { const el = brushCursorElRef.current; const p = cursorPosRef.current; @@ -850,7 +856,7 @@ export function ControlApp() { const ps = previewSizeRef.current; const t = toolRef.current; if (!el) return; - if (!p) { + if (!p || t.tool === 'none') { el.style.visibility = 'hidden'; return; } @@ -1447,7 +1453,7 @@ export function ControlApp() { iconOnly title={t('control.eraser')} ariaLabel={t('control.eraser')} - onClick={() => void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'eraser' } })} + onClick={() => selectEffectTool('eraser')} > 🧹 @@ -1483,7 +1489,7 @@ export function ControlApp() { iconOnly title={t('control.fog')} ariaLabel={t('control.fog')} - onClick={() => void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'fog' } })} + onClick={() => selectEffectTool('fog')} > 🌫️ @@ -1492,7 +1498,7 @@ export function ControlApp() { iconOnly title={t('control.rain')} ariaLabel={t('control.rain')} - onClick={() => void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'rain' } })} + onClick={() => selectEffectTool('rain')} > 🌧️ @@ -1501,7 +1507,7 @@ export function ControlApp() { iconOnly title={t('control.fire')} ariaLabel={t('control.fire')} - onClick={() => void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'fire' } })} + onClick={() => selectEffectTool('fire')} > 🔥 @@ -1510,7 +1516,7 @@ export function ControlApp() { iconOnly title={t('control.water')} ariaLabel={t('control.water')} - onClick={() => void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'water' } })} + onClick={() => selectEffectTool('water')} > 💧 @@ -1524,9 +1530,7 @@ export function ControlApp() { iconOnly title={t('control.lightning')} ariaLabel={t('control.lightning')} - onClick={() => - void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'lightning' } }) - } + onClick={() => selectEffectTool('lightning')} > @@ -1535,7 +1539,7 @@ export function ControlApp() { iconOnly title={t('control.sunbeam')} ariaLabel={t('control.sunbeam')} - onClick={() => void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'sunbeam' } })} + onClick={() => selectEffectTool('sunbeam')} > ☀️ @@ -1544,7 +1548,7 @@ export function ControlApp() { iconOnly title={t('control.freeze')} ariaLabel={t('control.freeze')} - onClick={() => void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'freeze' } })} + onClick={() => selectEffectTool('freeze')} > ❄️ @@ -1553,9 +1557,7 @@ export function ControlApp() { iconOnly title={t('control.darkness')} ariaLabel={t('control.darkness')} - onClick={() => - void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'darkness' } }) - } + onClick={() => selectEffectTool('darkness')} > 🌑 @@ -1564,9 +1566,7 @@ export function ControlApp() { iconOnly title={t('control.poisonCloud')} ariaLabel={t('control.poisonCloud')} - onClick={() => - void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'poisonCloud' } }) - } + onClick={() => selectEffectTool('poisonCloud')} > ☣️ @@ -1575,9 +1575,7 @@ export function ControlApp() { iconOnly title={t('control.explosion')} ariaLabel={t('control.explosion')} - onClick={() => - void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'explosion' } }) - } + onClick={() => selectEffectTool('explosion')} > 💥 @@ -1592,9 +1590,7 @@ export function ControlApp() { iconOnly title={t('control.explorerBrush')} ariaLabel={t('control.explorerBrush')} - onClick={() => - void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'exploreBrush' } }) - } + onClick={() => selectEffectTool('exploreBrush')} > 🔦 @@ -1772,6 +1768,7 @@ export function ControlApp() { return; } if (e.button !== 0) return; + if (tool.tool === 'none') return; const p = toNPoint(e); if (!p) return; cursorPosRef.current = p; diff --git a/app/renderer/control/controlApp.effectsPanel.test.ts b/app/renderer/control/controlApp.effectsPanel.test.ts index 91f0388..1e4b951 100644 --- a/app/renderer/control/controlApp.effectsPanel.test.ts +++ b/app/renderer/control/controlApp.effectsPanel.test.ts @@ -97,6 +97,8 @@ void test('ControlApp: эффекты в пульте, иконки с тулт assert.ok(src.includes("title={t('control.poisonCloud')}")); assert.ok(src.includes("title={t('control.fog')}")); assert.ok(src.includes("ariaLabel={t('control.fog')}")); + assert.ok(src.includes('selectEffectTool'), 'повторный клик снимает инструмент'); + assert.ok(src.includes("tool.tool === next ? 'none' : next")); assert.ok(src.includes('iconOnly')); assert.ok(src.includes("title={t('control.clearEffects')}")); assert.ok(src.includes("ariaLabel={t('control.clearEffects')}")); diff --git a/app/renderer/editor/EditorApp.tsx b/app/renderer/editor/EditorApp.tsx index dd864fb..b431c5a 100644 --- a/app/renderer/editor/EditorApp.tsx +++ b/app/renderer/editor/EditorApp.tsx @@ -143,6 +143,7 @@ export function EditorApp() { const [importReport, setImportReport] = useState(null); const [previewDialogSceneId, setPreviewDialogSceneId] = useState(null); const [presentationOpen, setPresentationOpen] = useState(false); + const [launching, setLaunching] = useState(false); const [licenseSnap, setLicenseSnap] = useState(null); const [checkUpdatesOpen, setCheckUpdatesOpen] = useState(false); const [appPackaged, setAppPackaged] = useState(false); @@ -351,15 +352,24 @@ export function EditorApp() { const launchFromGraphNode = useCallback( (graphNodeId: GraphNodeId) => { - if (!licenseActive) return; + if (!licenseActive || launching) return; + setLaunching(true); void (async () => { - await getDndApi().invoke(ipcChannels.project.setCurrentGraphNode, { graphNodeId }); - await getDndApi().invoke(ipcChannels.windows.openMultiWindow, {}); + try { + await getDndApi().invoke(ipcChannels.project.setCurrentGraphNode, { graphNodeId }); + await getDndApi().invoke(ipcChannels.windows.openMultiWindow, {}); + } catch { + setLaunching(false); + } })(); }, - [licenseActive], + [licenseActive, launching], ); + useEffect(() => { + if (presentationOpen) setLaunching(false); + }, [presentationOpen]); + const currentProjectName = state.project?.meta.name ?? ''; const currentFileBaseName = state.project?.meta.fileBaseName ?? ''; const existingProjectNames = useMemo(() => state.projects.map((p) => p.name), [state.projects]); @@ -646,6 +656,17 @@ export function EditorApp() { return ( <> + {launching + ? createPortal( +
+
+
+
{t('top.launching')}
+
+
, + document.body, + ) + : null} {presentationOpen ? createPortal(
@@ -821,9 +842,9 @@ export function EditorApp() { <> -
+
+
+
- ) : null} +
@@ -264,8 +263,10 @@ export function NpcsApp() { ); }) ) : ( -
- {npcs.length === 0 ? t('npcs.windowEmpty') : t('npcs.selectToShow')} +
+
+ {npcs.length === 0 ? t('npcs.windowEmpty') : t('npcs.selectToShow')} +
)}
diff --git a/app/renderer/shared/npcs/NpcsSceneOverlay.tsx b/app/renderer/shared/npcs/NpcsSceneOverlay.tsx index 990b71a..05042c3 100644 --- a/app/renderer/shared/npcs/NpcsSceneOverlay.tsx +++ b/app/renderer/shared/npcs/NpcsSceneOverlay.tsx @@ -342,11 +342,6 @@ function NpcAvatarFrame({ src={url} alt="" draggable={false} - style={{ - width: w, - height: h, - transform: 'translate(-50%, -50%)', - }} onLoad={(e) => { const img = e.currentTarget; setNatural({ w: img.naturalWidth || 1, h: img.naturalHeight || 1 }); diff --git a/app/shared/types/effects.ts b/app/shared/types/effects.ts index a7e1a02..f08f4ef 100644 --- a/app/shared/types/effects.ts +++ b/app/shared/types/effects.ts @@ -1,4 +1,5 @@ export type EffectToolType = + | 'none' | 'fog' | 'fire' | 'rain'