forked from HSF/phoenix
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patheffects-manager.ts
More file actions
464 lines (414 loc) · 14.5 KB
/
Copy patheffects-manager.ts
File metadata and controls
464 lines (414 loc) · 14.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import {
Camera,
Scene,
WebGLRenderer,
Vector2,
ShaderMaterial,
Mesh,
EdgesGeometry,
LineSegments,
NormalBlending,
} from 'three';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass.js';
import { Pass } from 'three/examples/jsm/postprocessing/Pass.js';
/**
* Manager for managing three.js event display effects like outline pass and unreal bloom.
*
* Selection uses OutlinePass for true silhouette outlines (boundary only, no internal
* mesh edges). This addresses the feedback that EdgesGeometry showed too many
* internal edges, especially on jets where all cone triangles were visible.
*
* Hover uses EdgesGeometry (15° threshold) for lightweight, immediate feedback.
*
* Selection color defaults to amber but is configurable via setSelectionColor()
* for experiments like LHCb where amber-colored objects are common.
*/
/**
* Represents the possible visual states of objects managed
* by EffectsManager. Provides typed state management instead
* of implicit boolean checks scattered across the render path.
*/
export enum EffectsState {
/** Default state of the object. */
DEFAULT = 'DEFAULT',
/** State when the object is being hovered over. */
HOVERED = 'HOVERED',
/** State when the object is selected. */
SELECTED = 'SELECTED',
/** State when the object is highlighted. */
HIGHLIGHTED = 'HIGHLIGHTED',
/** State when the object is dimmed/faded out. */
DIMMED = 'DIMMED',
}
/**
* Manager for managing three.js event display effects.
*/
export class EffectsManager {
/** Effect composer for effect passes. */
public composer: EffectComposer;
/** The camera inside the scene. */
private camera: Camera;
/** The default scene used for event display. */
private scene: Scene;
/** Render pass for rendering the default scene. */
private defaultRenderPass: RenderPass;
/**
* Array of outline passes that require camera updates during rendering.
*/
private outlinePasses: OutlinePass[] = [];
/**
* Indicates whether antialiasing is enabled for rendering.
*/
public antialiasing: boolean = true;
/**
* WebGL renderer instance used for rendering the scene.
*/
private renderer: WebGLRenderer;
/**
* Set of currently selected objects.
*/
private selectedObjectsSet: Set<Mesh> = new Set();
/**
* OutlinePass used for selection silhouette rendering.
* Lazily initialized on first selection.
*/
private selectionOutlinePass: OutlinePass | null = null;
/**
* Currently active hover outline object.
*/
private hoverOutline: LineSegments | null = null;
/**
* Reference to the hovered object for cleanup.
*/
private hoverTarget: Mesh | null = null;
/**
* Render function used to draw the scene.
* Switches between normal render and effects render based on antialiasing.
* @param scene The scene to render.
* @param camera The camera used for rendering.
*/
public render: (scene: Scene, camera: Camera) => void;
/**
* Vertex shader used for hover outline rendering.
* Handles projection transformation of vertices.
*/
private static readonly VERTEX_SHADER = `
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
/**
* Fragment shader used for hover outlines.
* Produces a blue outline with adjustable opacity.
*/
private static readonly HOVER_FRAGMENT_SHADER = `
uniform float opacity;
void main() {
vec3 color = vec3(0.2, 0.6, 1.0);
gl_FragColor = vec4(color, opacity);
}
`;
/**
* Constructor for the effects manager.
* @param camera The camera inside the scene.
* @param scene The default scene used for event display.
* @param renderer The main renderer used by the event display.
*/
constructor(camera: Camera, scene: Scene, renderer: WebGLRenderer) {
this.composer = new EffectComposer(renderer);
this.camera = camera;
this.scene = scene;
this.renderer = renderer;
this.defaultRenderPass = new RenderPass(this.scene, this.camera);
this.composer.addPass(this.defaultRenderPass);
// Set the starting render function
this.render = this.antialiasing ? this.antialiasRender : this.effectsRender;
}
/**
* Lazily initialize the selection OutlinePass on first use.
* Keeps the composer clean until selection is needed, preserving
* existing tests that check composer.passes.length.
* @returns The selection OutlinePass instance.
*/
private ensureSelectionPass(): OutlinePass {
if (!this.selectionOutlinePass) {
this.selectionOutlinePass = new OutlinePass(
new Vector2(window.innerWidth, window.innerHeight),
this.scene,
this.camera,
);
this.selectionOutlinePass.visibleEdgeColor.set(0xffcc44); // bright amber
this.selectionOutlinePass.hiddenEdgeColor.set(0x190a05);
this.selectionOutlinePass.edgeGlow = 0; // no glow bleeding onto neighbors
this.selectionOutlinePass.edgeThickness = 1; // tight silhouette line
this.selectionOutlinePass.edgeStrength = 3;
this.selectionOutlinePass.pulsePeriod = 0; // we handle pulsing manually
this.selectionOutlinePass.enabled = false;
this.composer.addPass(this.selectionOutlinePass);
this.outlinePasses.push(this.selectionOutlinePass);
}
return this.selectionOutlinePass;
}
/**
* Render the effects composer with outline support.
* Called when antialiasing is off (selection mode).
* @param scene The default scene used for event display.
* @param camera The camera inside the scene.
*/
private effectsRender(scene: Scene, camera: Camera) {
if (this.composer) {
this.defaultRenderPass.camera = camera;
this.defaultRenderPass.scene = scene;
for (const outlinePass of this.outlinePasses) {
outlinePass.renderCamera = camera;
}
this.updateSelectionPulse();
this.composer.render();
}
}
/**
* Render for antialias without the effects composer.
* Falls back to composer if there are active selections (OutlinePass needs it).
* @param scene The default scene used for event display.
* @param camera The camera inside the scene.
*/
private antialiasRender(scene: Scene, camera: Camera) {
if (this.selectedObjectsSet.size > 0) {
// Selections require OutlinePass which needs the composer
this.defaultRenderPass.camera = camera;
this.defaultRenderPass.scene = scene;
for (const outlinePass of this.outlinePasses) {
outlinePass.renderCamera = camera;
}
this.updateSelectionPulse();
this.composer.render();
} else if (this.hoverOutline) {
// Hover outlines are scene children, direct render handles them
this.renderer.render(scene, camera);
} else {
this.composer.renderer.render(scene, camera);
}
}
/**
* Initialize an outline pass for external use.
* @returns OutlinePass for highlighting event display elements.
*/
public addOutlinePassForSelection(): OutlinePass {
const outlinePass = new OutlinePass(
new Vector2(window.innerWidth, window.innerHeight),
this.scene,
this.camera,
);
outlinePass.overlayMaterial.blending = NormalBlending;
outlinePass.visibleEdgeColor.set(0xdf5330);
this.composer.addPass(outlinePass);
// Keep track for camera updates
this.outlinePasses.push(outlinePass);
return outlinePass;
}
/**
* Remove a pass from the effect composer.
* @param pass Effect pass to be removed from the effect composer.
*/
public removePass(pass: Pass) {
const passIndex = this.composer.passes.indexOf(pass);
if (passIndex > -1) {
this.composer.passes.splice(passIndex, 1);
}
// If it's an outline pass, remove from tracking array
if (pass instanceof OutlinePass) {
const outlineIndex = this.outlinePasses.indexOf(pass);
if (outlineIndex > -1) {
this.outlinePasses.splice(outlineIndex, 1);
}
}
}
/**
* Set the antialiasing of renderer.
* @param antialias Whether antialiasing is to enabled or disabled.
*/
public setAntialiasing(antialias: boolean) {
this.antialiasing = antialias;
this.render = this.antialiasing ? this.antialiasRender : this.effectsRender;
}
/**
* Update the pulsing animation on the selection OutlinePass.
* Oscillates edgeStrength for a gentle breathing effect.
*/
private updateSelectionPulse() {
if (this.selectionOutlinePass && this.selectedObjectsSet.size > 0) {
const time = performance.now() * 0.001;
// Pulse between 1.5 and 4.5 — always visible, gentle breathing
this.selectionOutlinePass.edgeStrength = 3 + 1.5 * Math.sin(time * 2.5);
}
}
/**
* Get performance statistics for the outline system.
* @returns Performance stats including object counts and hover state.
*/
public getOutlinePerformanceStats() {
return {
selectedObjectsCount: this.selectedObjectsSet.size,
hasHoverOutline: !!this.hoverOutline,
totalOutlines: this.selectedObjectsSet.size + (this.hoverOutline ? 1 : 0),
};
}
/**
* Add an object to the selected set (sticky selection).
* Uses OutlinePass for true silhouette rendering (boundary only).
* @param object The mesh object to be selected.
*/
public selectObject(object: Mesh) {
if (this.selectedObjectsSet.has(object)) {
return;
}
this.selectedObjectsSet.add(object);
const pass = this.ensureSelectionPass();
pass.selectedObjects = Array.from(this.selectedObjectsSet);
pass.enabled = true;
}
/**
* Remove an object from the selected set.
* @param object The mesh object to be deselected.
*/
public deselectObject(object: Mesh) {
if (!this.selectedObjectsSet.has(object)) {
return;
}
this.selectedObjectsSet.delete(object);
if (this.selectionOutlinePass) {
this.selectionOutlinePass.selectedObjects = Array.from(
this.selectedObjectsSet,
);
this.selectionOutlinePass.enabled = this.selectedObjectsSet.size > 0;
}
}
/**
* Toggle selection state of an object.
* @param object The mesh object to toggle.
* @returns True if object is now selected, false if deselected.
*/
public toggleSelection(object: Mesh): boolean {
if (this.selectedObjectsSet.has(object)) {
this.deselectObject(object);
return false;
} else {
this.selectObject(object);
return true;
}
}
/**
* Clear all selected objects.
*/
public clearAllSelections() {
this.selectedObjectsSet.clear();
if (this.selectionOutlinePass) {
this.selectionOutlinePass.selectedObjects = [];
this.selectionOutlinePass.enabled = false;
}
}
/**
* Set hover outline for an object (temporary, non-sticky).
* Uses EdgesGeometry at 15° for visible hover feedback.
* @param object The mesh object to hover outline, or null to clear.
*/
public setHoverOutline(object: Mesh | null) {
// Clear existing hover outline
if (this.hoverOutline) {
this.hoverOutline.removeFromParent();
this.hoverOutline.geometry.dispose();
(this.hoverOutline.material as ShaderMaterial).dispose();
this.hoverOutline = null;
this.hoverTarget = null;
}
// Create new hover outline if object provided and not already selected
if (object && !this.selectedObjectsSet.has(object)) {
this.hoverOutline = this.createHoverOutline(object);
this.hoverTarget = object;
// Add as child so outline inherits all transformations
object.add(this.hoverOutline);
}
}
/**
* Set the selection outline color.
* Default is amber (0xffa633). Experiments with amber-colored objects
* (e.g. LHCb calorimeter deposits) may want a different color for contrast.
* @param color The color as a hex number (e.g. 0x00ff00 for green).
*/
public setSelectionColor(color: number) {
const pass = this.ensureSelectionPass();
pass.visibleEdgeColor.set(color);
}
/**
* Create an EdgesGeometry hover outline for an object.
* Added as a child so it inherits transforms and is excluded from raycasts.
* @param object The mesh object to create hover outline for.
* @returns The created outline helper.
*/
private createHoverOutline(object: Mesh): LineSegments {
// 15° threshold: shows enough edges for clear visibility without clutter
const edges = new EdgesGeometry(object.geometry, 15);
const lineMaterial = new ShaderMaterial({
vertexShader: EffectsManager.VERTEX_SHADER,
fragmentShader: EffectsManager.HOVER_FRAGMENT_SHADER,
uniforms: {
opacity: { value: 0.8 },
},
transparent: true,
depthTest: true,
polygonOffset: true,
polygonOffsetFactor: -1,
polygonOffsetUnits: -1,
});
const outlineHelper = new LineSegments(edges, lineMaterial);
// Prevent hover flicker: outline intercepts raycast → removed → cycle
outlineHelper.raycast = () => {};
// Identity transform — child of target, inherits transformations
outlineHelper.position.set(0, 0, 0);
outlineHelper.rotation.set(0, 0, 0);
outlineHelper.scale.set(1, 1, 1);
return outlineHelper;
}
/**
* Cleanup and dispose all WebGL resources to prevent memory leaks.
* Must be called before re-initialization or when destroying the event display.
*/
public cleanup() {
// Clear all selections (resets OutlinePass)
this.clearAllSelections();
// Clear hover outline (disposes geometry and material)
this.setHoverOutline(null);
// Dispose the selection outline pass
if (this.selectionOutlinePass) {
this.selectionOutlinePass.dispose();
const passIndex = this.composer.passes.indexOf(this.selectionOutlinePass);
if (passIndex > -1) {
this.composer.passes.splice(passIndex, 1);
}
const outlineIndex = this.outlinePasses.indexOf(
this.selectionOutlinePass,
);
if (outlineIndex > -1) {
this.outlinePasses.splice(outlineIndex, 1);
}
this.selectionOutlinePass = null;
}
// Dispose remaining outline passes
for (const pass of this.outlinePasses) {
if (pass.dispose) {
pass.dispose();
}
const passIndex = this.composer.passes.indexOf(pass);
if (passIndex > -1) {
this.composer.passes.splice(passIndex, 1);
}
}
this.outlinePasses = [];
// Dispose the effect composer (frees render targets/framebuffers)
if (this.composer) {
this.composer.dispose();
}
}
}