Warning, /firebird/packages/firebird-ng/README.md is written in an unsupported language. File is not indexed.
0001 # @dexvis/firebird-ng
0002
0003 The Angular library of the Firebird event display: `provideFirebird()` and
0004 its `with*()` features, the display pages and `<firebird-display>`, the
0005 display services, and the code of the geometry and ROOT file web workers.
0006 An application (Firebird's own or an external experiment's) assembles its
0007 event display by composing features:
0008
0009 ```ts
0010 // app.config.ts: provideFirebird() installs Firebird's built-ins first
0011 provideFirebird(
0012 withUrlAlias('data://', 'https://my.host/data/'),
0013
0014 // an experiment's contributions:
0015 withEventPiece(CherenkovRingPieceFactory), // DEX type decoder (worker-safe, @dexvis/firebird-core)
0016 withPainter(CherenkovRingPainter, { forPieceType: 'example.CherenkovRing' }),
0017 withThreeExtension(HoverInfoExtension), // machinery hook (scene/frame/input)
0018 withGeometryLoader(IgesGeometryLoader), // teach Firebird a file format
0019 withCommandHandler(MyCommandHandler), // extend the command vocabulary
0020 withDefaultGeometry('data://detector.root'), // sugar for withConfigDefaults({'geometry.selectedGeometry': ...})
0021 withDataCatalog({ entries: [...], facets: [...] }), // datasets for the data selector (presets, physics tags)
0022 withDataSelectorTab({ id: 'campaigns', label: 'Campaigns', load: () => import('./campaigns-tab') }), // a selector tab
0023 withRootGeometryRules(() => import('./rules').then(m => m.rules)), // pre-build TGeo edits and cut lists (data)
0024 withGeometryTheme({ id: 'my-colors', load: () => import('./theme').then(m => m.ruleSets) }),
0025 withGeometryPostProcessor({ id: 'my.mirrors', load: () => import('./mirrors').then(m => m.Mirrors) }),
0026 withCameraPreset({ name: 'home', position: [0, 7000, 0], target: [0, 0, 0] }), // pins the start view
0027 withCollisionIntro(() => import('./intro').then(m => m.BeamsIntro)),
0028 withToolbarAction({ id: 'intro', icon: 'close_fullscreen', label: 'Intro', command: { type: 'animate-collision' } }),
0029 withoutFeatures('firebird.navigation-cube'), // drop a built-in part by feature id
0030 )
0031 ```
0032
0033 One contribution per `with*()` call. Packs compose with
0034 `firebirdPack(id, ...)` (a replaceable, removable unit with an id) or
0035 `firebirdFeatures(...)` (no id): an experiment pack is one function returning
0036 a composed feature. See `@dexvis/firebird-epic` (`packages/epic/`, the ePIC
0037 pack the flagship installs), `src/lib/with-firebird-builtins.ts` (the
0038 built-ins as named sub-features, with their feature ids), and
0039 `@dexvis/firebird-example-extension` for a complete out-of-tree example with
0040 its own piece type and painter.
0041
0042 ## Setting up an application
0043
0044 The 0.1.0 betas publish under the npm dist-tag `next`. Install the library
0045 with its peers into an Angular 22 application:
0046
0047 ```bash
0048 npm install @dexvis/firebird-ng@next @dexvis/firebird-core@next @dexvis/app-features @dexvis/shell \
0049 @angular/material @angular/cdk @angular/aria @angular/forms @angular/router three jsroot
0050 ```
0051
0052 ```ts
0053 // app.config.ts
0054 import { provideFirebird, withAppVersion, withWorkers } from '@dexvis/firebird-ng';
0055 import { version } from '../../package.json';
0056
0057 export const appConfig: ApplicationConfig = {
0058 providers: [
0059 provideZonelessChangeDetection(),
0060 provideRouter(routes),
0061 provideHttpClient(withFetch()),
0062 provideFirebird(
0063 withAppVersion(version),
0064 withWorkers({
0065 geometry: () => new Worker(new URL('./workers/geometry.worker', import.meta.url), { type: 'module' }),
0066 rootFile: () => new Worker(new URL('./workers/root-file.worker', import.meta.url), { type: 'module' }),
0067 }),
0068 withMyExperiment(),
0069 ),
0070 ],
0071 };
0072
0073 // app.routes.ts: the pages load lazily from the display entry
0074 import { firebirdRoutes } from '@dexvis/firebird-ng';
0075
0076 export const routes: Routes = [
0077 { path: '', redirectTo: '/display', pathMatch: 'full' },
0078 ...firebirdRoutes(), // display, split-window, config
0079 ];
0080 ```
0081
0082 - **Workers.** The application owns the two worker entry modules, so that its
0083 build bundles them (a `new Worker(new URL(...))` inside a prebuilt library
0084 is not bundled). Each entry is one call: `runGeometryWorker(self)` from
0085 `@dexvis/firebird-ng/workers/geometry`, `runRootFileWorker(self)` from
0086 `@dexvis/firebird-ng/workers/root-file`. Without `withWorkers()`, ROOT
0087 geometry and in-browser ROOT conversion fail with a message naming it.
0088 - **angular.json.** The geometry worker bundles jsroot's geometry painter,
0089 which can import node-only renderers: add
0090 `"externalDependencies": ["@resvg/resvg-js", "canvas"]` to the build
0091 options. jsroot and the display entry import CommonJS modules; to silence
0092 the build's CommonJS warnings, add
0093 `"allowedCommonJsDependencies": ["jszip", "@oneidentity/zstd-js", "tmp", "jsdom", "mathjax"]`.
0094 - **index.html.** The toolbars use Material Icons ligatures: load the font,
0095 for example
0096 `<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">`.
0097 Without it the buttons show the icon names as text.
0098 - **Server config.** At startup the application fetches
0099 `assets/config.jsonc`, the server tier of the config (see Configs).
0100 Without the file the defaults apply and the console shows one
0101 "Failed to load config" error; an empty object (`{}`) is a valid file.
0102 - **Your own page.** `<firebird-display>` (from `@dexvis/firebird-ng/display`)
0103 is the canvas alone: it attaches the display to its element, follows the
0104 element's size, loads the configured geometry and events and runs the
0105 startup commands; `(attached)` tells when the scene exists.
0106
0107 ## Entry points
0108
0109 | Import | Holds | Load |
0110 |--------|-------|------|
0111 | `@dexvis/firebird-ng` | `provideFirebird()`, the built-ins, `firebirdRoutes()`, and everything in `/api` | In the initial bundle: stays light |
0112 | `@dexvis/firebird-ng/api` | Tokens, `with*()` features, ThreeExtension and render view contracts, config keys, the light services | Re-exported by the root; the display entry builds on it |
0113 | `@dexvis/firebird-ng/display` | Pages, `<firebird-display>`, components, display services | Lazily only (routes, `import()`): pulls three.js and Material |
0114 | `@dexvis/firebird-ng/workers/geometry`, `/workers/root-file` | The worker code and its messages | From the application's worker entry modules |
0115 | `@dexvis/firebird-ng/geometry-palette` | Named colors for geometry themes | From lazily loaded theme rule sets |
0116
0117 The root reaches the display entry through dynamic imports only, and the
0118 display entry imports `/api`: an entry may not import an entry that imports
0119 it, so the contracts both need live in `/api`. Each worker has its own entry
0120 because a worker bundle must hold only its own code: one shared entry made
0121 the ROOT file worker carry jsroot's geometry painter (1.17 MB instead of
0122 116 kB).
0123
0124 ## Precedence
0125
0126 One rule for every registry: contributions keep registration order (the
0127 `provideFirebird()` arguments, depth first, after the built-ins); a later
0128 contribution with the id of an earlier one replaces it at the earlier one's
0129 position; where one entry is picked among several, the earliest wins (the
0130 first loader that claims a source, the first painter registered for a piece
0131 type). The ids: feature ids, `DataLoaderMeta.id`, `CommandHandler.type`,
0132 `EventPieceFactory.type`, `PainterMeta.id`, and the `id`/`name` of tabs,
0133 themes, post-processors, camera presets and toolbar actions (`tokens.ts`).
0134 Registries whose ids are only known at runtime apply `resolveRegistry()`
0135 from `@dexvis/app-features`; read loaders through `injectEventLoaders()` /
0136 `injectGeometryLoaders()`, which apply the id rule the raw tokens skip.
0137
0138 ## What lives where
0139
0140 The composition mechanism is the generic `@dexvis/app-features` package
0141 (`dexvis/app-features-ng/`): the feature type and `appFeatures()`
0142 composition (`FirebirdFeature` and `firebirdFeatures()` are the same things
0143 under Firebird names), `contributeValue()`/`contributeClass()`, the config
0144 registry (`ConfigService`, `ConfigProperty`), server config loading
0145 (`ServerConfigService`), the command bus and URL startup parsing, and the
0146 generic features `withConfigDefaults`, `withCommandHandler`, `withUrlAlias`,
0147 `withUrlShorthand` and `withoutFeatures`, feature identity
0148 (`appFeaturePack()`, the `kind`/`id` of every feature) and
0149 `resolveRegistry()`. `provideFirebird()` (`src/lib/provide-firebird.ts`) is
0150 `provideAppFeatures()` plus the built-ins (`withFirebirdBuiltins()`, which
0151 include Firebird's server config defaults from
0152 `api/src/lib/services/server-config.ts` and its URL shorthands,
0153 `withFirebirdUrlShorthands()`) and event piece factory registration.
0154
0155 This package keeps what is Firebird's:
0156
0157 - `api/src/lib/`: the event display tokens (`tokens.ts`) and their `with*()`
0158 functions (`firebird-features.ts`), the geometry pipeline seams
0159 (`geometry-pipeline.ts`), `ThreeExtension` (`three-extension.ts`), the
0160 render view contracts and layer constants (`views.ts`), the config keys,
0161 `BatchStatusService` (`window.firebird`), and the light services
0162 (`services/`: data catalog, data selection, URL resolution).
0163 - `src/lib/`: `provideFirebird()`, the built-ins, the built-in loaders and
0164 command handlers, `firebirdRoutes()`.
0165 - `display/src/lib/`: the services that run the display (`services/`), the
0166 components and pages, the navigation cube extension (`extensions/`).
0167 - `workers/geometry/`, `workers/root-file/`: the worker code.
0168 - `geometry-palette/`: named colors for geometry themes; a subpath of its own
0169 because everything the package root exports lands in the initial bundle.
0170
0171 The package root re-exports the generic parts, so extensions import
0172 everything from `@dexvis/firebird-ng`.
0173
0174 ## Painter or ThreeExtension?
0175
0176 If it turns **data** (event pieces, fields, geometry) into visuals, it is a
0177 **painter** — time-aware, per-piece, config-driven. If it hooks the
0178 **machinery** — scene lifecycle, frame loop, input, UI — it is a
0179 **ThreeExtension**. Field lines visualizing a field map are a painter, not an
0180 extension.
0181
0182 ## ThreeExtension lifecycle
0183
0184 - `onSceneInit(ctx)` runs strictly AFTER the async renderer init resolved.
0185 Never touch the scene before it. Attach pointer listeners to a view's
0186 container (`ctx.mainView.container`), not to `ctx.canvas`: on multi-view
0187 pages the view containers sit above the canvas and receive the events (an
0188 `AbortController` is the documented cleanup idiom).
0189 - `onFrame(ctx)` runs inside the render loop, on rendered frames. Keep it
0190 cheap: no allocation. One that throws is logged and not called again; the
0191 loop continues.
0192 - `onEventLoaded(event)` fires once for every event that becomes the painted
0193 one: the first event of each load, and each switch to another event.
0194 - `onDispose()` runs once at application teardown — remove listeners and
0195 objects. Leaving a display page does not call it: the scene and the
0196 extensions outlive pages, and the loop only stops until the next display
0197 page attaches.
0198 - Heavy extensions register with `withLazyThreeExtension(() => import(...))` —
0199 they load in their own chunk after the scene is up, and receive the
0200 `onSceneInit` call they missed.
0201
0202 ## Render views and overlays
0203
0204 The scene renders through **render views** (`ctx.views`, `ctx.mainView` on
0205 `SceneContext`; the `RenderView` interface). `ThreeService` keeps the one
0206 Scene and the one render loop; each view owns its DOM container, cameras,
0207 OrbitControls, picking and viewport rectangle inside the shared canvas. The
0208 main display is `views[0]`; the quad-projection page adds three orthographic
0209 views over the same scene.
0210
0211 - **Add a view**: `ctx.addView({ name, container, orthographic,
0212 fixedDirection })`: the container must sit above the shared canvas (see
0213 `display/src/lib/pages/split-window` for the reference layout). Remove it
0214 with `ctx.removeView(view)` when your page or panel goes away.
0215 - **A view with its own geometry cut** (experimental): create the slice with
0216 `ctx.createGeometrySlice()` and pass `geometrySlice` and `clipPlane` to
0217 `addView`; the view then renders the slice copy (its camera gets
0218 `GEOMETRY_SLICE_LAYER`). Edits to the original geometry reach the copy
0219 only through `ctx.rebuildGeometrySlice()`.
0220 - **Draw on top of a view**: `view.addOverlay({ render, onViewResize,
0221 onViewContainerChange, dispose })`. `render(view)` runs every frame after
0222 the view's scene render; set your own viewport/scissor from
0223 `view.viewportRect` and restore what you change. The navigation cube
0224 (`display/src/lib/extensions/viewport-gizmo.extension.ts`) is the built-in
0225 consumer of this seam.
0226 - **Add objects to the event data**: `ctx.addEventObject(object)` puts them
0227 under `sceneEvent` on `EVENT_DATA_LAYER`, so every view draws them and
0228 tracks-on-top views draw them over the geometry. The layer constants
0229 (`GEOMETRY_MAIN_LAYER`, `GEOMETRY_SLICE_LAYER`, `EVENT_DATA_LAYER`) are
0230 exported.
0231 - Cameras and `camera.up` handling are per view. When you change a view's up
0232 vector, use `view.setCameraUp()`: OrbitControls captures the up axis at
0233 construction and must be resynced.
0234
0235 ## Rendering rules (read before writing onFrame)
0236
0237 1. **No per-frame polling as change propagation.** State changes travel
0238 through signals and effects; `onFrame` is for animation only. The loop
0239 renders ON DEMAND by default (config `rendering.mode`), so code that
0240 polls app state every frame does not merely waste cycles — it never runs
0241 while the display is idle.
0242 2. **Changed something renderable? Call `ctx.invalidate()`.** The next
0243 animation frame renders. Without it, your mutation appears only when
0244 something else triggers a render. For an animation: seed one
0245 `invalidate()` when it starts and call it again from every update — the
0246 chain ends by itself when the animation stops updating. `onFrame` runs
0247 only on frames that render; its `deltaTime` is the time since the
0248 previous rendered frame.
0249 3. **The render loop lives in exactly one place** (`ThreeService.renderLoop`).
0250 Do not start your own rAF chain against the same scene; use `onFrame`.
0251
0252 ## Bundle discipline
0253
0254 Anything referenced from `app.config.ts` lands in the initial bundle. Painter
0255 classes pull three.js material code: register built-in-style painters with
0256 `withLazyPainter(type, () => import(...))`. Loaders and command handlers that
0257 need display services load `@dexvis/firebird-ng/display` with a dynamic
0258 `import()` inside the method that needs them (see `src/lib/builtin-loaders.ts`
0259 for the pattern). A pack may inject `EventDisplayService`, `DataModelService`
0260 and `SelectionService` from the display entry that way, or statically in its
0261 own lazily loaded code.
0262
0263 ## Loaders
0264
0265 A loader returns data; the display shows it.
0266
0267 - `canLoad(source)` claims a source by its name (extension, scheme); the
0268 first registered loader that claims it loads it. A `.root` file is
0269 ambiguous (geometry or events): the data selector probes it and asks
0270 `canLoadContent(probe)` to pick the field, geometry or events.
0271 - `load(source, context)` (geometry) resolves to the geometry root, in the
0272 loader's `millimetersPerUnit` (default 1; ROOT TGeo: 10, centimeters).
0273 `loadEvents(source, context)` (events) resolves to a `DataExchange`.
0274 - `context.resolveUrl(url)` turns aliases (`asset://`, a pack's alias) and
0275 server paths into a URL `fetch()` reads. `context.signal` aborts when a
0276 newer load of the same kind starts: stop and reject with its reason.
0277 - Reject with an `Error` whose message says what went wrong (HTTP status,
0278 unsupported version): the display shows it to the user and lists it in
0279 `window.firebird.errors`.
0280
0281 `EventDisplayService.openGeometry(source)` and `openEvents(source)` run this
0282 for every source: the config keys, picked files, the data selector, the
0283 `open-geometry`/`open-dex` commands. They resolve to null when a newer load
0284 replaced theirs.
0285
0286 ## Configs
0287
0288 Declare configs through `ConfigService.declare({ key, default, label, ... })`.
0289 Every declared key obeys the source precedence
0290 `defaults < server(config.jsonc) < localStorage < URL (?config.key=value) < runtime`,
0291 where URL values win for the session without being persisted. Read typed
0292 server config fields through `ServerConfigService<ServerConfig>`
0293 (`inject<ServerConfigService<ServerConfig>>(ServerConfigService)`).
0294
0295 - Declare each key from ONE exported schema object and import it wherever
0296 the key is read or written. The first declaration decides the default and
0297 the validator; in development builds a later declaration with a different
0298 default or validator logs a `[ConfigService]` warning. Firebird's own keys
0299 live in `config-keys.ts` (`ROOT_EVENT_RANGE_CONFIG`, `GEOMETRY_URL_CONFIG`,
0300 ...), exported from this package.
0301 - Give URL-valued keys the `isPersistableUrl` validator: it rejects `blob:`
0302 and `data:` URLs from every source, including values an older build saved.
0303 - A reset (`property.setDefault()`) removes the saved value and the URL
0304 override; the value falls back to the server value or the default.
0305
0306 ## Painter metadata and knobs
0307
0308 A painter declares itself with `static meta: PainterMeta`:
0309
0310 ```ts
0311 export class TrajectoryPainter extends EventPiecePainter {
0312 static meta: PainterMeta = {
0313 id: 'trajectory-lines',
0314 forPieceTypes: ['PointTrajectory'],
0315 label: 'Trajectory lines',
0316 configs: [
0317 { key: 'colorMode', default: 'pid', options: ['pid', 'momentum', 'solid'], label: 'Coloring' },
0318 { key: 'lineWidth', default: 30, min: 1, max: 300, label: 'Line width [mm]' },
0319 ],
0320 };
0321 }
0322 ```
0323
0324 - **Selection**: several painters may register for one piece type; the config
0325 key `painters.byPiece.<pieceName>` picks which one paints each piece: from
0326 the panel, a yaml file, or a URL. The default is the first painter in
0327 registration order, whatever order lazy painter chunks load in.
0328 - **Knobs** live under `painters.byPiece.<pieceName>.<key>` with normal config
0329 precedence. The right-pane painter panel auto-renders them from the meta —
0330 no per-painter UI code. The knob names `visible` and `time` are reserved
0331 (`RESERVED_PAINTER_KNOB_KEYS`); registering a painter that declares one
0332 throws.
0333 - The instance reads knobs through `this.config` (a `PainterConfigView`) and
0334 restyles existing objects in `onConfigChanged()` — knob changes must apply
0335 live, never require a rebuild. Without a config system (workers, scripts)
0336 the declared defaults apply; painter code stays DI-free.
0337
0338 ## Selection
0339
0340 Painters register their scene objects per entity as they build
0341 (`registerEntityObject(index, object)`), which powers `SelectionService`:
0342 a 3D click resolves the picked object back to `(pieceName, entityIndex)`,
0343 and a model-tree click resolves the entity to its objects for highlighting
0344 (`highlightEntity`/`unhighlightEntity`). Pieces describe their entities to
0345 the model tree via `entityCount`, `entityLabel(i)` and `entityRefs(i)` —
0346 override them in custom piece types to get meaningful labels and navigable
0347 reference links for free.
0348
0349 ## Commands
0350
0351 Serializable commands drive the display from URLs, server config and batch:
0352 `?dex=<url>&event=2`, `?cmd=type:arg;type:arg`, config.jsonc
0353 `startupCommands`, `pyrobird screenshot --commands "..."`. Add your own with
0354 `withCommandHandler` — implement `type`, `execute(cmd)`, and optionally
0355 `fromUrlArg(arg)` for the URL grammar. A URL shorthand
0356 (`withUrlShorthand('dex', 'open-dex')`) is an alias for `?cmd=<type>:<value>`,
0357 so `fromUrlArg` builds the shorthand's command too. Batch tools await
0358 `window.firebird.ready` (geometry loaded, startup commands done).