@eturnity/eturnity_3d 9.10.0 → 9.13.0-google3dTile.0

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/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ .canvas3DContainer{height:100%;width:100%}.canvas3DContainer>canvas{height:100%;width:100%}
package/dist/vite.svg ADDED
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@eturnity/eturnity_3d",
3
3
  "private": false,
4
- "version": "9.10.0",
4
+ "version": "9.13.0-google3dTile.0",
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.7.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,344 @@
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
+ /** Same rhythm as ThreeDModelOverlay.animatePlaceholder (sin² on time) */
45
+ function placeholderBounceScale(timeSeconds) {
46
+ const baseRadius = 0.65
47
+ const amplitude = 0.55
48
+ return baseRadius + amplitude * Math.sin(timeSeconds * 0.7 * Math.PI) ** 2
49
+ }
50
+
51
+ export default class Google3DTilesOverlay extends ThreeDModelOverlay {
52
+ constructor(overlay, emit, origin) {
53
+ super(overlay, emit, origin || {}, null)
54
+ this.hasToBeSavedInBE = false
55
+ this.origin = origin || {}
56
+ this.tilesData = null
57
+ this._initPromise = null
58
+ // Center for ReorientationPlugin: use settings or origin (degrees → radians)
59
+ const lat = this.settings?.centerLat ?? this.origin?.lat
60
+ const lon = this.settings?.centerLon ?? this.origin?.lng
61
+ this._centerLatRad =
62
+ lat != null ? lat * (Math.PI / 180) : 35.6586 * (Math.PI / 180)
63
+ this._centerLonRad =
64
+ lon != null ? lon * (Math.PI / 180) : 139.7454 * (Math.PI / 180)
65
+ // Ground elevation in meters from Google Elevation API (used to offset tiles to ground level)
66
+ this._groundElevation = this.settings?.groundElevation ?? null
67
+ // Dedicated camera for tile loading and resolution so the user's camera does not affect which tiles load
68
+ // Orthographic camera looking down (Z up), covering -50 to +50 meters in X and Y
69
+ this._tilesCamera = new THREE.OrthographicCamera(
70
+ -50,
71
+ 50,
72
+ 50,
73
+ -50,
74
+ 0.1,
75
+ 10000
76
+ )
77
+ this._tilesCamera.position.set(0, 0, -100)
78
+ this._tilesCamera.lookAt(0, 0, 0)
79
+ this._tilesCamera.updateMatrixWorld(true)
80
+ const raycaster = new THREE.Raycaster()
81
+ const raycasterOrigin = new THREE.Vector3(0, 0, -100)
82
+ const direction = new THREE.Vector3(0, 0, 1)
83
+ raycaster.far = 1000000
84
+ raycaster.near = 1
85
+ raycaster.set(raycasterOrigin, direction)
86
+ this._raycaster = raycaster
87
+ this.sphereMesh = null
88
+ /** Transparent bouncing sphere at origin; removed once ground align settles */
89
+ this._groundAlignPlaceholderMesh = null
90
+ /** True when raycast tile bbox min.z is within ±10 after stick-to-ground */
91
+ this._tilesGroundAligned = false
92
+ }
93
+ getOverlayMeshForRaycast() {
94
+ return this.tilesData?.tiles.group
95
+ }
96
+ async initialiseModel() {
97
+ this.isReady = true
98
+ return true
99
+ }
100
+
101
+ /**
102
+ * Cast a vertical ray at the origin pointing down and return the intersection height.
103
+ * @returns {number|null} - The z coordinate of the first intersection, or null if none
104
+ */
105
+ raycastAtOrigin() {
106
+ const { tilesWrapper } = this.tilesData || {}
107
+ if (!tilesWrapper) return null
108
+
109
+ tilesWrapper.updateMatrixWorld(true)
110
+
111
+ const intersects = this._raycaster.intersectObject(tilesWrapper, true)
112
+ if (intersects.length > 0) {
113
+ if (intersects[0].point.z < 30) {
114
+ this._tilesGroundAligned = true
115
+ }
116
+ return intersects[0]
117
+ }
118
+ return null
119
+ }
120
+
121
+ _getOrCreateTilesData() {
122
+ if (this.tilesData) return this.tilesData
123
+ const apiKey = process.env.VUE_APP_GOOGLE_MAP_API_KEY
124
+ const tiles = new TilesRenderer()
125
+ tiles.registerPlugin(
126
+ new GoogleCloudAuthPlugin({
127
+ apiToken: apiKey,
128
+ sessionOptions: null,
129
+ autoRefreshToken: true,
130
+ useRecommendedSettings: true,
131
+ })
132
+ )
133
+ tiles.registerPlugin(
134
+ new ReorientationPlugin({
135
+ lat: this._centerLatRad,
136
+ lon: this._centerLonRad,
137
+ })
138
+ )
139
+ tiles.registerPlugin(new TileCompressionPlugin())
140
+ tiles.registerPlugin(new TilesFadePlugin({ maximumFadeOutTiles: 50 }))
141
+
142
+ // After GoogleCloudAuthPlugin.init (errorTarget 20), tighten LOD for the ortho frustum only
143
+ tiles.errorTarget = TILES_FRUSTUM_ERROR_TARGET
144
+ // Prefer loading tiles that intersect _tilesCamera before prefetch / out-of-frustum work
145
+ tiles.optimizedLoadStrategy = true
146
+
147
+ tiles.addEventListener('load-model', ({ scene }) => {
148
+ applyDoubleSideToTileScene(scene)
149
+ })
150
+
151
+ tiles.lruCache.minSize = 400
152
+ tiles.lruCache.maxSize = 800
153
+ tiles.parseQueue.maxJobs = 4
154
+
155
+ const tilesWrapper = new THREE.Group()
156
+ tilesWrapper.name = 'Google3DTilesOverlay'
157
+ tilesWrapper.rotation.x = Math.PI / 2
158
+ tilesWrapper.rotation.y = Math.PI
159
+ tilesWrapper.add(tiles.group)
160
+ tilesWrapper.position.z = -200
161
+ this.tilesData = { tiles, tilesWrapper }
162
+ return this.tilesData
163
+ }
164
+
165
+ _ensureGroundAlignPlaceholder(threeJSComponent) {
166
+ if (this._tilesGroundAligned || !threeJSComponent?.scene) return
167
+ const { scene } = threeJSComponent
168
+ if (this._groundAlignPlaceholderMesh) {
169
+ if (scene.getObjectById(this._groundAlignPlaceholderMesh.id)) return
170
+ this._groundAlignPlaceholderMesh.geometry?.dispose()
171
+ if (this._groundAlignPlaceholderMesh.material?.dispose) {
172
+ this._groundAlignPlaceholderMesh.material.dispose()
173
+ }
174
+ this._groundAlignPlaceholderMesh = null
175
+ }
176
+ const geometry = new THREE.SphereGeometry(10, 32, 32)
177
+ const material = new THREE.MeshBasicMaterial({
178
+ color: 0xffffff,
179
+ transparent: true,
180
+ opacity: 0.2,
181
+ depthWrite: false,
182
+ })
183
+ const mesh = new THREE.Mesh(geometry, material)
184
+ mesh.name = 'Google3DTilesGroundAlignPlaceholder'
185
+ mesh.userData.googleTilesGroundAlignPlaceholder = true
186
+ mesh.position.set(0, 0, 0)
187
+ mesh.scale.setScalar(1)
188
+ scene.add(mesh)
189
+ this._groundAlignPlaceholderMesh = mesh
190
+ this._tickGroundAlignPlaceholderAnimation()
191
+ }
192
+
193
+ _tickGroundAlignPlaceholderAnimation() {
194
+ const mesh = this._groundAlignPlaceholderMesh
195
+ if (!mesh?.material) return
196
+
197
+ if (this.opacity === 0) {
198
+ mesh.visible = false
199
+ mesh.material.opacity = 0
200
+ return
201
+ }
202
+
203
+ mesh.visible = true
204
+ const t = performance.now() * 0.001
205
+ const scale = placeholderBounceScale(t)
206
+ mesh.scale.setScalar(scale)
207
+ // Light Z bob so it reads as a bounce (Z up)
208
+ mesh.position.z = 0.2 * Math.sin(t * 1.4 * Math.PI) ** 2
209
+ mesh.position.x = 0
210
+ mesh.position.y = 0
211
+ mesh.material.opacity = 0.06 + 0.22 * Math.sin(t * 0.85 * Math.PI) ** 2
212
+ }
213
+
214
+ _removeGroundAlignPlaceholder(threeJSComponent) {
215
+ const mesh = this._groundAlignPlaceholderMesh
216
+ if (!mesh) return
217
+ const scene = threeJSComponent?.scene
218
+ if (scene && scene.getObjectById(mesh.id)) {
219
+ scene.remove(mesh)
220
+ } else {
221
+ mesh.parent?.remove(mesh)
222
+ }
223
+ mesh.geometry?.dispose()
224
+ if (mesh.material?.dispose) mesh.material.dispose()
225
+ this._groundAlignPlaceholderMesh = null
226
+ }
227
+
228
+ _syncGroundAlignPlaceholder(threeJSComponent) {
229
+ if (!threeJSComponent?.scene) return
230
+ if (this._tilesGroundAligned) {
231
+ this._removeGroundAlignPlaceholder(threeJSComponent)
232
+ } else {
233
+ this._ensureGroundAlignPlaceholder(threeJSComponent)
234
+ }
235
+ }
236
+
237
+ stickObjectToTheGround() {
238
+ const { tiles, tilesWrapper } = this.tilesData || {}
239
+ if (!tiles || !tilesWrapper) {
240
+ this._tilesGroundAligned = false
241
+ return
242
+ }
243
+
244
+ const raycastIntersect = this.raycastAtOrigin()
245
+ if (!raycastIntersect) {
246
+ this._tilesGroundAligned = false
247
+ return
248
+ }
249
+
250
+ const tileMesh = raycastIntersect.object
251
+ const geom = tileMesh.geometry
252
+ if (!geom) {
253
+ this._tilesGroundAligned = false
254
+ return
255
+ }
256
+ if (!geom.boundingBox) geom.computeBoundingBox()
257
+
258
+ const boundingBox = new THREE.Box3()
259
+ .copy(geom.boundingBox)
260
+ .applyMatrix4(tileMesh.matrixWorld)
261
+ if (
262
+ Math.abs(boundingBox.min.z) > 1 &&
263
+ Math.abs(tilesWrapper.position.z - boundingBox.min.z) < 2000
264
+ ) {
265
+ tilesWrapper.position.z -= boundingBox.min.z
266
+ }
267
+
268
+ tilesWrapper.updateMatrixWorld(true)
269
+ }
270
+ async renderOnThreeJS(threeJSComponent) {
271
+ const tilesData = this._getOrCreateTilesData()
272
+ if (!tilesData) return null
273
+
274
+ const { tiles, tilesWrapper } = tilesData
275
+ const { renderer, scene } = threeJSComponent
276
+ tiles.setCamera(this._tilesCamera)
277
+ tiles.setResolutionFromRenderer(this._tilesCamera, renderer)
278
+
279
+ tilesWrapper.userData.type = 'overlayMeshes'
280
+ tilesWrapper.userData.meshId = this.id
281
+ tilesWrapper.userData.version = this.version
282
+
283
+ this.stickObjectToTheGround()
284
+ this._syncGroundAlignPlaceholder(threeJSComponent)
285
+ if (this._groundAlignPlaceholderMesh) {
286
+ this._tickGroundAlignPlaceholderAnimation()
287
+ }
288
+
289
+ tilesWrapper.userData.update = () => {
290
+ if (!tiles || !renderer) return
291
+ tiles.errorTarget = TILES_FRUSTUM_ERROR_TARGET
292
+ tiles.setCamera(this._tilesCamera)
293
+ tiles.setResolutionFromRenderer(this._tilesCamera, renderer)
294
+ tiles.update()
295
+ this.stickObjectToTheGround()
296
+ this._syncGroundAlignPlaceholder(threeJSComponent)
297
+ if (this._groundAlignPlaceholderMesh) {
298
+ this._tickGroundAlignPlaceholderAnimation()
299
+ }
300
+ }
301
+
302
+ if (tilesWrapper.parent !== scene) {
303
+ if (tilesWrapper.parent) tilesWrapper.parent.remove(tilesWrapper)
304
+ scene.add(tilesWrapper)
305
+ }
306
+
307
+ threeJSComponent.meshes.overlayMeshes[this.id] = tilesWrapper
308
+ this.overlayMesh = tilesWrapper
309
+ return tilesWrapper
310
+ }
311
+
312
+ renderOnPaperJS() {
313
+ // No 2D representation for 3D Tiles overlay
314
+ }
315
+
316
+ serializeSettings() {
317
+ return {
318
+ ...super.serializeSettings(),
319
+ centerLat: this.origin?.lat ?? this.settings?.centerLat,
320
+ centerLon: this.origin?.lng ?? this.settings?.centerLon,
321
+ groundElevation: this._groundElevation,
322
+ }
323
+ }
324
+
325
+ dispose() {
326
+ if (this._groundAlignPlaceholderMesh) {
327
+ this._groundAlignPlaceholderMesh.parent?.remove(
328
+ this._groundAlignPlaceholderMesh
329
+ )
330
+ this._groundAlignPlaceholderMesh.geometry?.dispose()
331
+ if (this._groundAlignPlaceholderMesh.material?.dispose) {
332
+ this._groundAlignPlaceholderMesh.material.dispose()
333
+ }
334
+ this._groundAlignPlaceholderMesh = null
335
+ }
336
+ if (this.tilesData?.tiles) {
337
+ this.tilesData.tiles.dispose()
338
+ }
339
+ this.tilesData = null
340
+ this.overlayMesh = null
341
+ this._initPromise = null
342
+ this._tilesGroundAligned = false
343
+ }
344
+ }
@@ -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
  }
