feat(control): инструмент «Маркер» на области видимости

Добавлена кисть маркера на пульте и презентации с настройками в панели эффектов; штрихи привязаны к жёлтой рамке, ластик и очистка работают с отдельным сессионным слоем.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-09-02 12:53:06 +08:00
parent 153be98780
commit 8b28a9b11a
18 changed files with 906 additions and 32 deletions
+28 -1
View File
@@ -7,8 +7,10 @@ import { USERS_BRANCH_FEATURES_ENABLED } from '../../shared/features/usersBranch
import { ExplosionVideoOverlay } from './effects/ExplosionVideoOverlay';
import { PixiEffectsOverlay } from './effects/PxiEffectsOverlay';
import { SceneDarknessOverlay } from './effects/SceneDarknessOverlay';
import { SceneMarkerOverlay } from './effects/SceneMarkerOverlay';
import { useEffectsState } from './effects/useEffectsState';
import { useSceneDarknessState } from './effects/useSceneDarknessState';
import { useSceneMarkerState } from './effects/useSceneMarkerState';
import { DEFAULT_MATERIALS_OVERLAY_LAYOUT, DEFAULT_NPCS_OVERLAY_LAYOUT, defaultLegendLayoutForMaterial } from '../../shared/types';
import { SceneGridOverlay } from './grid/SceneGridOverlay';
import { MaterialLegendPanel } from './materials/MaterialLegendPanel';
@@ -57,6 +59,7 @@ export function PresentationView({
}: PresentationViewProps) {
const [fxState] = useEffectsState();
const [sdState] = useSceneDarknessState();
const [markerState] = useSceneMarkerState();
const [sceneTraps] = useSceneTrapsState();
const [sceneView] = useSceneViewState();
const [materialsOverlay] = useMaterialsOverlayState();
@@ -70,9 +73,30 @@ export function PresentationView({
const [tokenPathSession, tokenPathApi] = useTokenPathSession();
const [vp] = useVideoPlaybackState();
const videoElRef = useRef<HTMLVideoElement | null>(null);
const rootRef = useRef<HTMLDivElement | null>(null);
const [contentRect, setContentRect] = React.useState<{ x: number; y: number; w: number; h: number } | null>(
null,
);
const [markerViewport, setMarkerViewport] = React.useState<{
x: number;
y: number;
w: number;
h: number;
} | null>(null);
useEffect(() => {
const el = rootRef.current;
if (!el) return;
const sync = () => {
const w = Math.max(1, el.clientWidth);
const h = Math.max(1, el.clientHeight);
setMarkerViewport({ x: 0, y: 0, w, h });
};
sync();
const ro = new ResizeObserver(sync);
ro.observe(el);
return () => ro.disconnect();
}, []);
const scene =
session?.project && session.currentSceneId ? session.project.scenes[session.currentSceneId] : undefined;
const project = session?.project;
@@ -181,7 +205,7 @@ export function PresentationView({
]);
return (
<div className={styles.root}>
<div ref={rootRef} className={styles.root}>
{shownImageUrl && scene?.previewAssetType === 'image' ? (
<div className={styles.fill}>
<RotatedImage
@@ -285,6 +309,9 @@ export function PresentationView({
contentRect ? (
<SceneDarknessOverlay state={sdState} overlayAlpha={1} viewport={contentRect} style={{ zIndex: 30 }} />
) : null}
{markerViewport ? (
<SceneMarkerOverlay state={markerState} viewport={markerViewport} style={{ zIndex: 48 }} />
) : null}
<SceneOverlayHost
active={activeMaterialItems.length > 0 || activeNpcItems.length > 0}
legendLayer={