feat: scene traps, material legends, and scene editor window

Add trap overlays with session state, material legend editor/panel, and a dedicated scene editor window wired through IPC and project persistence.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-07-24 16:29:43 +08:00
parent d3b1c4660d
commit 02d73ddf81
39 changed files with 2563 additions and 36 deletions
+40 -15
View File
@@ -1,13 +1,14 @@
import React, { useEffect, useMemo, useState } from 'react';
import { createPortal } from 'react-dom';
import type { MaterialId, ProjectMaterial } from '../../shared/types';
import type { MaterialId, MaterialLegend, ProjectMaterial } from '../../shared/types';
import { RotatedImage } from '../shared/RotatedImage';
import { Button, Input } from '../shared/ui/controls';
import { useAssetUrl } from '../shared/useAssetImageUrl';
import styles from './EditorApp.module.css';
import { useEditorI18n } from './i18n/EditorI18nContext';
import { MaterialLegendEditor } from './MaterialLegendEditor';
import matStyles from './MaterialsModals.module.css';
const DND_MATERIAL_ID_MIME = 'application/x-dnd-material-id';
@@ -24,6 +25,7 @@ export type MaterialsBrowserProps = {
onDelete?: (materialId: MaterialId) => Promise<void>;
onReorder?: (materialIds: MaterialId[]) => Promise<void>;
onRotate?: (materialId: MaterialId, rotationDeg: 0 | 90 | 180 | 270) => void;
onLegendChange?: (materialId: MaterialId, legend: MaterialLegend) => Promise<void>;
onTileActivate?: (materialId: MaterialId) => void;
toolbar?: React.ReactNode;
className?: string | undefined;
@@ -44,6 +46,7 @@ export function MaterialsBrowser({
onDelete,
onReorder,
onRotate,
onLegendChange,
onTileActivate,
toolbar,
className,
@@ -179,20 +182,42 @@ export function MaterialsBrowser({
</div>
{!listOnly ? (
<div className={matStyles.previewColumn}>
<div className={matStyles.previewPane}>
{selected && selectedUrl ? (
<div className={matStyles.previewLargeHost}>
<RotatedImage
url={selectedUrl}
rotationDeg={selected.rotationDeg ?? 0}
mode="contain"
/>
</div>
) : (
<div className={matStyles.previewEmpty}>{t('materials.addPrompt')}</div>
)}
</div>
<div
className={[
matStyles.previewColumn,
onLegendChange ? matStyles.previewColumnLegend : '',
]
.filter(Boolean)
.join(' ')}
>
{selected && selectedUrl && onLegendChange ? (
<div className={matStyles.previewLegendScroll}>
<MaterialLegendEditor
key={selected.id}
largeMap
legend={selected.legend}
previewUrl={selectedUrl}
rotationDeg={selected.rotationDeg ?? 0}
onChange={(next) => {
void onLegendChange(selected.id, next);
}}
/>
</div>
) : (
<div className={matStyles.previewPane}>
{selected && selectedUrl ? (
<div className={matStyles.previewLargeHost}>
<RotatedImage
url={selectedUrl}
rotationDeg={selected.rotationDeg ?? 0}
mode="contain"
/>
</div>
) : (
<div className={matStyles.previewEmpty}>{t('materials.addPrompt')}</div>
)}
</div>
)}
{selected && onRotate ? (
<div className={matStyles.previewActions}>
<Button