@@ -122,7 +122,6 @@ export default class ThreeDModelOverlay extends Overlay {
122
122
  return
123
123
  }
124
124
  group = new this.Paper.Group()
125
-
126
125
  group.addChild(this.renderImagePlaceHolder(paperJSComponent))
127
126
  group.itemType = 'placeholderOverlays'
128
127
  group.type = 'imageOverlays'
@@ -138,7 +137,6 @@ export default class ThreeDModelOverlay extends Overlay {
138
137
  this.hasPlaceholderPath = true
139
138
  return
140
139
  }
141
-
142
140
  if (this.hasPlaceholderPath) {
143
141
  if (group) {
144
142
  paperJSComponent.clearPath(group)
@@ -352,8 +350,24 @@ export default class ThreeDModelOverlay extends Overlay {
352
350
  resolve(imageUrl)
353
351
  })
354
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
+
355
363
  getProjectedPlanesOnOverlay(polygons) {
364
+ const mesh = this.getOverlayMeshForRaycast()
365
+ if (!mesh) {
366
+ return polygons.map(() => null)
367
+ }
368
+ mesh.updateMatrixWorld(true)
356
369
  const RayCaster = new THREE.Raycaster()
370
+ const recursive = this.getRaycastRecursive()
357
371
  const cloudsPoints = polygons.map((polygon) => {
358
372
  const outline = polygon.outline
359
373
  const holeOutlines = polygon.holes.map((h) => h.outline)
@@ -370,8 +384,8 @@ export default class ThreeDModelOverlay extends Overlay {
370
384
  let { xMin, xMax, yMin, yMax } = outlineBounds
371
385
  const cloudPoints = []
372
386
  let stepNum = 8
373
- for (let i = 1; i < stepNum; i++) {
374
- 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++) {
375
389
  const testPoint = {
376
390
  x: xMin + (i * (xMax - xMin)) / stepNum,
377
391
  y: yMin + (j * (yMax - yMin)) / stepNum,
@@ -386,7 +400,7 @@ export default class ThreeDModelOverlay extends Overlay {
386
400
  new THREE.Vector3(testPoint.x / 1000, testPoint.y / 1000, 100),
387
401
  new THREE.Vector3(0, 0, -1)
388
402
  )
389
- let intersects = RayCaster.intersectObject(this.overlayMesh)
403
+ let intersects = RayCaster.intersectObject(mesh, recursive)
390
404
  if (intersects.length > 0) {
391
405
  cloudPoints.push(intersects[0].point)
392
406
  }
@@ -440,7 +440,10 @@ export default {
440
440
  this.setTweenTo({ position, target })
441
441
  }
442
442
  },
443
- setCameraGlobalPosition() {
443
+ setCameraGlobalPositionImmediately() {
444
+ this.setCameraGlobalPosition({ duration: 0 })
445
+ },
446
+ setCameraGlobalPosition({ duration = 1500 } = {}) {
444
447
  if (this.camera.isOrthographicCamera) {
445
448
  this.setOrthographicCameraGlobalPosition()
446
449
  } else {
@@ -466,10 +469,41 @@ export default {
466
469
  positionVector.z,
467
470
  ]
468
471
  let target = [targetVector.x, targetVector.y, targetVector.z]
469
- this.setTweenTo({ position, target, fov: newFov })
472
+ this.setTweenTo({ position, target, fov: newFov, duration })
470
473
  }
471
474
  },
472
- setCameraGlobalVerticalPosition() {
475
+ setCameraGlobalAlmostVerticalPosition({ duration = 1000 } = {}) {
476
+ let position, target
477
+ if (this.roofs.length != 0) {
478
+ let bounds = this.roofsBounds
479
+ let targetVector = {
480
+ x: (bounds.xMax + bounds.xMin) / 2000,
481
+ y: (bounds.yMax + bounds.yMin) / 2000,
482
+ z: 0,
483
+ }
484
+ let objectRadius_m = Math.max(
485
+ (bounds.xMax - bounds.xMin) / 2000,
486
+ (bounds.yMax - bounds.yMin) / 2000
487
+ )
488
+
489
+ let distance =
490
+ 2 *
491
+ (objectRadius_m / Math.sin(((this.camera.fov / 2) * Math.PI) / 180))
492
+ let cameraMoveVector = { x: 0, y: 0, z: distance }
493
+ const positionVector = addVector(targetVector, cameraMoveVector)
494
+ position = [positionVector.x, positionVector.y - 1, positionVector.z]
495
+ target = [targetVector.x, targetVector.y, targetVector.z]
496
+ } else {
497
+ position = [0, -1, 300]
498
+ target = [0, 0, 0]
499
+ }
500
+ this.setTweenTo({
501
+ position,
502
+ target,
503
+ duration,
504
+ })
505
+ },
506
+ setCameraGlobalVerticalPosition(changeCameraTypeAsCallback = true) {
473
507
  let position, target
474
508
  let newFov = 10
475
509
  if (this.roofs.length != 0) {
@@ -498,10 +532,12 @@ export default {
498
532
  position,
499
533
  target,
500
534
  fov: newFov,
501
- callback: this.turnPerspectiveCameraToOrthogonal,
535
+ callback: changeCameraTypeAsCallback
536
+ ? this.turnPerspectiveCameraToOrthogonal
537
+ : undefined,
502
538
  })
503
539
  },
504
- setCameraOrthogonalTo(polygon) {
540
+ setCameraOrthogonalTo(polygon, { duration = 1000 } = {}) {
505
541
  if (polygon) {
506
542
  let polygonRef = polygon
507
543
  const targetVector = polygonRef.meanPoint
@@ -519,7 +555,7 @@ export default {
519
555
  positionVector.y / 1000 - 1,
520
556
  positionVector.z / 1000,
521
557
  ]
522
- this.setTweenTo({ position, target })
558
+ this.setTweenTo({ position, target, duration })
523
559
  }
524
560
  },
525
561
  setCameraSideTo(polygon) {
@@ -553,11 +589,24 @@ export default {
553
589
  position,
554
590
  target,
555
591
  fov = 50,
556
- duration = 2000,
592
+ duration = 1500,
557
593
  callback = () => {},
558
594
  } = payload
559
595
  TWEEN.removeAll()
560
596
  if (this.camera) {
597
+ if (duration == 0) {
598
+ this.camera.fov = fov
599
+ this.camera.position.set(position[0], position[1], position[2])
600
+
601
+ this.camera.updateProjectionMatrix()
602
+
603
+ this.orbitControl.target.set(target[0], target[1], target[2])
604
+ if (this.orbitControl && this.orbitControl.update) {
605
+ this.orbitControl.update()
606
+ }
607
+ callback()
608
+ return
609
+ }
561
610
  const currentTweenVar = {
562
611
  t: 0,
563
612
  }
@@ -695,6 +744,23 @@ export default {
695
744
  waitingBall.scale.set(scale, scale, scale)
696
745
  this.render()
697
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
+ }
698
764
  this.animationFrameId = requestAnimationFrame(this.animate)
699
765
  },
700
766
  },
@@ -60,7 +60,7 @@ export default {
60
60
  (edge.layer != 'moduleField' ||
61
61
  !this.selectedModuleField ||
62
62
  !edge.belongsTo
63
- .map((i) => i.polygonId)
63
+ .map((i) => i.itemId)
64
64
  .includes(this.selectedModuleField.id))
65
65
  ) {
66
66
  return
@@ -263,7 +263,7 @@ export function getBufferWallGeometry(polygon) {
263
263
  // itemSize = 3 because there are 3 values (components) per vertex
264
264
  geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3))
265
265
  geometry.computeVertexNormals()
266
-
266
+ geometry.userData.version = 0
267
267
  return geometry
268
268
  }
269
269
 
@@ -321,6 +321,7 @@ export function getBufferObstacleGeometry(obstaclePolygon, roofPolygon) {
321
321
  // itemSize = 3 because there are 3 values (components) per vertex
322
322
  geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3))
323
323
  geometry.computeVertexNormals()
324
+ geometry.userData.version = 0
324
325
  return geometry
325
326
  }
326
327
 
@@ -405,6 +406,7 @@ export function getBufferObstacleSideGeometry(obstaclePolygon, roofPolygon) {
405
406
  // itemSize = 3 because there are 3 values (components) per vertex
406
407
  geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3))
407
408
  geometry.computeVertexNormals()
409
+ geometry.userData.version = 0
408
410
  return geometry
409
411
  }
410
412
 
@@ -440,6 +442,7 @@ export function getBufferModuleFieldGeometry(moduleFieldPolygon) {
440
442
  geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2))
441
443
 
442
444
  geometry.computeVertexNormals()
445
+ geometry.userData.version = 0
443
446
  return geometry
444
447
  }
445
448
 
@@ -485,6 +488,7 @@ export function getBufferImageOverlayGeometry(corners) {
485
488
  geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3))
486
489
  geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2))
