Warning, /firebird/packages/firebird-core/src/painters/event-piece-painter.ts is written in an unsupported language. File is not indexed.
0001 import { Object3D } from "three";
0002 import { Signal, signal } from "@angular/core";
0003 import { EventPiece } from "../model/event-piece";
0004 import {disposeHierarchy} from '@dexvis/threejs-tree-editor';
0005
0006
0007 /** Define the type for the constructor of EventPiecePainter subclasses */
0008 export type PiecePainterConstructor =
0009 new (node: Object3D, piece: EventPiece, config?: PainterConfigView) => EventPiecePainter;
0010
0011 /**
0012 * One configurable knob of a painter, declared statically in `PainterMeta`.
0013 * The declaration drives the auto-rendered painter panel and the config keys
0014 * the knob is stored under (`painters.byPiece.<pieceName>.<key>`), so a knob
0015 * is settable from the UI, a URL, a server config file, and batch scripts
0016 * without painter code knowing any of those sources.
0017 */
0018 export interface PainterConfigDescriptor {
0019 /** The knob name. Must not be one of `RESERVED_PAINTER_KNOB_KEYS`. */
0020 key: string;
0021 default: unknown;
0022 label?: string;
0023 /** Enumerated choices; rendered as a picker. */
0024 options?: readonly unknown[];
0025 /** Numeric range; rendered as a slider. */
0026 min?: number;
0027 max?: number;
0028 step?: number;
0029 }
0030
0031 /**
0032 * Static painter self-description: what it paints, how UI names it, and what
0033 * its knobs are. Declared as `static meta` on painter classes; the painter
0034 * registry uses `forPieceTypes` and `id` (painter selection is the config key
0035 * `painters.byPiece.<pieceName>` holding an id from the registered painters).
0036 */
0037 export interface PainterMeta {
0038 id: string;
0039 forPieceTypes: string[];
0040 label?: string;
0041 configs?: PainterConfigDescriptor[];
0042 }
0043
0044 /**
0045 * The resolved configuration a painter instance reads its knobs through.
0046 * Values arrive resolved across all config sources; the painter neither knows
0047 * nor cares which source set them. Worker-safe: implementations are plain
0048 * objects over signals — no DI.
0049 */
0050 export interface PainterConfigView {
0051 /** Current value of a declared knob. */
0052 value<T>(key: string): T;
0053 /** Signal of a declared knob, for reactive bindings. */
0054 signal<T>(key: string): Signal<T>;
0055 }
0056
0057 /**
0058 * Knob keys no painter may declare:
0059 * - `visible` is the piece visibility toggle
0060 * (`painters.byPiece.<pieceName>.visible`). The display applies it to the
0061 * painter's root node; painter code never reads it.
0062 * - `time` would share its storage key with the timestamp that the config
0063 * storage keeps beside the painter-selection key
0064 * (`painters.byPiece.<pieceName>` is stored with `painters.byPiece.<pieceName>.time`).
0065 */
0066 export const RESERVED_PAINTER_KNOB_KEYS: readonly string[] = ['time', 'visible'];
0067
0068 /**
0069 * Throws when a painter's meta declares a reserved knob key. Painter
0070 * registration calls it, so the mistake surfaces when the painter is
0071 * registered instead of as a silently shared setting.
0072 */
0073 export function assertPainterKnobKeys(painterClass: PiecePainterConstructor): void {
0074 const meta = painterMetaOf(painterClass);
0075 for (const descriptor of meta?.configs ?? []) {
0076 if (RESERVED_PAINTER_KNOB_KEYS.includes(descriptor.key)) {
0077 throw new Error(`Painter '${painterIdOf(painterClass)}' declares the knob '${descriptor.key}', ` +
0078 `whose name is reserved (${RESERVED_PAINTER_KNOB_KEYS.join(', ')}). Rename the knob.`);
0079 }
0080 }
0081 }
0082
0083 /** The `meta` declaration of a painter class, when it has one. */
0084 export function painterMetaOf(painterClass: PiecePainterConstructor): PainterMeta | undefined {
0085 return (painterClass as unknown as { meta?: PainterMeta }).meta;
0086 }
0087
0088 /** The registry/config id of a painter class (meta id, class name fallback). */
0089 export function painterIdOf(painterClass: PiecePainterConstructor): string {
0090 return painterMetaOf(painterClass)?.id ?? painterClass.name;
0091 }
0092
0093 /**
0094 * A config view holding the declared defaults — what a painter gets in
0095 * contexts without a config system (workers, scripts, tests).
0096 */
0097 export function defaultPainterConfigView(meta?: PainterMeta): PainterConfigView {
0098 const signals = new Map<string, Signal<unknown>>();
0099 for (const descriptor of meta?.configs ?? []) {
0100 signals.set(descriptor.key, signal(descriptor.default));
0101 }
0102 const emptySignal = signal(undefined);
0103 return {
0104 value<T>(key: string): T {
0105 return (signals.get(key)?.() as T)!;
0106 },
0107 signal<T>(key: string): Signal<T> {
0108 return (signals.get(key) ?? emptySignal) as Signal<T>;
0109 },
0110 };
0111 }
0112
0113 /**
0114 * Identifies one entity inside one piece: the piece name (unique within the
0115 * event) plus the entity index (entity id ≡ index into the piece columns).
0116 */
0117 export interface EntityRef {
0118 pieceName: string;
0119 entityIndex: number;
0120 }
0121
0122 /**
0123 * Resolves a scene object (or any descendant of one) back to the entity it
0124 * was built from, by walking up to the nearest ancestor a painter stamped
0125 * with `registerEntityObject`. Returns null for objects that do not belong
0126 * to any painted entity (geometry, helpers).
0127 *
0128 * Batched painters draw MANY entities in one object, so a per-object stamp
0129 * cannot name the entity — they stamp `userData['entityIndexResolver']`, a
0130 * function mapping the raycast intersection (its picked segment/instance)
0131 * to the entity index. Pass the intersection through when there is one.
0132 */
0133 export function entityRefOf(object: Object3D, intersection?: unknown): EntityRef | null {
0134 for (let node: Object3D | null = object; node; node = node.parent) {
0135 const pieceName = node.userData?.["pieceName"];
0136 if (typeof pieceName !== "string") continue;
0137
0138 const resolver = node.userData?.["entityIndexResolver"];
0139 if (typeof resolver === "function" && intersection !== undefined) {
0140 const resolved = resolver(intersection);
0141 if (typeof resolved === "number") return { pieceName, entityIndex: resolved };
0142 }
0143 const entityIndex = node.userData?.["entityIndex"];
0144 if (typeof entityIndex === "number") {
0145 return { pieceName, entityIndex };
0146 }
0147 }
0148 return null;
0149 }
0150
0151 /**
0152 * Paints all primitives for a given EventPiece.
0153 *
0154 * Painters own the entity↔object mapping: as a painter builds scene objects
0155 * it registers them per entity index (a plain array — entity id ≡ index, no
0156 * lookup maps). The mapping powers selection both ways: a 3D pick resolves
0157 * to (piece, entityIndex) via `entityRefOf`, and a model-tree selection
0158 * resolves to scene objects via `objectForEntity`.
0159 */
0160 export abstract class EventPiecePainter {
0161
0162 /** Scene object per entity index; holes where no object was built. */
0163 protected entityObjects: (Object3D | null)[] = [];
0164
0165 /**
0166 * The painter's resolved knobs. Always usable: without an injected view the
0167 * declared defaults from `static meta` apply (workers, scripts, tests).
0168 */
0169 protected config: PainterConfigView;
0170
0171 /** Constructor is public since we can instantiate directly */
0172 constructor(protected parentNode: Object3D, protected piece: EventPiece, config?: PainterConfigView) {
0173 this.config = config ?? defaultPainterConfigView(painterMetaOf(this.constructor as PiecePainterConstructor));
0174 }
0175
0176 /** Gets the `type` identifier for the piece this class works with */
0177 public get pieceType() {
0178 return this.piece.type;
0179 }
0180
0181 /** The name of the painted piece (unique within its event). */
0182 public get pieceName() {
0183 return this.piece.name;
0184 }
0185
0186 /**
0187 * The scene group holding everything this painter builds. Piece-level
0188 * show/hide (the model tree eye, `painters.byPiece.<name>.visible`) flips
0189 * `visible` here instead of touching individual entity objects.
0190 */
0191 public get node(): Object3D {
0192 return this.parentNode;
0193 }
0194
0195 /**
0196 * Records the scene object of one entity and stamps it so `entityRefOf`
0197 * can resolve the object (or its mesh children) back to the entity.
0198 * Painters call this as they build objects.
0199 */
0200 protected registerEntityObject(entityIndex: number, object: Object3D): void {
0201 this.entityObjects[entityIndex] = object;
0202 object.userData["pieceName"] = this.piece.name;
0203 object.userData["entityIndex"] = entityIndex;
0204 }
0205
0206 /** The scene object built for an entity, or null when there is none. */
0207 public objectForEntity(entityIndex: number): Object3D | null {
0208 return this.entityObjects[entityIndex] ?? null;
0209 }
0210
0211 /**
0212 * Applies the highlight visual of one entity. The default runs the
0213 * `highlightFunction` the painter stored on the entity's object; painters
0214 * with special highlight needs override this instead.
0215 */
0216 public highlightEntity(entityIndex: number): void {
0217 const object = this.objectForEntity(entityIndex);
0218 const highlight = object?.userData["highlightFunction"];
0219 if (typeof highlight === "function") highlight();
0220 }
0221
0222 /** Reverts `highlightEntity` (default: the stored `unhighlightFunction`). */
0223 public unhighlightEntity(entityIndex: number): void {
0224 const object = this.objectForEntity(entityIndex);
0225 const unhighlight = object?.userData["unhighlightFunction"];
0226 if (typeof unhighlight === "function") unhighlight();
0227 }
0228
0229 /**
0230 * Called after any of the painter's config knobs changed. Painters restyle
0231 * their existing objects here (colors, widths) — a knob change must not
0232 * require a rebuild or a data reload.
0233 */
0234 onConfigChanged(): void {}
0235
0236 /**
0237 * Paints
0238 * @param time - time in [ns], null - draw in non-dynamic mode (all visible)
0239 */
0240 abstract paint(time: number | null): void;
0241
0242 /**
0243 * Frees what the painter built and removes its node from the scene. The
0244 * base disposes the geometries and materials of the node's whole subtree,
0245 * so a painter that only adds objects under its node needs no override.
0246 * Override to free resources held outside the node (shared materials,
0247 * textures) and call `super.dispose()`.
0248 */
0249 public dispose(): void {
0250 this.entityObjects = [];
0251 if (this.parentNode) {
0252 disposeHierarchy(this.parentNode);
0253 }
0254 }
0255 }