Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,24 @@ 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';

/**
* 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 like outline pass and unreal bloom.
*
Expand All @@ -25,6 +43,8 @@ import { Pass } from 'three/examples/jsm/postprocessing/Pass.js';
*
* Selection color defaults to amber but is configurable via setSelectionColor()
* for experiments like LHCb where amber-colored objects are common.
* Hover color defaults to blue but is configurable via setHoverColor()
* for experiments where blue objects are common.
*/
export class EffectsManager {
/** Effect composer for effect passes. */
Expand All @@ -35,41 +55,57 @@ export class EffectsManager {
private scene: Scene;
/** Render pass for rendering the default scene. */
private defaultRenderPass: RenderPass;
/** Array to keep track of outline passes that need camera updates */
/** Array of outline passes that require camera updates during rendering. */
private outlinePasses: OutlinePass[] = [];
/** Whether antialiasing is enabled or disabled. */
/** Indicates whether antialiasing is enabled for rendering. */
public antialiasing: boolean = true;
/** WebGL renderer reference */
/** WebGL renderer instance used for rendering the scene. */
private renderer: WebGLRenderer;

// Selection support (OutlinePass — true silhouette)
/** Set of currently selected objects */
/** Set of currently selected objects. */
private selectedObjectsSet: Set<Mesh> = new Set();
/** OutlinePass for selection silhouette (lazy-initialized on first select) */
/** OutlinePass used for selection silhouette rendering. Lazily initialized on first selection. */
private selectionOutlinePass: OutlinePass | null = null;

// Hover support (EdgesGeometry — lightweight)
/** Currently hovered object outline (temporary) */
/** Currently active hover outline object. */
private hoverOutline: LineSegments | null = null;
/** Reference to the hovered object for cleanup */
/** Reference to the hovered object for cleanup. */
private hoverTarget: Mesh | null = null;
/** Current hover color as RGB components (default blue). */
private _hoverColor = { r: 0.2, g: 0.6, b: 1.0 };
/**
* Current visual state of the effects system.
* Introduced to provide a typed alternative to implicit state checks.
*/
private currentState: EffectsState = EffectsState.DEFAULT;

/** Render function with (normal render) or without antialias (effects render). */
/**
* 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 for hover outline rendering. */
/**
* 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 for hover outlines (static blue). */
/**
* Fragment shader used for hover outlines.
* Color is controlled via uniforms to support setHoverColor().
*/
private static readonly HOVER_FRAGMENT_SHADER = `
uniform float opacity;
uniform float colorR;
uniform float colorG;
uniform float colorB;
void main() {
vec3 color = vec3(0.2, 0.6, 1.0);
gl_FragColor = vec4(color, opacity);
gl_FragColor = vec4(colorR, colorG, colorB, opacity);
}
`;

Expand All @@ -96,6 +132,7 @@ export class EffectsManager {
* 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) {
Expand Down Expand Up @@ -225,7 +262,7 @@ export class EffectsManager {

/**
* Get performance statistics for the outline system.
* @returns Object containing performance metrics.
* @returns Performance stats including object counts and hover state.
*/
public getOutlinePerformanceStats() {
return {
Expand All @@ -244,7 +281,7 @@ export class EffectsManager {
if (this.selectedObjectsSet.has(object)) {
return;
}

this.currentState = EffectsState.SELECTED;
this.selectedObjectsSet.add(object);
const pass = this.ensureSelectionPass();
pass.selectedObjects = Array.from(this.selectedObjectsSet);
Expand All @@ -259,8 +296,10 @@ export class EffectsManager {
if (!this.selectedObjectsSet.has(object)) {
return;
}

this.selectedObjectsSet.delete(object);
if (this.selectedObjectsSet.size === 0) {
this.currentState = EffectsState.DEFAULT;
}
if (this.selectionOutlinePass) {
this.selectionOutlinePass.selectedObjects = Array.from(
this.selectedObjectsSet,
Expand Down Expand Up @@ -289,6 +328,7 @@ export class EffectsManager {
*/
public clearAllSelections() {
this.selectedObjectsSet.clear();
this.currentState = EffectsState.DEFAULT;
if (this.selectionOutlinePass) {
this.selectionOutlinePass.selectedObjects = [];
this.selectionOutlinePass.enabled = false;
Expand All @@ -301,6 +341,7 @@ export class EffectsManager {
* @param object The mesh object to hover outline, or null to clear.
*/
public setHoverOutline(object: Mesh | null) {
this.currentState = object ? EffectsState.HOVERED : EffectsState.DEFAULT;
// Clear existing hover outline
if (this.hoverOutline) {
this.hoverOutline.removeFromParent();
Expand All @@ -321,7 +362,7 @@ export class EffectsManager {

/**
* Set the selection outline color.
* Default is amber (0xffa633). Experiments with amber-colored objects
* Default is amber (0xffcc44). 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).
*/
Expand All @@ -330,6 +371,28 @@ export class EffectsManager {
pass.visibleEdgeColor.set(color);
}

/**
* Set the hover outline color.
* Default is blue (0x3399ff). Experiments with blue-colored objects
* (e.g. LHCb calorimeter deposits) can override this for contrast
* with the selection color. Mirrors setSelectionColor() for hover.
* @param color The color as a hex number (e.g. 0xff6600 for orange).
*/
public setHoverColor(color: number): void {
this._hoverColor = {
r: ((color >> 16) & 255) / 255,
g: ((color >> 8) & 255) / 255,
b: (color & 255) / 255,
};
// Update active hover outline immediately if one exists
if (this.hoverOutline) {
const mat = this.hoverOutline.material as ShaderMaterial;
mat.uniforms['colorR'].value = this._hoverColor.r;
mat.uniforms['colorG'].value = this._hoverColor.g;
mat.uniforms['colorB'].value = this._hoverColor.b;
}
}

/**
* Create an EdgesGeometry hover outline for an object.
* Added as a child so it inherits transforms and is excluded from raycasts.
Expand All @@ -345,6 +408,9 @@ export class EffectsManager {
fragmentShader: EffectsManager.HOVER_FRAGMENT_SHADER,
uniforms: {
opacity: { value: 0.8 },
colorR: { value: this._hoverColor.r },
colorG: { value: this._hoverColor.g },
colorB: { value: this._hoverColor.b },
},
transparent: true,
depthTest: true,
Expand Down
Loading