487
490
  geometry.computeVertexNormals()
491
+ geometry.userData.version = 0
488
492
  return geometry
489
493
  }
490
494
  export function getPanelOutlineWithHeightOffset(panelPolygon) {
@@ -525,6 +529,7 @@ export function getBufferPanelGeometry(panelPolygon) {
525
529
  geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2))
526
530
 
527
531
  geometry.computeVertexNormals()
532
+ geometry.userData.version = 0
528
533
  return geometry
529
534
  }
530
535
  export function updateBufferPanelGeometry(panelPolygon, geometry) {
@@ -635,6 +640,7 @@ export function getBufferPanelSideGeometry(panelPolygon) {
635
640
  // itemSize = 3 because there are 3 values (components) per vertex
636
641
  geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3))
637
642
  geometry.computeVertexNormals()
643
+ geometry.userData.version = 0
638
644
  return geometry
639
645
  }
640
646
 
@@ -6,9 +6,12 @@ export default {
6
6
  const mergedVertices = []
7
7
  const mergedNormals = []
8
8
  const mergedUvs = []
9
- const geometryVersion = mergedGeometry.userData.version || 0
9
+ const geometryVersion = mergedGeometry?.userData?.version || 0
10
10
  let geometryChanged = false
11
11
  geometries.forEach((geometry) => {
12
+ if (!geometry || !geometry.attributes?.position || !geometry.attributes?.normal) {
13
+ return
14
+ }
12
15
  const positions = geometry.attributes.position.array
13
16
  const normals = geometry.attributes.normal.array
14
17
  mergedVertices.push(...positions.slice(0, 3 * geometry.drawRange.count))
@@ -66,6 +69,9 @@ export default {
66
69
  const mergedUvs = []
67
70
 
68
71
  geometries.forEach((geometry) => {
72
+ if (!geometry || !geometry.attributes?.position || !geometry.attributes?.normal) {
73
+ return
74
+ }
69
75
  const positions = geometry.attributes.position.array
70
76
  const normals = geometry.attributes.normal.array
71
77
 
@@ -96,11 +102,16 @@ export default {
96
102
  },
97
103
 
98
104
  updateOrCreateMergedMesh(mesh, geometries, material) {
99
- if (!mesh) {
105
+ if (!mesh || !mesh.geometry) {
100
106
  let mergedGeometry = this.createMergedGeometry(geometries)
101
- mesh = new THREE.Mesh(mergedGeometry, material)
102
- // Add the merged mesh to the scene
103
- this.scene.add(mesh)
107
+ if (!mesh) {
108
+ mesh = new THREE.Mesh(mergedGeometry, material)
109
+ // Add the merged mesh to the scene
110
+ this.scene.add(mesh)
111
+ } else {
112
+ mesh.geometry = mergedGeometry
113
+ if (material) mesh.material = material
114
+ }
104
115
  } else {
105
116
  let mergedGeometry = this.updateMergedGeometry(
106
117
  geometries,