feat(control): grid snap, NPC context actions, and overlay dim fix

Re-enable users-branch UI, snap session tokens to square/hex grid from the control preview, refine inactive/open-info NPC menus, block marker actions while an effect brush is active, and dim materials/NPC overlays only when they are open.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-08-06 13:48:59 +08:00
parent e53d1ea934
commit 4456eb0277
24 changed files with 563 additions and 41 deletions
@@ -15,9 +15,12 @@ export type SceneOverlayCloseAction = {
type SceneOverlayHostProps = {
/** Есть ли что показывать (материал и/или NPC). */
active: boolean;
/** Область картинки сцены (contain); координаты относительно родителя. */
/**
* Область раскладки кадров материалов/NPC (и жёлтой рамки).
* На пульте — прямоугольник соотношения сторон презентации; на презентации обычно не задаётся (весь экран).
*/
viewport?: SceneOverlayViewport | null;
/** Рамка видимой области (предпросмотр пульта). */
/** Жёлтая рамка видимой области презентации (предпросмотр пульта). Без dim. */
showViewportGuide?: boolean;
zoomTool?: MaterialsZoomTool | NpcsZoomTool;
onZoomAt?: (nx: number, ny: number, target: EventTarget | null) => void;
@@ -26,8 +29,9 @@ type SceneOverlayHostProps = {
};
/**
* Общий слой подложки для Materials + NPCs: один root и один `.dim`.
* Кадры остаются в дочерних оверлеях (`embedded`).
* Общий слой подложки для Materials + NPCs.
* Dim — только при `active`, на весь родитель (экран презентации / рамка превью пульта).
* Кадры остаются в дочерних оверлеях (`embedded`) внутри `viewport`.
*/
export function SceneOverlayHost({
active,
@@ -61,7 +65,7 @@ export function SceneOverlayHost({
ro.disconnect();
if (raf !== 0) window.cancelAnimationFrame(raf);
};
}, [active]);
}, [active, showViewportGuide, viewport]);
const ctx = useMemo(() => ({ rootRef, view }), [view]);
@@ -83,6 +87,7 @@ export function SceneOverlayHost({
return (
<SceneOverlayViewContext.Provider value={ctx}>
{active ? <div className={styles.dim} aria-hidden /> : null}
<div
ref={rootRef}
className={[styles.root, styles.hostHitThrough, captureZoom ? styles.captureZoom : '', zoomCursor]
@@ -98,7 +103,6 @@ export function SceneOverlayHost({
}}
>
{showViewportGuide ? <div className={styles.viewportGuide} aria-hidden /> : null}
<div className={styles.dim} />
{children}
{closes.length > 0 ? (
<div className={styles.closeStack}>
@@ -18,6 +18,9 @@ void test('SceneOverlayHost: один dim для materials + npcs в Control и
assert.ok(host.includes('styles.dim'));
assert.ok(host.includes('hostHitThrough'));
// Dim только при active; жёлтая рамка без затемнения.
assert.match(host, /\{active \? <div className=\{styles\.dim\}/);
assert.ok(host.includes('showViewportGuide'));
assert.ok(control.includes('SceneOverlayHost'));
assert.ok(control.includes('embedded'));
assert.ok(presentation.includes('SceneOverlayHost'));
@@ -29,6 +32,7 @@ void test('SceneOverlayHost: один dim для materials + npcs в Control и
assert.ok(control.includes('embedded'));
assert.ok(presentation.includes('embedded'));
assert.match(css, /\.hostHitThrough\s*\{[^}]*pointer-events:\s*none/s);
assert.match(css, /\.dim\s*\{[^}]*inset:\s*0/s);
});
void test('MaterialOverlay / NpcsSceneOverlay поддерживают embedded без собственного dim', () => {