@eturnity/eturnity_3d 9.10.1 → 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.10.1",
4
+ "version": "9.13.0-google3dTile.1",
5
5
  "files": [
6
6
  "dist",
7
7
  "src"
@@ -16,8 +16,9 @@
16
16
  "merge-remote-master": "node scripts/merge-remote-master.js"
17
17
  },
18
18
  "dependencies": {
19
- "@eturnity/eturnity_maths": "9.10.0",
19
+ "@eturnity/eturnity_maths": "9.13.0",
20
20
  "@originjs/vite-plugin-commonjs": "1.0.3",
21
+ "3d-tiles-renderer": "^0.4.22",
21
22
  "core-js": "3.30.2",
22
23
  "cors": "2.8.5",
23
24
  "earcut": "2.2.4",
@@ -0,0 +1,526 @@
1
+ /**
2
+ * Google 3D Tiles Overlay
3
+ *
4
+ * Loads 3D Tiles from Google's Photorealistic 3D Tiles API and displays them
5
+ * in the Three.js scene. The scene origin is at (centerLat, centerLon) at ground level,
6
+ * with an ENU coordinate system: X = East, Y = North, Z = Up, in meters.
7
+ *
8
+ * Integrates with the existing Overlay system so it can be rendered via renderOnThreeJS
9
+ * and updated every frame via userData.update on the mesh.
10
+ * Extends ThreeDModelOverlay to reuse getProjectedPlanesOnOverlay for roof projection.
11
+ */
12
+
13
+ import ThreeDModelOverlay from './ThreeDModelOverlay'
14
+ import * as THREE from 'three'
15
+ import { TilesRenderer } from '3d-tiles-renderer'
16
+ import {
17
+ TilesFadePlugin,
18
+ GoogleCloudAuthPlugin,
19
+ ReorientationPlugin,
20
+ TileCompressionPlugin,
21
+ } from '3d-tiles-renderer/plugins'
22
+
23
+ /** Set DoubleSide on every mesh material in a loaded 3D Tiles scene */
24
+ function applyDoubleSideToTileScene(scene) {
25
+ if (!scene) return
26
+ scene.traverse((object) => {
27
+ if (!object.isMesh || !object.material) return
28
+ const materials = Array.isArray(object.material)
29
+ ? object.material
30
+ : [object.material]
31
+ for (const mat of materials) {
32
+ if (mat) mat.side = THREE.DoubleSide
33
+ }
34
+ })
35
+ }
36
+
37
+ /**
38
+ * Max screen-space error for tile refinement (3d-tiles-renderer).
39
+ * Lower = finer meshes for tiles inside {@link Google3DTilesOverlay#_tilesCamera}'s frustum.
40
+ * GoogleCloudAuthPlugin sets 20 for broad views; we override for this fixed ortho footprint.
41
+ */
42
+ const TILES_FRUSTUM_ERROR_TARGET = 4
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
+
53
+ /** Same rhythm as ThreeDModelOverlay.animatePlaceholder (sin² on time) */
54
+ function placeholderBounceScale(timeSeconds) {
55
+ const baseRadius = 0.65
56
+ const amplitude = 0.55
57
+ return baseRadius + amplitude * Math.sin(timeSeconds * 0.7 * Math.PI) ** 2
58
+ }
59
+
60
+ export default class Google3DTilesOverlay extends ThreeDModelOverlay {
61
+ constructor(overlay, emit, origin) {
62
+ super(overlay, emit, origin || {}, null)
63
+ this.hasToBeSavedInBE = false
64
+ this.origin = origin || {}
65
+ this.tilesData = null
66
+ this._initPromise = null
67
+ // Center for ReorientationPlugin: use settings or origin (degrees → radians)
68
+ const lat = this.settings?.centerLat ?? this.origin?.lat
69
+ const lon = this.settings?.centerLon ?? this.origin?.lng
70
+ this._centerLatRad =
71
+ lat != null ? lat * (Math.PI / 180) : 35.6586 * (Math.PI / 180)
72
+ this._centerLonRad =
73
+ lon != null ? lon * (Math.PI / 180) : 139.7454 * (Math.PI / 180)
74
+ // Ground elevation in meters from Google Elevation API (used to offset tiles to ground level)
75
+ this._groundElevation = this.settings?.groundElevation ?? null
76
+ // Dedicated camera for tile loading and resolution so the user's camera does not affect which tiles load
77
+ // Orthographic camera looking down (Z up), covering -50 to +50 meters in X and Y
78
+ this._tilesCamera = new THREE.OrthographicCamera(
79
+ -50,
80
+ 50,
81
+ 50,
82
+ -50,
83
+ 0.1,
84
+ 10000
85
+ )
86
+ this._tilesCamera.position.set(0, 0, -100)
87
+ this._tilesCamera.lookAt(0, 0, 0)
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
97
+ const raycaster = new THREE.Raycaster()
98
+ raycaster.far = 1000000
99
+ raycaster.near = 1
100
+ this._defaultRayOrigin = new THREE.Vector3(0, 0, this._defaultTilesCamZ)
101
+ raycaster.set(this._defaultRayOrigin, this._rayDirDown)
102
+ this._raycaster = raycaster
103
+ this.sphereMesh = null
104
+ /** Transparent bouncing sphere at origin; removed once ground align settles */
105
+ this._groundAlignPlaceholderMesh = null
106
+ /** True when raycast tile bbox min.z is within ±10 after stick-to-ground */
107
+ this._tilesGroundAligned = false
108
+ }
109
+ getOverlayMeshForRaycast() {
110
+ return this.tilesData?.tiles.group
111
+ }
112
+ async initialiseModel() {
113
+ this.isReady = true
114
+ return true
115
+ }
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
+
212
+ /**
213
+ * Cast a vertical ray at the origin pointing down and return the intersection height.
214
+ * @returns {number|null} - The z coordinate of the first intersection, or null if none
215
+ */
216
+ raycastAtRoofsBoundCenter() {
217
+ const { tilesWrapper } = this.tilesData || {}
218
+ if (!tilesWrapper) return null
219
+
220
+ tilesWrapper.updateMatrixWorld(true)
221
+
222
+ const intersects = this._raycaster.intersectObject(tilesWrapper, true)
223
+ if (intersects.length > 0) {
224
+ if (intersects[0].point.z < 30) {
225
+ this._tilesGroundAligned = true
226
+ }
227
+ return intersects[0]
228
+ }
229
+ return null
230
+ }
231
+
232
+ _getOrCreateTilesData() {
233
+ if (this.tilesData) return this.tilesData
234
+ const apiKey = process.env.VUE_APP_GOOGLE_MAP_API_KEY
235
+ const tiles = new TilesRenderer()
236
+ tiles.registerPlugin(
237
+ new GoogleCloudAuthPlugin({
238
+ apiToken: apiKey,
239
+ sessionOptions: null,
240
+ autoRefreshToken: true,
241
+ useRecommendedSettings: true,
242
+ })
243
+ )
244
+ tiles.registerPlugin(
245
+ new ReorientationPlugin({
246
+ lat: this._centerLatRad,
247
+ lon: this._centerLonRad,
248
+ })
249
+ )
250
+ tiles.registerPlugin(new TileCompressionPlugin())
251
+ tiles.registerPlugin(new TilesFadePlugin({ maximumFadeOutTiles: 50 }))
252
+
253
+ // After GoogleCloudAuthPlugin.init (errorTarget 20), tighten LOD for the ortho frustum only
254
+ tiles.errorTarget = TILES_FRUSTUM_ERROR_TARGET
255
+ // Prefer loading tiles that intersect _tilesCamera before prefetch / out-of-frustum work
256
+ tiles.optimizedLoadStrategy = true
257
+
258
+ tiles.lruCache.minSize = 400
259
+ tiles.lruCache.maxSize = 800
260
+ tiles.parseQueue.maxJobs = 4
261
+
262
+ const tilesWrapper = new THREE.Group()
263
+ tilesWrapper.name = 'Google3DTilesOverlay'
264
+ tilesWrapper.renderOrder = GOOGLE_TILES_RENDER_ORDER
265
+ tilesWrapper.rotation.x = Math.PI / 2
266
+ tilesWrapper.rotation.y = Math.PI
267
+ tilesWrapper.add(tiles.group)
268
+ tilesWrapper.position.z = -200
269
+ this.tilesData = { tiles, tilesWrapper }
270
+
271
+ tiles.addEventListener('load-model', ({ scene }) => {
272
+ applyDoubleSideToTileScene(scene)
273
+ this._applyGoogleTilesOpacity(tilesWrapper)
274
+ })
275
+
276
+ return this.tilesData
277
+ }
278
+
279
+ _ensureGroundAlignPlaceholder(threeJSComponent) {
280
+ if (this._tilesGroundAligned || !threeJSComponent?.scene) return
281
+ const { scene } = threeJSComponent
282
+ if (this._groundAlignPlaceholderMesh) {
283
+ if (scene.getObjectById(this._groundAlignPlaceholderMesh.id)) return
284
+ this._groundAlignPlaceholderMesh.geometry?.dispose()
285
+ if (this._groundAlignPlaceholderMesh.material?.dispose) {
286
+ this._groundAlignPlaceholderMesh.material.dispose()
287
+ }
288
+ this._groundAlignPlaceholderMesh = null
289
+ }
290
+ const geometry = new THREE.SphereGeometry(10, 32, 32)
291
+ const material = new THREE.MeshBasicMaterial({
292
+ color: 0xffffff,
293
+ transparent: true,
294
+ opacity: 0.2,
295
+ depthWrite: false,
296
+ })
297
+ const mesh = new THREE.Mesh(geometry, material)
298
+ mesh.name = 'Google3DTilesGroundAlignPlaceholder'
299
+ mesh.userData.googleTilesGroundAlignPlaceholder = true
300
+ mesh.position.set(
301
+ this._placeholderCenterX,
302
+ this._placeholderCenterY,
303
+ 0
304
+ )
305
+ mesh.scale.setScalar(1)
306
+ scene.add(mesh)
307
+ this._groundAlignPlaceholderMesh = mesh
308
+ this._tickGroundAlignPlaceholderAnimation()
309
+ }
310
+
311
+ _tickGroundAlignPlaceholderAnimation() {
312
+ const mesh = this._groundAlignPlaceholderMesh
313
+ if (!mesh?.material) return
314
+
315
+ if (this.opacity === 0) {
316
+ mesh.visible = false
317
+ mesh.material.opacity = 0
318
+ return
319
+ }
320
+
321
+ mesh.visible = true
322
+ const t = performance.now() * 0.001
323
+ const scale = placeholderBounceScale(t)
324
+ mesh.scale.setScalar(scale)
325
+ mesh.position.x = this._placeholderCenterX
326
+ mesh.position.y = this._placeholderCenterY
327
+ // Light Z bob so it reads as a bounce (Z up)
328
+ mesh.position.z = 0.2 * Math.sin(t * 1.4 * Math.PI) ** 2
329
+ mesh.material.opacity = 0.06 + 0.22 * Math.sin(t * 0.85 * Math.PI) ** 2
330
+ }
331
+
332
+ _removeGroundAlignPlaceholder(threeJSComponent) {
333
+ const mesh = this._groundAlignPlaceholderMesh
334
+ if (!mesh) return
335
+ const scene = threeJSComponent?.scene
336
+ if (scene && scene.getObjectById(mesh.id)) {
337
+ scene.remove(mesh)
338
+ } else {
339
+ mesh.parent?.remove(mesh)
340
+ }
341
+ mesh.geometry?.dispose()
342
+ if (mesh.material?.dispose) mesh.material.dispose()
343
+ this._groundAlignPlaceholderMesh = null
344
+ }
345
+
346
+ _syncGroundAlignPlaceholder(threeJSComponent) {
347
+ if (!threeJSComponent?.scene) return
348
+ if (this._tilesGroundAligned) {
349
+ this._removeGroundAlignPlaceholder(threeJSComponent)
350
+ } else {
351
+ this._ensureGroundAlignPlaceholder(threeJSComponent)
352
+ }
353
+ }
354
+
355
+ stickObjectToTheGround() {
356
+ const { tiles, tilesWrapper } = this.tilesData || {}
357
+ if (!tiles || !tilesWrapper) {
358
+ this._tilesGroundAligned = false
359
+ return
360
+ }
361
+
362
+ const raycastIntersect = this.raycastAtRoofsBoundCenter()
363
+ if (!raycastIntersect) {
364
+ this._tilesGroundAligned = false
365
+ return
366
+ }
367
+
368
+ const tileMesh = raycastIntersect.object
369
+ const geom = tileMesh.geometry
370
+ if (!geom) {
371
+ this._tilesGroundAligned = false
372
+ return
373
+ }
374
+ if (!geom.boundingBox) geom.computeBoundingBox()
375
+
376
+ const boundingBox = new THREE.Box3()
377
+ .copy(geom.boundingBox)
378
+ .applyMatrix4(tileMesh.matrixWorld)
379
+ if (
380
+ Math.abs(boundingBox.min.z) > 1 &&
381
+ Math.abs(tilesWrapper.position.z - boundingBox.min.z) < 2000
382
+ ) {
383
+ tilesWrapper.position.z -= boundingBox.min.z
384
+ }
385
+
386
+ tilesWrapper.updateMatrixWorld(true)
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
+
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
+
451
+ const tilesData = this._getOrCreateTilesData()
452
+ if (!tilesData) return null
453
+
454
+ const { tiles, tilesWrapper } = tilesData
455
+ const { renderer, scene } = threeJSComponent
456
+ tiles.setCamera(this._tilesCamera)
457
+ tiles.setResolutionFromRenderer(this._tilesCamera, renderer)
458
+
459
+ tilesWrapper.userData.type = 'overlayMeshes'
460
+ tilesWrapper.userData.meshId = this.id
461
+ tilesWrapper.userData.version = this.version
462
+
463
+ this.stickObjectToTheGround()
464
+ this._syncGroundAlignPlaceholder(threeJSComponent)
465
+ if (this._groundAlignPlaceholderMesh) {
466
+ this._tickGroundAlignPlaceholderAnimation()
467
+ }
468
+
469
+ tilesWrapper.userData.update = () => {
470
+ if (!this.isActive || !tiles || !renderer) return
471
+ tiles.errorTarget = TILES_FRUSTUM_ERROR_TARGET
472
+ tiles.setCamera(this._tilesCamera)
473
+ tiles.setResolutionFromRenderer(this._tilesCamera, renderer)
474
+ tiles.update()
475
+ this.stickObjectToTheGround()
476
+ this._syncGroundAlignPlaceholder(threeJSComponent)
477
+ if (this._groundAlignPlaceholderMesh) {
478
+ this._tickGroundAlignPlaceholderAnimation()
479
+ }
480
+ }
481
+
482
+ if (tilesWrapper.parent !== scene) {
483
+ if (tilesWrapper.parent) tilesWrapper.parent.remove(tilesWrapper)
484
+ scene.add(tilesWrapper)
485
+ }
486
+
487
+ threeJSComponent.meshes.overlayMeshes[this.id] = tilesWrapper
488
+ this.overlayMesh = tilesWrapper
489
+ this._applyGoogleTilesOpacity(tilesWrapper)
490
+ return tilesWrapper
491
+ }
492
+
493
+ renderOnPaperJS() {
494
+ // No 2D representation for 3D Tiles overlay
495
+ }
496
+
497
+ serializeSettings() {
498
+ return {
499
+ ...super.serializeSettings(),
500
+ centerLat: this.origin?.lat ?? this.settings?.centerLat,
501
+ centerLon: this.origin?.lng ?? this.settings?.centerLon,
502
+ groundElevation: this._groundElevation,
503
+ }
504
+ }
505
+
506
+ dispose() {
507
+ if (this._groundAlignPlaceholderMesh) {
508
+ this._groundAlignPlaceholderMesh.parent?.remove(
509
+ this._groundAlignPlaceholderMesh
510
+ )
511
+ this._groundAlignPlaceholderMesh.geometry?.dispose()
512
+ if (this._groundAlignPlaceholderMesh.material?.dispose) {
513
+ this._groundAlignPlaceholderMesh.material.dispose()
514
+ }
515
+ this._groundAlignPlaceholderMesh = null
516
+ }
517
+ if (this.tilesData?.tiles) {
518
+ this.tilesData.tiles.dispose()
519
+ }
520
+ this.tilesData = null
521
+ this.overlayMesh = null
522
+ this.roofsBound = null
523
+ this._initPromise = null
524
+ this._tilesGroundAligned = false
525
+ }
526
+ }
@@ -2,6 +2,7 @@ import Overlay from './Overlay'
2
2
  import GLBOverlay from './GLBOverlay'
3
3
  import ImageOverlay from './ImageOverlay'
4
4
  import YearlySunShadingOverlay from './YearlySunShadingOverlay'
5
+ import Google3DTilesOverlay from './Google3DTilesOverlay'
5
6
 
6
7
  const OverlayFactory = {
7
8
  createOverlay(serializedOverlay, emit, origin = { lat: null, lng: null }) {
@@ -12,6 +13,8 @@ const OverlayFactory = {
12
13
  overlayInstance = new ImageOverlay(serializedOverlay, emit)
13
14
  } else if (serializedOverlay.type == 'yearly_sun_shading') {
14
15
  overlayInstance = new YearlySunShadingOverlay(serializedOverlay, emit)
16
+ } else if (serializedOverlay.type == 'google_3d_tiles') {
17
+ overlayInstance = new Google3DTilesOverlay(serializedOverlay, emit, origin)
15
18
  } else {
16
19
  overlayInstance = new Overlay(serializedOverlay, emit)
17
20
  }
@@ -350,8 +350,24 @@ export default class ThreeDModelOverlay extends Overlay {
350
350
  resolve(imageUrl)
351
351
  })
352
352
  }
353
+ /** Override in subclasses to use a different mesh (e.g. tiles wrapper) or null when not yet ready */
354
+ getOverlayMeshForRaycast() {
355
+ return this.overlayMesh
356
+ }
357
+
358
+ /** Override in subclasses to raycast into child meshes (e.g. for Group-based overlays like 3D Tiles) */
359
+ getRaycastRecursive() {
360
+ return false
361
+ }
362
+
353
363
  getProjectedPlanesOnOverlay(polygons) {
364
+ const mesh = this.getOverlayMeshForRaycast()
365
+ if (!mesh) {
366
+ return polygons.map(() => null)
367
+ }
368
+ mesh.updateMatrixWorld(true)
354
369
  const RayCaster = new THREE.Raycaster()
370
+ const recursive = this.getRaycastRecursive()
355
371
  const cloudsPoints = polygons.map((polygon) => {
356
372
  const outline = polygon.outline
357
373
  const holeOutlines = polygon.holes.map((h) => h.outline)
@@ -368,8 +384,8 @@ export default class ThreeDModelOverlay extends Overlay {
368
384
  let { xMin, xMax, yMin, yMax } = outlineBounds
369
385
  const cloudPoints = []
370
386
  let stepNum = 8
371
- for (let i = 1; i < stepNum; i++) {
372
- for (let j = 1; j < stepNum; j++) {
387
+ for (let i = 1; i < stepNum - 1; i++) {
388
+ for (let j = 1; j < stepNum - 1; j++) {
373
389
  const testPoint = {
374
390
  x: xMin + (i * (xMax - xMin)) / stepNum,
375
391
  y: yMin + (j * (yMax - yMin)) / stepNum,
@@ -384,7 +400,7 @@ export default class ThreeDModelOverlay extends Overlay {
384
400
  new THREE.Vector3(testPoint.x / 1000, testPoint.y / 1000, 100),
385
401
  new THREE.Vector3(0, 0, -1)
386
402
  )
387
- let intersects = RayCaster.intersectObject(this.overlayMesh)
403
+ let intersects = RayCaster.intersectObject(mesh, recursive)
388
404
  if (intersects.length > 0) {
389
405
  cloudPoints.push(intersects[0].point)
390
406
  }
@@ -744,6 +744,23 @@ export default {
744
744
  waitingBall.scale.set(scale, scale, scale)
745
745
  this.render()
746
746
  }
747
+ // Per-frame updates for overlays (e.g. Google 3D Tiles)
748
+ const overlayMeshes = this.meshes?.overlayMeshes
749
+ let overlayUpdated = false
750
+ if (overlayMeshes) {
751
+ Object.values(overlayMeshes).forEach((mesh) => {
752
+ if (
753
+ mesh?.userData?.update &&
754
+ typeof mesh.userData.update === 'function'
755
+ ) {
756
+ mesh.userData.update()
757
+ overlayUpdated = true
758
+ }
759
+ })
760
+ }
761
+ if (overlayUpdated) {
762
+ this.render()
763
+ }
747
764
  this.animationFrameId = requestAnimationFrame(this.animate)
748
765
  },
749
766
  },