@eturnity/eturnity_3d 9.13.0-google3dTile.0 → 9.13.0-google3dTile.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@eturnity/eturnity_3d",
3
3
  "private": false,
4
- "version": "9.13.0-google3dTile.0",
4
+ "version": "9.13.0-google3dTile.1",
5
5
  "files": [
6
6
  "dist",
7
7
  "src"
@@ -41,6 +41,15 @@ function applyDoubleSideToTileScene(scene) {
41
41
  */
42
42
  const TILES_FRUSTUM_ERROR_TARGET = 4
43
43
 
44
+ /**
45
+ * Draw after merged base / roofs (renderOrder 10) and edges (15) so transparent
46
+ * tiles blend with geometry already in the color buffer instead of hiding it.
47
+ */
48
+ const GOOGLE_TILES_RENDER_ORDER = 25
49
+
50
+ /** Extra meters padded on each side of roof AABB for tile loading frustum */
51
+ const ROOFS_BOUNDS_MARGIN_M = 50
52
+
44
53
  /** Same rhythm as ThreeDModelOverlay.animatePlaceholder (sin² on time) */
45
54
  function placeholderBounceScale(timeSeconds) {
46
55
  const baseRadius = 0.65
@@ -77,12 +86,19 @@ export default class Google3DTilesOverlay extends ThreeDModelOverlay {
77
86
  this._tilesCamera.position.set(0, 0, -100)
78
87
  this._tilesCamera.lookAt(0, 0, 0)
79
88
  this._tilesCamera.updateMatrixWorld(true)
89
+ /** @type {{ xMin: number, xMax: number, yMin: number, yMax: number, zMin?: number, zMax?: number } | null} mm — same shape as Vuex getRoofsBounds (three-d-module) */
90
+ this.roofsBound = null
91
+ /** Padded roof-bounds center (m), same as tile camera look-at XY — placeholder sits at (cx, cy, 0) */
92
+ this._placeholderCenterX = 0
93
+ this._placeholderCenterY = 0
94
+ this._rayDirDown = new THREE.Vector3(0, 0, 1)
95
+ this._defaultTilesCamZ = -100
96
+ this._defaultOrthoHalfExtentM = 100
80
97
  const raycaster = new THREE.Raycaster()
81
- const raycasterOrigin = new THREE.Vector3(0, 0, -100)
82
- const direction = new THREE.Vector3(0, 0, 1)
83
98
  raycaster.far = 1000000
84
99
  raycaster.near = 1
85
- raycaster.set(raycasterOrigin, direction)
100
+ this._defaultRayOrigin = new THREE.Vector3(0, 0, this._defaultTilesCamZ)
101
+ raycaster.set(this._defaultRayOrigin, this._rayDirDown)
86
102
  this._raycaster = raycaster
87
103
  this.sphereMesh = null
88
104
  /** Transparent bouncing sphere at origin; removed once ground align settles */
@@ -98,11 +114,106 @@ export default class Google3DTilesOverlay extends ThreeDModelOverlay {
98
114
  return true
99
115
  }
100
116
 
117
+ _roofsBoundsAreEqual(a, b) {
118
+ if (a === b) return true
119
+ if (!a || !b) return false
120
+ return (
121
+ a.xMin === b.xMin &&
122
+ a.xMax === b.xMax &&
123
+ a.yMin === b.yMin &&
124
+ a.yMax === b.yMax
125
+ )
126
+ }
127
+
128
+ /**
129
+ * Store roof AABB (mm) and fit {@link #_tilesCamera} to it (ortho frustum in m).
130
+ * Bounds shape matches Vuex `getRoofsBounds` in `three-d-module.js`.
131
+ * Camera sits above the bounds center; left/right/top/bottom are set so the world XY
132
+ * footprint matches [xMin,xMax]×[yMin,yMax] in meters (same convention as ThreeCanvas orthographic).
133
+ *
134
+ * @param {{ xMin: number, xMax: number, yMin: number, yMax: number, zMin?: number, zMax?: number } | null | undefined} boundsMm
135
+ */
136
+ updateRoofsBound(boundsMm) {
137
+ this._tilesGroundAligned = false
138
+ if (
139
+ !boundsMm ||
140
+ !Number.isFinite(boundsMm.xMin) ||
141
+ boundsMm.xMin === Infinity
142
+ ) {
143
+ this.roofsBound = null
144
+ const h = this._defaultOrthoHalfExtentM
145
+ const cam = this._tilesCamera
146
+ const z = this._defaultTilesCamZ
147
+ cam.position.set(0, 0, z)
148
+ cam.left = -h
149
+ cam.right = h
150
+ cam.top = h
151
+ cam.bottom = -h
152
+ cam.lookAt(0, 0, 0)
153
+ cam.updateProjectionMatrix()
154
+ cam.updateMatrixWorld(true)
155
+ this._defaultRayOrigin.set(0, 0, z)
156
+ this._raycaster.set(this._defaultRayOrigin, this._rayDirDown)
157
+ this._placeholderCenterX = 0
158
+ this._placeholderCenterY = 0
159
+ this._syncPlaceholderWorldXY()
160
+ return
161
+ }
162
+
163
+ this.roofsBound = {
164
+ xMin: boundsMm.xMin,
165
+ xMax: boundsMm.xMax,
166
+ yMin: boundsMm.yMin,
167
+ yMax: boundsMm.yMax,
168
+ zMin: boundsMm.zMin,
169
+ zMax: boundsMm.zMax,
170
+ }
171
+
172
+ const m = ROOFS_BOUNDS_MARGIN_M
173
+ const xMinM = boundsMm.xMin / 1000 - m
174
+ const xMaxM = boundsMm.xMax / 1000 + m
175
+ const yMinM = boundsMm.yMin / 1000 - m
176
+ const yMaxM = boundsMm.yMax / 1000 + m
177
+
178
+ const cx = (xMinM + xMaxM) / 2
179
+ const cy = (yMinM + yMaxM) / 2
180
+
181
+ let halfW = (xMaxM - xMinM) / 2
182
+ let halfH = (yMaxM - yMinM) / 2
183
+ const minHalf = 100
184
+ if (halfW < minHalf) halfW = minHalf
185
+ if (halfH < minHalf) halfH = minHalf
186
+
187
+ const cam = this._tilesCamera
188
+ const z = this._defaultTilesCamZ
189
+ cam.position.set(cx, cy, z)
190
+ cam.left = xMinM - cx
191
+ cam.right = xMaxM - cx
192
+ cam.top = yMaxM - cy
193
+ cam.bottom = yMinM - cy
194
+ cam.lookAt(cx, cy, 0)
195
+ cam.updateProjectionMatrix()
196
+ cam.updateMatrixWorld(true)
197
+
198
+ this._raycaster.set(new THREE.Vector3(cx, cy, z), this._rayDirDown)
199
+
200
+ this._placeholderCenterX = cx
201
+ this._placeholderCenterY = cy
202
+ this._syncPlaceholderWorldXY()
203
+ }
204
+
205
+ _syncPlaceholderWorldXY() {
206
+ const mesh = this._groundAlignPlaceholderMesh
207
+ if (!mesh) return
208
+ mesh.position.x = this._placeholderCenterX
209
+ mesh.position.y = this._placeholderCenterY
210
+ }
211
+
101
212
  /**
102
213
  * Cast a vertical ray at the origin pointing down and return the intersection height.
103
214
  * @returns {number|null} - The z coordinate of the first intersection, or null if none
104
215
  */
105
- raycastAtOrigin() {
216
+ raycastAtRoofsBoundCenter() {
106
217
  const { tilesWrapper } = this.tilesData || {}
107
218
  if (!tilesWrapper) return null
108
219
 
@@ -144,21 +255,24 @@ export default class Google3DTilesOverlay extends ThreeDModelOverlay {
144
255
  // Prefer loading tiles that intersect _tilesCamera before prefetch / out-of-frustum work
145
256
  tiles.optimizedLoadStrategy = true
146
257
 
147
- tiles.addEventListener('load-model', ({ scene }) => {
148
- applyDoubleSideToTileScene(scene)
149
- })
150
-
151
258
  tiles.lruCache.minSize = 400
152
259
  tiles.lruCache.maxSize = 800
153
260
  tiles.parseQueue.maxJobs = 4
154
261
 
155
262
  const tilesWrapper = new THREE.Group()
156
263
  tilesWrapper.name = 'Google3DTilesOverlay'
264
+ tilesWrapper.renderOrder = GOOGLE_TILES_RENDER_ORDER
157
265
  tilesWrapper.rotation.x = Math.PI / 2
158
266
  tilesWrapper.rotation.y = Math.PI
159
267
  tilesWrapper.add(tiles.group)
160
268
  tilesWrapper.position.z = -200
161
269
  this.tilesData = { tiles, tilesWrapper }
270
+
271
+ tiles.addEventListener('load-model', ({ scene }) => {
272
+ applyDoubleSideToTileScene(scene)
273
+ this._applyGoogleTilesOpacity(tilesWrapper)
274
+ })
275
+
162
276
  return this.tilesData
163
277
  }
164
278
 
@@ -183,7 +297,11 @@ export default class Google3DTilesOverlay extends ThreeDModelOverlay {
183
297
  const mesh = new THREE.Mesh(geometry, material)
184
298
  mesh.name = 'Google3DTilesGroundAlignPlaceholder'
185
299
  mesh.userData.googleTilesGroundAlignPlaceholder = true
186
- mesh.position.set(0, 0, 0)
300
+ mesh.position.set(
301
+ this._placeholderCenterX,
302
+ this._placeholderCenterY,
303
+ 0
304
+ )
187
305
  mesh.scale.setScalar(1)
188
306
  scene.add(mesh)
189
307
  this._groundAlignPlaceholderMesh = mesh
@@ -204,10 +322,10 @@ export default class Google3DTilesOverlay extends ThreeDModelOverlay {
204
322
  const t = performance.now() * 0.001
205
323
  const scale = placeholderBounceScale(t)
206
324
  mesh.scale.setScalar(scale)
325
+ mesh.position.x = this._placeholderCenterX
326
+ mesh.position.y = this._placeholderCenterY
207
327
  // Light Z bob so it reads as a bounce (Z up)
208
328
  mesh.position.z = 0.2 * Math.sin(t * 1.4 * Math.PI) ** 2
209
- mesh.position.x = 0
210
- mesh.position.y = 0
211
329
  mesh.material.opacity = 0.06 + 0.22 * Math.sin(t * 0.85 * Math.PI) ** 2
212
330
  }
213
331
 
@@ -241,7 +359,7 @@ export default class Google3DTilesOverlay extends ThreeDModelOverlay {
241
359
  return
242
360
  }
243
361
 
244
- const raycastIntersect = this.raycastAtOrigin()
362
+ const raycastIntersect = this.raycastAtRoofsBoundCenter()
245
363
  if (!raycastIntersect) {
246
364
  this._tilesGroundAligned = false
247
365
  return
@@ -267,7 +385,69 @@ export default class Google3DTilesOverlay extends ThreeDModelOverlay {
267
385
 
268
386
  tilesWrapper.updateMatrixWorld(true)
269
387
  }
388
+ /**
389
+ * Apply global overlay opacity to all tile mesh materials (base Overlay.setOpacity only updates state + emit).
390
+ */
391
+ _applyGoogleTilesOpacity(wrapper) {
392
+ if (!wrapper) return
393
+ const opacity = this.opacity
394
+ const isTransparent = opacity < 1 - 1e-6
395
+ wrapper.renderOrder = GOOGLE_TILES_RENDER_ORDER
396
+ wrapper.visible = Boolean(this.isActive) && opacity > 0
397
+ wrapper.traverse((object) => {
398
+ if (!object.isMesh || !object.material) return
399
+ object.renderOrder = GOOGLE_TILES_RENDER_ORDER
400
+ const materials = Array.isArray(object.material)
401
+ ? object.material
402
+ : [object.material]
403
+ for (const mat of materials) {
404
+ if (mat) {
405
+ mat.transparent = isTransparent
406
+ mat.opacity = opacity
407
+ // Avoid writing depth when semi-transparent so merged base (base.js) and
408
+ // other geometry drawn earlier remain visible through the tiles.
409
+ mat.depthWrite = !isTransparent
410
+ }
411
+ }
412
+ })
413
+ }
414
+
415
+ setOpacity(opacity) {
416
+ super.setOpacity(opacity)
417
+ const wrapper = this.overlayMesh || this.tilesData?.tilesWrapper
418
+ if (wrapper) {
419
+ this._applyGoogleTilesOpacity(wrapper)
420
+ }
421
+ }
422
+
423
+ setIsActive(isActive) {
424
+ this.isActive = Boolean(isActive)
425
+ const wrapper = this.overlayMesh || this.tilesData?.tilesWrapper
426
+ if (wrapper) {
427
+ this._applyGoogleTilesOpacity(wrapper)
428
+ }
429
+ this.emit('item-updated', this)
430
+ }
431
+
270
432
  async renderOnThreeJS(threeJSComponent) {
433
+ const rb = threeJSComponent?.roofsBounds
434
+ if (rb && !this._roofsBoundsAreEqual(this.roofsBound, rb)) {
435
+ this.updateRoofsBound(rb)
436
+ }
437
+
438
+ if (!this.isActive) {
439
+ this._removeGroundAlignPlaceholder(threeJSComponent)
440
+ const tilesWrapper =
441
+ this.tilesData?.tilesWrapper ||
442
+ threeJSComponent.meshes.overlayMeshes[this.id]
443
+ if (tilesWrapper) {
444
+ tilesWrapper.visible = false
445
+ threeJSComponent.meshes.overlayMeshes[this.id] = tilesWrapper
446
+ this.overlayMesh = tilesWrapper
447
+ }
448
+ return tilesWrapper || null
449
+ }
450
+
271
451
  const tilesData = this._getOrCreateTilesData()
272
452
  if (!tilesData) return null
273
453
 
@@ -287,7 +467,7 @@ export default class Google3DTilesOverlay extends ThreeDModelOverlay {
287
467
  }
288
468
 
289
469
  tilesWrapper.userData.update = () => {
290
- if (!tiles || !renderer) return
470
+ if (!this.isActive || !tiles || !renderer) return
291
471
  tiles.errorTarget = TILES_FRUSTUM_ERROR_TARGET
292
472
  tiles.setCamera(this._tilesCamera)
293
473
  tiles.setResolutionFromRenderer(this._tilesCamera, renderer)
@@ -306,6 +486,7 @@ export default class Google3DTilesOverlay extends ThreeDModelOverlay {
306
486
 
307
487
  threeJSComponent.meshes.overlayMeshes[this.id] = tilesWrapper
308
488
  this.overlayMesh = tilesWrapper
489
+ this._applyGoogleTilesOpacity(tilesWrapper)
309
490
  return tilesWrapper
310
491
  }
311
492
 
@@ -338,6 +519,7 @@ export default class Google3DTilesOverlay extends ThreeDModelOverlay {
338
519
  }
339
520
  this.tilesData = null
340
521
  this.overlayMesh = null
522
+ this.roofsBound = null
341
523
  this._initPromise = null
342
524
  this._tilesGroundAligned = false
343
525
  }