11import * as THREE from 'three' ;
22
3- // Floor height constants (matching WorldObjectFactory/HouseBuilder)
4- // Units: meters
5- const FOUNDATION_HEIGHT = 0.4 ;
6- const FLOOR_HEIGHT = 2.7 ;
7- const FLOOR1_TOP = FOUNDATION_HEIGHT + FLOOR_HEIGHT ; // 3.1m
8- const FLOOR2_TOP = FOUNDATION_HEIGHT + FLOOR_HEIGHT * 2 ; // 5.8m
9-
10- const HIDDEN_OPACITY = 0.15 ;
3+ const HIDDEN_OPACITY = 0.35 ;
114const VISIBLE_OPACITY = 1.0 ;
125
6+ type OpacityMaterial = THREE . Material & {
7+ opacity : number ;
8+ transparent : boolean ;
9+ needsUpdate : boolean ;
10+ userData : Record < string , unknown > ;
11+ } ;
12+
1313/**
1414 * Manages height-based opacity for world objects.
1515 * Objects above a threshold become semi-transparent.
16- * Doors and roof elements are made transparent along with walls at the same floor level.
16+ *
17+ * Implementation detail:
18+ * - We treat "above the cutoff" as: the mesh's world-space bounding box extends above the cutoff.
19+ * This gives a better "cutaway" feel than using the mesh origin, while keeping doors (short)
20+ * opaque when the cutoff is above them.
1721 */
1822export class HeightOpacityManager {
1923 private readonly worldObjects : THREE . Object3D [ ] ;
24+ private readonly tmpBox = new THREE . Box3 ( ) ;
2025
2126 constructor ( worldObjects : THREE . Object3D [ ] ) {
2227 this . worldObjects = worldObjects ;
@@ -25,44 +30,61 @@ export class HeightOpacityManager {
2530 /**
2631 * Sets opacity for objects based on height threshold.
2732 * Objects above the threshold become semi-transparent.
28- * Doors and roof elements are made transparent along with walls at the same floor level.
2933 * @param heightThreshold - Height in meters above which objects become transparent. null = fully opaque.
3034 */
3135 public setHeightOpacity ( heightThreshold : number | null ) : void {
3236 for ( const obj of this . worldObjects ) {
3337 obj . traverse ( ( child ) => {
34- if ( child instanceof THREE . Mesh && child . material instanceof THREE . MeshStandardMaterial ) {
35- // Get world position of the mesh
36- const worldPos = new THREE . Vector3 ( ) ;
37- child . getWorldPosition ( worldPos ) ;
38+ if ( ! ( child instanceof THREE . Mesh ) ) return ;
39+
40+ const materials = Array . isArray ( child . material ) ? child . material : [ child . material ] ;
41+ if ( materials . length === 0 ) return ;
42+
43+ // Determine target opacity (per mesh) based on world-space height.
44+ let applyHidden = false ;
45+ if ( heightThreshold !== null ) {
46+ // Ensure matrixWorld is current before we transform the local bbox.
47+ child . updateWorldMatrix ( true , false ) ;
48+
49+ const geom = child . geometry ;
50+ if ( geom ?. boundingBox === null ) {
51+ geom . computeBoundingBox ( ) ;
52+ }
53+ if ( geom ?. boundingBox ) {
54+ this . tmpBox . copy ( geom . boundingBox ) . applyMatrix4 ( child . matrixWorld ) ;
55+ applyHidden = this . tmpBox . max . y > heightThreshold ;
56+ }
57+ }
58+
59+ for ( const material of materials ) {
60+ if ( ! material ) continue ;
61+ const mat = material as Partial < OpacityMaterial > ;
62+ if ( typeof mat . opacity !== 'number' || typeof mat . transparent !== 'boolean' ) continue ;
63+
64+ // Cache original state once so we can restore on reset.
65+ const userData = ( material . userData ??= { } ) ;
66+ if ( userData . __heightOpacityOriginalOpacity === undefined ) {
67+ userData . __heightOpacityOriginalOpacity = mat . opacity ;
68+ }
69+ if ( userData . __heightOpacityOriginalTransparent === undefined ) {
70+ userData . __heightOpacityOriginalTransparent = mat . transparent ;
71+ }
72+
73+ const originalOpacity = userData . __heightOpacityOriginalOpacity as number ;
74+ const originalTransparent = userData . __heightOpacityOriginalTransparent as boolean ;
3875
39- // Determine target opacity based on type
40- let targetOpacity : number ;
4176 if ( heightThreshold === null ) {
42- targetOpacity = VISIBLE_OPACITY ;
77+ mat . opacity = originalOpacity ;
78+ mat . transparent = originalTransparent || originalOpacity < VISIBLE_OPACITY ;
79+ } else if ( applyHidden ) {
80+ mat . opacity = Math . min ( originalOpacity , HIDDEN_OPACITY ) ;
81+ mat . transparent = true ;
4382 } else {
44- const occlusionType = child . userData . occlusionType as string | undefined ;
45- const floorLevel = child . userData . floorLevel as number | undefined ;
46-
47- if ( occlusionType === 'door' && floorLevel !== undefined ) {
48- // Doors become transparent when their floor's walls would be transparent
49- // Floor 1 doors: transparent when threshold < floor1Top (3.1m)
50- // Floor 2 doors: transparent when threshold < floor2Top (5.8m)
51- const floorTop = floorLevel === 1 ? FLOOR1_TOP : FLOOR2_TOP ;
52- targetOpacity = heightThreshold < floorTop ? HIDDEN_OPACITY : VISIBLE_OPACITY ;
53- } else if ( occlusionType === 'roof' ) {
54- // Roof becomes transparent when threshold < floor2Top (when looking at 2nd floor)
55- targetOpacity = heightThreshold < FLOOR2_TOP ? HIDDEN_OPACITY : VISIBLE_OPACITY ;
56- } else {
57- // Default height-based check for walls and other objects
58- targetOpacity = worldPos . y > heightThreshold ? HIDDEN_OPACITY : VISIBLE_OPACITY ;
59- }
83+ mat . opacity = originalOpacity ;
84+ mat . transparent = originalTransparent || originalOpacity < VISIBLE_OPACITY ;
6085 }
6186
62- // Update material opacity
63- child . material . transparent = true ;
64- child . material . opacity = targetOpacity ;
65- child . material . needsUpdate = true ;
87+ mat . needsUpdate = true ;
6688 }
6789 } ) ;
6890 }
0 commit comments