From 5facb6f13639e400e9fa2e66b0a898317900eb3a Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Tue, 14 Apr 2026 01:53:02 +0200 Subject: [PATCH] Add set project scheduler command and update scene document to include scheduler --- src/commands/set-project-scheduler-command.ts | 49 ++++++++++++++++ src/document/scene-document.ts | 22 +++++++- src/entities/npc-actor-registry.ts | 56 ++++++++++++++++++- 3 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 src/commands/set-project-scheduler-command.ts diff --git a/src/commands/set-project-scheduler-command.ts b/src/commands/set-project-scheduler-command.ts new file mode 100644 index 00000000..db20172a --- /dev/null +++ b/src/commands/set-project-scheduler-command.ts @@ -0,0 +1,49 @@ +import { createOpaqueId } from "../core/ids"; +import { + cloneProjectScheduler, + type ProjectScheduler +} from "../scheduler/project-scheduler"; + +import type { EditorCommand } from "./command"; + +interface SetProjectSchedulerCommandOptions { + label: string; + scheduler: ProjectScheduler; +} + +export function createSetProjectSchedulerCommand( + options: SetProjectSchedulerCommandOptions +): EditorCommand { + const nextScheduler = cloneProjectScheduler(options.scheduler); + let previousScheduler: ProjectScheduler | null = null; + + return { + id: createOpaqueId("command"), + label: options.label, + execute(context) { + const currentProjectDocument = context.getProjectDocument(); + + if (previousScheduler === null) { + previousScheduler = cloneProjectScheduler( + currentProjectDocument.scheduler + ); + } + + context.setProjectDocument({ + ...currentProjectDocument, + scheduler: cloneProjectScheduler(nextScheduler) + }); + }, + undo(context) { + if (previousScheduler === null) { + return; + } + + const currentProjectDocument = context.getProjectDocument(); + context.setProjectDocument({ + ...currentProjectDocument, + scheduler: cloneProjectScheduler(previousScheduler) + }); + } + }; +} diff --git a/src/document/scene-document.ts b/src/document/scene-document.ts index 9b47a948..b7a573fe 100644 --- a/src/document/scene-document.ts +++ b/src/document/scene-document.ts @@ -19,8 +19,13 @@ import { type ProjectTimeSettings } from "./project-time-settings"; import { type ScenePath } from "./paths"; +import { + createEmptyProjectScheduler, + type ProjectScheduler +} from "../scheduler/project-scheduler"; -export const SCENE_DOCUMENT_VERSION = 45 as const; +export const SCENE_DOCUMENT_VERSION = 46 as const; +export const PROJECT_SCHEDULER_FOUNDATION_SCENE_DOCUMENT_VERSION = 46 as const; export const CONTROL_SURFACE_FOUNDATION_SCENE_DOCUMENT_VERSION = 45 as const; export const NPC_PRESENCE_SCENE_DOCUMENT_VERSION = 44 as const; export const PATH_FOUNDATION_SCENE_DOCUMENT_VERSION = 43 as const; @@ -136,6 +141,7 @@ export interface ProjectDocument { version: typeof SCENE_DOCUMENT_VERSION; name: string; time: ProjectTimeSettings; + scheduler: ProjectScheduler; activeSceneId: string; scenes: Record; materials: Record; @@ -147,6 +153,7 @@ export interface SceneDocument { version: typeof SCENE_DOCUMENT_VERSION; name: string; time: ProjectTimeSettings; + scheduler: ProjectScheduler; world: WorldSettings; materials: Record; textures: Record; @@ -167,6 +174,7 @@ export function createEmptySceneDocument( version: SCENE_DOCUMENT_VERSION, name: overrides.name ?? "Untitled Scene", time: overrides.time ?? createDefaultProjectTimeSettings(), + scheduler: createEmptyProjectScheduler(), world: overrides.world ?? createDefaultWorldSettings(), materials: cloneMaterialRegistry( overrides.materials ?? createStarterMaterialRegistry() @@ -211,7 +219,13 @@ export function createEmptyProjectDocument( overrides: Partial< Pick< ProjectDocument, - "name" | "time" | "activeSceneId" | "materials" | "textures" | "assets" + | "name" + | "time" + | "scheduler" + | "activeSceneId" + | "materials" + | "textures" + | "assets" > > & { sceneId?: string; @@ -230,6 +244,7 @@ export function createEmptyProjectDocument( version: SCENE_DOCUMENT_VERSION, name: overrides.name ?? DEFAULT_PROJECT_NAME, time: overrides.time ?? createDefaultProjectTimeSettings(), + scheduler: overrides.scheduler ?? createEmptyProjectScheduler(), activeSceneId: initialScene.id, scenes: { [initialScene.id]: initialScene @@ -265,6 +280,7 @@ export function createSceneDocumentFromProject( version: projectDocument.version, name: scene.name, time: projectDocument.time, + scheduler: projectDocument.scheduler, world: scene.world, materials: projectDocument.materials, textures: projectDocument.textures, @@ -286,6 +302,7 @@ export function createProjectDocumentFromSceneDocument( version: SCENE_DOCUMENT_VERSION, name: projectName, time: sceneDocument.time, + scheduler: sceneDocument.scheduler, activeSceneId: sceneId, scenes: { [sceneId]: { @@ -318,6 +335,7 @@ export function applySceneDocumentToProject( ...projectDocument, version: SCENE_DOCUMENT_VERSION, time: sceneDocument.time, + scheduler: sceneDocument.scheduler, materials: sceneDocument.materials, textures: sceneDocument.textures, assets: sceneDocument.assets, diff --git a/src/entities/npc-actor-registry.ts b/src/entities/npc-actor-registry.ts index 6d0d98f5..3bd255f7 100644 --- a/src/entities/npc-actor-registry.ts +++ b/src/entities/npc-actor-registry.ts @@ -10,6 +10,12 @@ export interface NpcActorUsage { entityName?: string; } +export interface ProjectNpcActorRecord { + actorId: string; + label: string; + usages: NpcActorUsage[]; +} + export function listNpcActorUsages( projectDocument: ProjectDocument, actorId: string @@ -48,4 +54,52 @@ export function listNpcActorUsages( }); return usages; -} \ No newline at end of file +} + +export function listProjectNpcActors( + projectDocument: ProjectDocument +): ProjectNpcActorRecord[] { + const actorUsages = new Map(); + + for (const scene of Object.values(projectDocument.scenes)) { + for (const entity of getEntityInstances(scene.entities)) { + if (entity.kind !== "npc") { + continue; + } + + const usages = actorUsages.get(entity.actorId) ?? []; + usages.push({ + actorId: entity.actorId, + sceneId: scene.id, + sceneName: scene.name, + entityId: entity.id, + entityName: entity.name + }); + actorUsages.set(entity.actorId, usages); + } + } + + return [...actorUsages.entries()] + .map(([actorId, usages]) => { + usages.sort((left, right) => { + return ( + left.sceneName.localeCompare(right.sceneName) || + left.sceneId.localeCompare(right.sceneId) || + (left.entityName ?? "").localeCompare(right.entityName ?? "") || + left.entityId.localeCompare(right.entityId) + ); + }); + + return { + actorId, + label: usages[0]?.entityName?.trim() || actorId, + usages + }; + }) + .sort((left, right) => { + return ( + left.label.localeCompare(right.label) || + left.actorId.localeCompare(right.actorId) + ); + }); +}