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
+108
View File
@@ -0,0 +1,108 @@
import { applySceneMarkerEraserStroke } from '../../shared/sceneMarkerEraser';
import type { SceneMarkerEvent, SceneMarkerState, SceneMarkerStroke } from '../../shared/types';
function emptyState(): SceneMarkerState {
return {
revision: 1,
cacheKey: null,
strokes: [],
draft: null,
};
}
function makeStrokeId(prefix: string): string {
return `${prefix}_${String(Date.now())}_${String(Math.floor(Math.random() * 1_000_000_000))}`;
}
export class SceneMarkerStore {
private state: SceneMarkerState = emptyState();
/** Кэш штрихов маркера по ключу (graphNodeId или sceneId) на время сессии показа. */
private cache = new Map<string, SceneMarkerStroke[]>();
private currentKey: string | null = null;
getState(): SceneMarkerState {
return { ...this.state, strokes: [...this.state.strokes] };
}
/** Сброс кэша при новом запуске показа (кнопка «Запустить»). */
resetSession(): void {
this.cache.clear();
this.currentKey = null;
this.state = emptyState();
}
/** Переключение сцены: сохраняем штрихи в кэш и загружаем новую сцену. */
switchScene(cacheKey: string | null): SceneMarkerState {
if (this.currentKey !== null) {
this.cache.set(this.currentKey, [...this.state.strokes]);
}
this.currentKey = cacheKey;
const strokes = cacheKey ? [...(this.cache.get(cacheKey) ?? [])] : [];
this.state = {
revision: this.state.revision + 1,
cacheKey,
strokes,
draft: null,
};
return this.getState();
}
dispatch(event: SceneMarkerEvent): SceneMarkerState {
switch (event.kind) {
case 'draft.set':
this.state = {
...this.state,
revision: this.state.revision + 1,
draft: event.draft,
};
return this.getState();
case 'stroke.add': {
const strokes = [...this.state.strokes, event.stroke];
if (this.currentKey) {
this.cache.set(this.currentKey, strokes);
}
this.state = {
...this.state,
revision: this.state.revision + 1,
strokes,
draft: null,
};
return this.getState();
}
case 'strokes.erase': {
const strokes = applySceneMarkerEraserStroke(
this.state.strokes,
event.points,
event.radiusN,
(prefix) => makeStrokeId(prefix),
);
if (this.currentKey) {
this.cache.set(this.currentKey, strokes);
}
this.state = {
...this.state,
revision: this.state.revision + 1,
strokes,
draft: null,
};
return this.getState();
}
case 'strokes.clear':
if (this.currentKey) {
this.cache.set(this.currentKey, []);
}
this.state = {
...this.state,
revision: this.state.revision + 1,
strokes: [],
draft: null,
};
return this.getState();
default: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _x: never = event;
return this.getState();
}
}
}
}