Warning, /firebird/packages/firebird-ng/src/lib/builtin-command-handlers.ts is written in an unsupported language. File is not indexed.
0001 /**
0002 * Built-in command handlers, registered through the same COMMAND_HANDLERS
0003 * token user extensions get. Each handler owns one command type and its
0004 * `?cmd=type:arg` URL grammar.
0005 */
0006
0007 import { Injectable, Injector, inject } from '@angular/core';
0008 import { AppCommand, CommandHandler, ConfigService, coerceConfigValue } from '@dexvis/app-features';
0009 import { injectCameraPresets } from '@dexvis/firebird-ng/api';
0010
0011 // Handlers live in the initial bundle (referenced from app.config), so the
0012 // display-stack services (three.js and friends) are resolved through DYNAMIC
0013 // imports at execute time: a static import would defeat route code-splitting.
0014
0015 /** Polls a condition (startup path only, never inside the frame loop). */
0016 async function waitFor(condition: () => boolean, timeoutMs: number, stepMs = 100): Promise<boolean> {
0017 const deadline = Date.now() + timeoutMs;
0018 while (Date.now() < deadline) {
0019 if (condition()) return true;
0020 await new Promise(resolve => setTimeout(resolve, stepMs));
0021 }
0022 return condition();
0023 }
0024
0025 /** `open-geometry`: load detector geometry: `{ type, url }` / `?cmd=open-geometry:URL`. */
0026 @Injectable()
0027 export class OpenGeometryCommandHandler implements CommandHandler {
0028 readonly type = 'open-geometry';
0029 private injector = inject(Injector);
0030
0031 fromUrlArg(arg: string): AppCommand {
0032 return { type: this.type, url: arg };
0033 }
0034
0035 /** Loads through the first geometry loader that claims the URL; resolves when it is on screen. */
0036 async execute(command: AppCommand): Promise<void> {
0037 const url = command['url'] as string;
0038 if (!url) throw new Error(`open-geometry: 'url' argument is required`);
0039 const { EventDisplayService } = await import('@dexvis/firebird-ng/display');
0040 await this.injector.get(EventDisplayService).openGeometry(url);
0041 }
0042 }
0043
0044 /** `open-dex`: load event data: `{ type, url }` / `?cmd=open-dex:URL` / `?dex=URL`. */
0045 @Injectable()
0046 export class OpenDexCommandHandler implements CommandHandler {
0047 readonly type = 'open-dex';
0048 private injector = inject(Injector);
0049
0050 fromUrlArg(arg: string): AppCommand {
0051 return { type: this.type, url: arg };
0052 }
0053
0054 /** Loads through the first event loader that claims the URL; resolves when the first event shows. */
0055 async execute(command: AppCommand): Promise<void> {
0056 const url = command['url'] as string;
0057 if (!url) throw new Error(`open-dex: 'url' argument is required`);
0058 const { EventDisplayService } = await import('@dexvis/firebird-ng/display');
0059 await this.injector.get(EventDisplayService).openEvents(url);
0060 }
0061 }
0062
0063 /** `show-event`: select entry by index: `{ type, index }` / `?event=N`. */
0064 @Injectable()
0065 export class ShowEventCommandHandler implements CommandHandler {
0066 readonly type = 'show-event';
0067 private injector = inject(Injector);
0068
0069 fromUrlArg(arg: string): AppCommand {
0070 return { type: this.type, index: parseInt(arg, 10) };
0071 }
0072
0073 async execute(command: AppCommand): Promise<void> {
0074 const index = Number(command['index']);
0075 if (isNaN(index)) throw new Error(`show-event: numeric 'index' argument is required`);
0076
0077 const { DataModelService } = await import('@dexvis/firebird-ng/display');
0078 const data = this.injector.get(DataModelService);
0079
0080 // Events may still be loading (config-driven autoload runs in parallel).
0081 const loaded = await waitFor(() => data.entries().length > 0, 30_000);
0082 if (!loaded) {
0083 throw new Error('show-event: no events were loaded within 30 s');
0084 }
0085 const entries = data.entries();
0086 if (index < 0 || index >= entries.length) {
0087 throw new Error(`show-event: index ${index} out of range (0..${entries.length - 1})`);
0088 }
0089 data.setCurrentEntry(entries[index]);
0090 }
0091 }
0092
0093 /**
0094 * `set-config`: set a config value: `{ type, key, value }` / `?cmd=set-config:key=value`.
0095 * URL/server/batch sources apply as SESSION values (never persisted: a link
0096 * or script cannot poison saved user preferences); ui/code sources persist.
0097 * A ui/code write to a key no code declared yet goes to a placeholder: the
0098 * owning code's later declaration still sets the key's default, type and
0099 * validator, and reads the written value with them.
0100 */
0101 @Injectable()
0102 export class SetConfigCommandHandler implements CommandHandler {
0103 readonly type = 'set-config';
0104 private config = inject(ConfigService);
0105
0106 fromUrlArg(arg: string): AppCommand {
0107 const eq = arg.indexOf('=');
0108 if (eq < 0) return { type: this.type, key: arg, value: '' };
0109 return { type: this.type, key: arg.substring(0, eq), value: arg.substring(eq + 1) };
0110 }
0111
0112 execute(command: AppCommand): void {
0113 const key = command['key'] as string;
0114 if (!key) throw new Error(`set-config: 'key' argument is required`);
0115 const value = command['value'];
0116 const transient = command.source === 'url' || command.source === 'server' || command.source === 'batch';
0117 if (transient) {
0118 this.config.applySessionValue(key, value);
0119 } else {
0120 const property = this.config.getConfigOrPlaceholder(key, value);
0121 // Text from a URL-style argument becomes the declared type ('7' -> 7)
0122 property.setValue(coerceConfigValue(value, property.codeDefault));
0123 }
0124 }
0125 }
0126
0127 /**
0128 * `camera-preset`: move the camera to a named preset: `?cmd=camera-preset:top`.
0129 * The presets come from `withCameraPreset()` registrations; the built-in
0130 * ones are the face views and `home` (see `withStandardCameraPresets()`).
0131 */
0132 @Injectable()
0133 export class CameraPresetCommandHandler implements CommandHandler {
0134 readonly type = 'camera-preset';
0135 private injector = inject(Injector);
0136 private presets = injectCameraPresets();
0137
0138 fromUrlArg(arg: string): AppCommand {
0139 return { type: this.type, name: arg };
0140 }
0141
0142 async execute(command: AppCommand): Promise<void> {
0143 const name = command['name'] as string;
0144 const preset = this.presets.find(candidate => candidate.name === name);
0145 if (!preset) {
0146 throw new Error(`camera-preset: unknown preset '${name}'. Known: ${this.presets.map(known => known.name).join(', ')}`);
0147 }
0148 const { ThreeService } = await import('@dexvis/firebird-ng/display');
0149 this.injector.get(ThreeService).applyCameraPreset(preset);
0150 }
0151 }
0152
0153 /**
0154 * `animate-collision`: play the collision intro (`withCollisionIntro()`),
0155 * then the event's time animation from the start: `?cmd=animate-collision`.
0156 * Without an intro the time animation starts at once.
0157 */
0158 @Injectable()
0159 export class AnimateCollisionCommandHandler implements CommandHandler {
0160 readonly type = 'animate-collision';
0161 private injector = inject(Injector);
0162
0163 async execute(): Promise<void> {
0164 const { EventDisplayService } = await import('@dexvis/firebird-ng/display');
0165 await this.injector.get(EventDisplayService).animateWithCollision();
0166 }
0167 }