diff --git a/app/main/foundry/foundryImport.ts b/app/main/foundry/foundryImport.ts index f9845b7..57449b0 100644 --- a/app/main/foundry/foundryImport.ts +++ b/app/main/foundry/foundryImport.ts @@ -387,6 +387,7 @@ export async function buildProjectFromFoundryDocuments( previewRotationDeg: 0, darkenScene: false, traps: [], + grid: { enabled: false, type: 'square', sizeN: 0.06, color: '#ffffff' }, media: { videos: [], audios: audioRefs }, settings: { autoplayVideo: previewAssetType === 'video', diff --git a/app/main/project/zipStore.ts b/app/main/project/zipStore.ts index a222708..66908a7 100644 --- a/app/main/project/zipStore.ts +++ b/app/main/project/zipStore.ts @@ -51,6 +51,7 @@ import type { } from '../../shared/types'; import { PROJECT_SCHEMA_VERSION } from '../../shared/types'; import { normalizeMaterialLegend } from '../../shared/types/materialLegend'; +import { DEFAULT_SCENE_GRID, normalizeSceneGrid } from '../../shared/types/sceneGrid'; import { normalizeSceneTrap } from '../../shared/types/sceneTraps'; import type { AssetId, GraphNodeId, MaterialId, NpcGroupId, NpcId, NpcRelationId } from '../../shared/types/ids'; import { @@ -616,11 +617,13 @@ export class ZipProjectStore { previewRotationDeg: 0, darkenScene: false, traps: [], + grid: { ...DEFAULT_SCENE_GRID }, } satisfies Scene); const next: Scene = { ...base, traps: base.traps ?? [], + grid: base.grid ?? { ...DEFAULT_SCENE_GRID }, ...(patch.title !== undefined ? { title: patch.title } : null), ...(patch.description !== undefined ? { description: patch.description } : null), ...(patch.previewAssetId !== undefined ? { previewAssetId: patch.previewAssetId } : null), @@ -640,6 +643,7 @@ export class ZipProjectStore { .filter((t): t is SceneTrap => Boolean(t)), } : null), + ...(patch.grid !== undefined ? { grid: normalizeSceneGrid(patch.grid) } : null), ...(patch.settings ? { settings: { ...base.settings, ...patch.settings } } : null), ...(patch.media ? { media: { ...base.media, ...patch.media } } : null), ...(patch.layout ? { layout: { ...base.layout, ...patch.layout } } : null), @@ -2325,6 +2329,7 @@ function normalizeScene(s: Scene): Scene { const traps = (Array.isArray(rawTraps) ? rawTraps : []) .map((t) => normalizeSceneTrap(t)) .filter((t): t is SceneTrap => Boolean(t)); + const grid = normalizeSceneGrid((s as unknown as { grid?: unknown }).grid); const rawAudios = Array.isArray(raw.audios) ? raw.audios : []; const audios = rawAudios @@ -2354,6 +2359,7 @@ function normalizeScene(s: Scene): Scene { previewRotationDeg, darkenScene, traps, + grid, layout: layoutIn ?? { x: 0, y: 0 }, media: { videos: raw.videos ?? [], diff --git a/app/renderer/control/ControlApp.tsx b/app/renderer/control/ControlApp.tsx index c6cf1fe..0fe9f7e 100644 --- a/app/renderer/control/ControlApp.tsx +++ b/app/renderer/control/ControlApp.tsx @@ -35,6 +35,7 @@ import { NpcsSceneOverlay } from '../shared/npcs/NpcsSceneOverlay'; import { useNpcsOverlayState } from '../shared/npcs/useNpcsOverlayState'; import { SceneOverlayHost } from '../shared/sceneOverlay/SceneOverlayHost'; import { useSceneViewState } from '../shared/sceneView/useSceneViewState'; +import { SceneGridOverlay } from '../shared/grid/SceneGridOverlay'; import { SceneTrapsOverlay } from '../shared/traps/SceneTrapsOverlay'; import { useSceneTrapsState } from '../shared/traps/useSceneTrapsState'; import { Button } from '../shared/ui/controls'; @@ -1710,10 +1711,11 @@ export function ControlApp() { {!isVideoPreviewScene ? ( <> + (null); const [trapsOpen, setTrapsOpen] = useState(true); + const [gridOpen, setGridOpen] = useState(true); const [selectedId, setSelectedId] = useState(null); const [view, setView] = useState({ scale: 1, ox: 0.5, oy: 0.5 }); const [contentRect, setContentRect] = useState<{ x: number; y: number; w: number; h: number } | null>( @@ -40,7 +49,8 @@ export function SceneEditorApp() { ); const hostRef = useRef(null); const dragRef = useRef(null); - const saveTimerRef = useRef(0); + const saveTrapsTimerRef = useRef(0); + const saveGridTimerRef = useRef(0); const spaceDownRef = useRef(false); const project = session?.project ?? null; @@ -49,11 +59,13 @@ export function SceneEditorApp() { const url = useAssetUrl(scene?.previewAssetId ?? null); const rot = scene?.previewRotationDeg ?? 0; const [localTraps, setLocalTraps] = useState([]); + const [localGrid, setLocalGrid] = useState({ ...DEFAULT_SCENE_GRID }); const trapsRef = useRef([]); trapsRef.current = localTraps; useEffect(() => { setLocalTraps(scene?.traps ?? []); + setLocalGrid(scene?.grid ?? { ...DEFAULT_SCENE_GRID }); setSelectedId(null); setView({ scale: 1, ox: 0.5, oy: 0.5 }); }, [sceneId, scene?.previewAssetId]); @@ -64,6 +76,11 @@ export function SceneEditorApp() { setLocalTraps(scene?.traps ?? []); }, [scene?.traps]); + useEffect(() => { + if (dragRef.current) return; + setLocalGrid(scene?.grid ?? { ...DEFAULT_SCENE_GRID }); + }, [scene?.grid]); + useEffect(() => { void api.invoke(ipcChannels.project.get, {}).then(({ project: p }) => { setSession({ project: p, currentSceneId: p?.currentSceneId ?? null }); @@ -78,14 +95,26 @@ export function SceneEditorApp() { if (!sceneId) return; setLocalTraps(next); trapsRef.current = next; - if (saveTimerRef.current) window.clearTimeout(saveTimerRef.current); - saveTimerRef.current = window.setTimeout(() => { + if (saveTrapsTimerRef.current) window.clearTimeout(saveTrapsTimerRef.current); + saveTrapsTimerRef.current = window.setTimeout(() => { void api.invoke(ipcChannels.project.updateScene, { sceneId, patch: { traps: next } }); }, 120); }, [api, sceneId], ); + const persistGrid = useCallback( + (next: SceneGrid) => { + if (!sceneId) return; + setLocalGrid(next); + if (saveGridTimerRef.current) window.clearTimeout(saveGridTimerRef.current); + saveGridTimerRef.current = window.setTimeout(() => { + void api.invoke(ipcChannels.project.updateScene, { sceneId, patch: { grid: next } }); + }, 120); + }, + [api, sceneId], + ); + useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { if (e.code === 'Space') spaceDownRef.current = true; @@ -201,6 +230,71 @@ export function SceneEditorApp() {
Колесо — зум. СКМ / Space+ЛКМ — пан. Delete — удалить выбранную ловушку.
+
+ + {gridOpen ? ( +
+ + + + +
+ ) : null} +