Warning, /firebird/packages/firebird-ng/api/src/lib/geometry-pipeline.ts is written in an unsupported language. File is not indexed.
0001 /**
0002 * The extension seams of the geometry pipeline, in pipeline order:
0003 *
0004 * 1. Pre-build TGeo rules (`withRootGeometryRules`): serializable data,
0005 * posted with each load request to the geometry worker, which prunes and
0006 * edits the ROOT tree before jsroot builds three.js objects. The rules
0007 * travel as data because the worker is a prebuilt module and cannot
0008 * import pack modules.
0009 * 2. Themes (`withGeometryTheme`): rule sets that color, merge and outline
0010 * subdetectors on the main thread, selected by `geometry.themeName`.
0011 * 3. Post-processors (`withGeometryPostProcessor`): main-thread code that runs
0012 * once the geometry sits in the scene (scaled to mm), before the first
0013 * frame shows it. May be async (an environment map needs the renderer).
0014 *
0015 * Plain TypeScript with type-only three.js imports: initial-bundle code
0016 * (tokens, feature functions, the config page) imports this module.
0017 */
0018
0019 import type { Type } from '@angular/core';
0020 import type { Object3D, Plane, Scene } from 'three';
0021 import type { ClippingGroup, WebGPURenderer } from 'three/webgpu';
0022 import type { GeoNodeEditRule } from '@dexvis/root-geo-tree-editor';
0023 import type { DetectorThreeRuleSet } from '@dexvis/threejs-tree-editor';
0024 import { resolveRegistry } from '@dexvis/app-features';
0025
0026 // ---------------------------------------------------------------------------
0027 // Pre-build TGeo rules
0028 // ---------------------------------------------------------------------------
0029
0030 /**
0031 * Edit rules for one detector. The rules apply, one at a time and in order,
0032 * to every top-level TGeo node whose path matches the wildcard
0033 * `namePattern`. A top-level path is the world volume name, a slash and the
0034 * node name ('Default/Tracker_14'), so patterns start with a `*` segment.
0035 */
0036 export interface DetectorEditRules {
0037 namePattern: string;
0038 editRules: GeoNodeEditRule[];
0039 }
0040
0041 /** A named set of detector edit rules; config `geometry.rootFilterName` selects one. */
0042 export interface RootGeometryEditRuleSet {
0043 /** Option text on the config page. */
0044 label?: string;
0045 detectors: DetectorEditRules[];
0046 }
0047
0048 /** A named list of top-level detectors removed before the build; config `geometry.cutListName` selects one. */
0049 export interface RootGeometryCutList {
0050 /** Option text on the config page. */
0051 label?: string;
0052 /** Name prefixes of top-level TGeo nodes to remove ('Beam' removes 'BeamMagnet_32'). */
0053 remove: string[];
0054 }
0055
0056 /**
0057 * The edit rule set name that the data selector's "Optimize geometry" toggle
0058 * selects. The toggle shows only when a contribution defines this name, with
0059 * the rule set's label as its hint.
0060 */
0061 export const OPTIMIZE_EDIT_RULE_SET = 'default';
0062
0063 /** What `withRootGeometryRules()` contributes. Plain data: it is posted to the geometry worker. */
0064 export interface RootGeometryRules {
0065 /** Edit rule sets by name. `OPTIMIZE_EDIT_RULE_SET` ('default') is the one the data selector offers. */
0066 editRules?: Record<string, RootGeometryEditRuleSet>;
0067 /** Cut lists by name. */
0068 cutLists?: Record<string, RootGeometryCutList>;
0069 }
0070
0071 /** Rules as data, or a dynamic import that resolves to them (keeps large rule data out of the initial bundle). */
0072 export type RootGeometryRulesSource = RootGeometryRules | (() => Promise<RootGeometryRules>);
0073
0074 /** What one geometry load asks the worker to do, resolved from the config names. */
0075 export interface RootGeometryLoadRules {
0076 /** Name prefixes of top-level nodes to remove; empty removes nothing. */
0077 cutList: string[];
0078 /** Detector edit rules to apply; empty edits nothing. */
0079 editRules: DetectorEditRules[];
0080 }
0081
0082 /**
0083 * Merges rule contributions in registration order. A later rule set or cut
0084 * list with the name of an earlier one replaces it (the registry rule of
0085 * every Firebird extension point).
0086 */
0087 export async function resolveRootGeometryRules(sources: readonly RootGeometryRulesSource[]): Promise<RootGeometryRules> {
0088 const resolved = await Promise.all(sources.map(source => typeof source === 'function' ? source() : source));
0089 const merged: Required<RootGeometryRules> = { editRules: {}, cutLists: {} };
0090 for (const rules of resolved) {
0091 Object.assign(merged.editRules, rules.editRules);
0092 Object.assign(merged.cutLists, rules.cutLists);
0093 }
0094 return merged;
0095 }
0096
0097 /**
0098 * Picks what one load applies: the edit rule set and cut list named by the
0099 * config. 'off', and a name no contribution defines, select nothing.
0100 */
0101 export function selectRootGeometryLoadRules(rules: RootGeometryRules, editRulesName: string, cutListName: string): RootGeometryLoadRules {
0102 return {
0103 editRules: rules.editRules?.[editRulesName]?.detectors ?? [],
0104 cutList: rules.cutLists?.[cutListName]?.remove ?? [],
0105 };
0106 }
0107
0108 // ---------------------------------------------------------------------------
0109 // Themes
0110 // ---------------------------------------------------------------------------
0111
0112 /** One geometry theme: subdetector rule sets selected by config `geometry.themeName`. */
0113 export interface GeometryThemeRegistration {
0114 /** The `geometry.themeName` value that selects this theme. 'off' is reserved: no theme. */
0115 id: string;
0116 /** Option text on the config page. */
0117 label?: string;
0118 /** Resolves the rule sets. A dynamic import: rule sets carry three.js materials. */
0119 load: () => Promise<readonly DetectorThreeRuleSet[]>;
0120 }
0121
0122 // ---------------------------------------------------------------------------
0123 // Post-processors
0124 // ---------------------------------------------------------------------------
0125
0126 /** What a geometry post-processor works on. */
0127 export interface GeometryPostProcessContext {
0128 /** The loaded geometry's root object, already inside `sceneGeometry`. */
0129 geometry: Object3D;
0130 /**
0131 * The detector geometry container, scaled from ROOT cm to mm. Objects added
0132 * here render and clip like the geometry itself.
0133 */
0134 sceneGeometry: ClippingGroup;
0135 scene: Scene;
0136 /** For GPU work such as environment maps (PMREM). */
0137 renderer: WebGPURenderer;
0138 /** The main view's wedge clipping planes, for materials a processor creates. */
0139 clippingPlanes: Plane[];
0140 /** True when the user chose fast opaque materials (`geometry.FastDefaultMaterial`): skip costly effects. */
0141 fastMaterials: boolean;
0142 }
0143
0144 /**
0145 * Main-thread code that adjusts a freshly loaded geometry: materials,
0146 * arrangement, helpers. Instantiated through a child injector, so `inject()`
0147 * works in its constructor.
0148 */
0149 export interface GeometryPostProcessor {
0150 process(context: GeometryPostProcessContext): void | Promise<void>;
0151 }
0152
0153 /** One post-processor registration. */
0154 export interface GeometryPostProcessorRegistration {
0155 /** Identity: a later registration with this id replaces this one. */
0156 id: string;
0157 /** Ids of post-processors that must run before this one. Ids nobody registered are ignored. */
0158 after?: string[];
0159 /** Resolves the class. A dynamic import: post-processors usually pull three.js. */
0160 load: () => Promise<Type<GeometryPostProcessor>>;
0161 }
0162
0163 /**
0164 * The run order of post-processors: registration order, except that each
0165 * runs after the ids in its `after`. Same-id registrations replace earlier
0166 * ones in place first.
0167 *
0168 * @throws Error naming the processors when the `after` constraints form a cycle.
0169 */
0170 export function orderGeometryPostProcessors(
0171 registrations: readonly GeometryPostProcessorRegistration[],
0172 ): GeometryPostProcessorRegistration[] {
0173 const pending = resolveRegistry(registrations, registration => registration.id);
0174 const known = new Set(pending.map(registration => registration.id));
0175 const ordered: GeometryPostProcessorRegistration[] = [];
0176 const placed = new Set<string>();
0177 while (pending.length > 0) {
0178 // The first registration whose predecessors are all placed goes next
0179 const index = pending.findIndex(registration =>
0180 (registration.after ?? []).every(id => placed.has(id) || !known.has(id)));
0181 if (index < 0) {
0182 throw new Error(`Geometry post-processors wait for each other: ${pending.map(registration => registration.id).join(', ')}`);
0183 }
0184 const [next] = pending.splice(index, 1);
0185 ordered.push(next);
0186 placed.add(next.id);
0187 }
0188 return ordered;
0189 }