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
@@ -0,0 +1,75 @@
import React, { useRef } from 'react';
import type { MaterialLegend, MaterialsOverlayLayout } from '../../../shared/types';
import { DEFAULT_MATERIALS_OVERLAY_LAYOUT } from '../../../shared/types';
import { useSceneOverlayView } from '../sceneOverlay/SceneOverlayViewContext';
import styles from './MaterialLegendPanel.module.css';
type Props = {
legend: MaterialLegend;
layout: MaterialsOverlayLayout;
editable?: boolean;
onLayoutChange?: (layout: MaterialsOverlayLayout) => void;
};
export function MaterialLegendPanel({
legend,
layout,
editable = false,
onLayoutChange,
}: Props) {
const host = useSceneOverlayView();
const view = host?.view ?? { w: 1, h: 1 };
const dragRef = useRef<{ startX: number; startY: number; origin: MaterialsOverlayLayout } | null>(
null,
);
const effective = layout ?? DEFAULT_MATERIALS_OVERLAY_LAYOUT;
const w = Math.max(180, Math.min(view.w * 0.36, 340) * effective.scale);
const left = effective.cx * view.w - w / 2;
const top = effective.cy * view.h - 40;
if (!legend.enabled || legend.items.length === 0) return null;
return (
<div
className={styles.panel}
style={{ left, top, width: w }}
onPointerDown={(e) => {
if (!editable || !onLayoutChange) return;
e.stopPropagation();
(e.currentTarget as HTMLDivElement).setPointerCapture(e.pointerId);
dragRef.current = {
startX: e.clientX,
startY: e.clientY,
origin: { ...effective },
};
}}
onPointerMove={(e) => {
const d = dragRef.current;
if (!d || !onLayoutChange) return;
const dx = (e.clientX - d.startX) / Math.max(1, view.w);
const dy = (e.clientY - d.startY) / Math.max(1, view.h);
onLayoutChange({
...d.origin,
cx: d.origin.cx + dx,
cy: d.origin.cy + dy,
});
}}
onPointerUp={() => {
dragRef.current = null;
}}
>
<div className={styles.title}>Легенда</div>
{legend.items
.slice()
.sort((a, b) => a.number - b.number)
.map((item) => (
<div key={item.id} className={styles.item}>
<div className={styles.num}>{item.number}</div>
<div className={styles.text}>{item.text || '—'}</div>
</div>
))}
</div>
);
}