@mcolabs/threebox-plugin 4.0.0 → 4.0.2

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcolabs/threebox-plugin",
3
- "version": "4.0.0",
3
+ "version": "4.0.2",
4
4
  "type": "module",
5
5
  "description": "A Three.js plugin for Mapbox GL JS, using the CustomLayerInterface feature. Provides convenient methods to manage objects in lnglat coordinates, and to synchronize the map and scene cameras.",
6
6
  "main": "dist/threebox.cjs",
@@ -39,7 +39,7 @@
39
39
  "test": "tests"
40
40
  },
41
41
  "peerDependencies": {
42
- "three": ">=0.150.0"
42
+ "three": ">=0.182.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "terser": "^5.44.1",
package/src/Threebox.js CHANGED
@@ -113,7 +113,14 @@ Threebox.prototype = {
113
113
  this.map.on('style.load', function () {
114
114
  this.tb.zoomLayers = [];
115
115
  //[jscastro] if multiLayer, create a by default layer in the map, so tb.update won't be needed in client side to avoid duplicating calls to render
116
- if (this.tb.options.multiLayer) this.addLayer({ id: "threebox_layer", type: 'custom', renderingMode: '3d', map: this, onAdd: function (map, gl) { }, render: function (gl, matrix) { this.map.tb.update(); } })
116
+ if (this.tb.options.multiLayer) this.addLayer({
117
+ id: "threebox_layer",
118
+ type: 'custom',
119
+ renderingMode: '3d',
120
+ map: this,
121
+ onAdd: function (map, gl) { },
122
+ render: function (gl, matrix) { this.map.tb.update(); }
123
+ })
117
124
 
118
125
  this.once('idle', () => {
119
126
  this.tb.setObjectsScale();
@@ -681,36 +688,58 @@ Threebox.prototype = {
681
688
  // Objects
682
689
  sphere: function (options) {
683
690
  this.setDefaultView(options, this.options);
684
- return sphere(options, this.world)
691
+ let obj = sphere(options, this.world);
692
+ obj.threebox = this;
693
+ return obj;
685
694
  },
686
695
 
687
- line: line,
696
+ line: function (options) {
697
+ let obj = line(options);
698
+ obj.threebox = this;
699
+ return obj;
700
+ },
688
701
 
689
- label: label,
702
+ label: function (options) {
703
+ let obj = label(options);
704
+ obj.threebox = this;
705
+ return obj;
706
+ },
690
707
 
691
- tooltip: tooltip,
708
+ tooltip: function (options) {
709
+ let obj = tooltip(options);
710
+ obj.threebox = this;
711
+ return obj;
712
+ },
692
713
 
693
714
  tube: function (options) {
694
715
  this.setDefaultView(options, this.options);
695
- return tube(options, this.world)
716
+ let obj = tube(options, this.world);
717
+ obj.threebox = this;
718
+ return obj;
696
719
  },
697
720
 
698
721
  extrusion: function (options) {
699
722
  this.setDefaultView(options, this.options);
700
- return extrusion(options);
723
+ let obj = extrusion(options);
724
+ obj.threebox = this;
725
+ return obj;
701
726
  },
702
727
 
703
728
  Object3D: function (options) {
704
729
  this.setDefaultView(options, this.options);
705
- return Object3D(options)
730
+ let obj = Object3D(options);
731
+ obj.threebox = this;
732
+ return obj;
706
733
  },
707
734
 
708
735
  loadObj: async function loadObj(options, cb) {
709
736
  this.setDefaultView(options, this.options);
737
+ const tb = this; // capture threebox reference for callbacks
710
738
  if (options.clone === false) {
711
739
  return new Promise(
712
740
  async (resolve) => {
713
741
  loader(options, cb, async (obj) => {
742
+ obj.threebox = tb;
714
743
  resolve(obj);
715
744
  });
716
745
  });
@@ -721,7 +750,9 @@ Threebox.prototype = {
721
750
  if (cache) {
722
751
  cache.promise
723
752
  .then(obj => {
724
- cb(obj.duplicate(options));
753
+ let dupe = obj.duplicate(options);
754
+ dupe.threebox = tb;
755
+ cb(dupe);
725
756
  })
726
757
  .catch(err => {
727
758
  this.objectsCache.delete(options.obj);
@@ -732,6 +763,7 @@ Threebox.prototype = {
732
763
  promise: new Promise(
733
764
  async (resolve, reject) => {
734
765
  loader(options, cb, async (obj) => {
766
+ obj.threebox = tb;
735
767
  if (obj.duplicate) {
736
768
  resolve(obj.duplicate());
737
769
  } else {
@@ -894,7 +926,7 @@ Threebox.prototype = {
894
926
  this.labelRenderer.toggleLabels(layerId, visible);
895
927
  },
896
928
 
897
- update: function () {
929
+ update: function (matrix) {
898
930
 
899
931
  if (this.map.repaint) this.map.repaint = false
900
932
 
@@ -907,6 +939,19 @@ Threebox.prototype = {
907
939
 
908
940
  // Render the scene and repaint the map
909
941
  this.renderer.resetState(); //update threejs r126
942
+ // Reset pixel store params that Mapbox sets - these aren't allowed for 3D texture uploads
943
+ const gl = this.renderer.getContext();
944
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
945
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
946
+
947
+ // Mapbox v3 fix: Re-enable depth testing
948
+ // Mapbox v3 disables depth testing before calling custom layer render
949
+ if (this.mapboxVersion >= 3.0) {
950
+ gl.enable(gl.DEPTH_TEST);
951
+ gl.depthFunc(gl.LESS);
952
+ gl.depthMask(true);
953
+ }
954
+
910
955
  this.renderer.render(this.scene, this.camera);
911
956
 
912
957
  // [jscastro] Render any label
@@ -917,6 +962,7 @@ Threebox.prototype = {
917
962
  add: function (obj, layerId, sourceId) {
918
963
  //[jscastro] remove the tooltip if not enabled
919
964
  if (!this.enableTooltips && obj.tooltip) { obj.tooltip.visibility = false };
965
+ obj.threebox = this; // store reference to threebox instance
920
966
  this.world.add(obj);
921
967
  if (layerId) {
922
968
  obj.layer = layerId;
@@ -1035,8 +1081,9 @@ Threebox.prototype = {
1035
1081
 
1036
1082
  this.lights.dirLight.position.set(azSin, azCos, alt);
1037
1083
  this.lights.dirLight.position.multiplyScalar(radius);
1038
- this.lights.dirLight.intensity = Math.max(alt, 0);
1039
- this.lights.hemiLight.intensity = Math.max(alt * 1, 0.1);
1084
+ // Intensities scaled up for physically-based lighting (Three.js r155+)
1085
+ this.lights.dirLight.intensity = Math.max(alt, 0) * 5;
1086
+ this.lights.hemiLight.intensity = Math.max(alt * 1, 0.1) * 3;
1040
1087
  //console.log("Intensity:" + this.lights.dirLight.intensity);
1041
1088
  this.lights.dirLight.updateMatrixWorld();
1042
1089
  this.updateLightHelper();
@@ -1115,25 +1162,26 @@ Threebox.prototype = {
1115
1162
  },
1116
1163
 
1117
1164
  defaultLights: function () {
1118
-
1119
- this.lights.ambientLight = new THREE.AmbientLight(new THREE.Color('hsl(0, 0%, 100%)'), 0.75);
1165
+ // Light intensities scaled up for physically-based lighting (Three.js r155+)
1166
+ // Original values were designed for legacy lighting mode
1167
+ this.lights.ambientLight = new THREE.AmbientLight(new THREE.Color('hsl(0, 0%, 100%)'), 3);
1120
1168
  this.scene.add(this.lights.ambientLight);
1121
1169
 
1122
- this.lights.dirLightBack = new THREE.DirectionalLight(new THREE.Color('hsl(0, 0%, 100%)'), 0.25);
1170
+ this.lights.dirLightBack = new THREE.DirectionalLight(new THREE.Color('hsl(0, 0%, 100%)'), 1);
1123
1171
  this.lights.dirLightBack.position.set(30, 100, 100);
1124
1172
  this.scene.add(this.lights.dirLightBack);
1125
1173
 
1126
- this.lights.dirLight = new THREE.DirectionalLight(new THREE.Color('hsl(0, 0%, 100%)'), 0.25);
1174
+ this.lights.dirLight = new THREE.DirectionalLight(new THREE.Color('hsl(0, 0%, 100%)'), 1);
1127
1175
  this.lights.dirLight.position.set(-30, 100, -100);
1128
1176
  this.scene.add(this.lights.dirLight);
1129
1177
 
1130
1178
  },
1131
1179
 
1132
1180
  realSunlight: function (helper = false) {
1133
-
1181
+ // Light intensities scaled up for physically-based lighting (Three.js r155+)
1134
1182
  this.renderer.shadowMap.enabled = true;
1135
1183
  //this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
1136
- this.lights.dirLight = new THREE.DirectionalLight(0xffffff, 1);
1184
+ this.lights.dirLight = new THREE.DirectionalLight(0xffffff, 5);
1137
1185
  this.scene.add(this.lights.dirLight);
1138
1186
  if (helper) {
1139
1187
  this.lights.dirLightHelper = new THREE.DirectionalLightHelper(this.lights.dirLight, 5);
@@ -1148,9 +1196,9 @@ Threebox.prototype = {
1148
1196
  this.lights.dirLight.shadow.camera.bottom = this.lights.dirLight.shadow.camera.left = -d2;
1149
1197
  this.lights.dirLight.shadow.camera.near = 1;
1150
1198
  this.lights.dirLight.shadow.camera.visible = true;
1151
- this.lights.dirLight.shadow.camera.far = 400000000;
1199
+ this.lights.dirLight.shadow.camera.far = 400000000;
1152
1200
 
1153
- this.lights.hemiLight = new THREE.HemisphereLight(new THREE.Color(0xffffff), new THREE.Color(0xffffff), 0.6);
1201
+ this.lights.hemiLight = new THREE.HemisphereLight(new THREE.Color(0xffffff), new THREE.Color(0xffffff), 3);
1154
1202
  this.lights.hemiLight.color.setHSL(0.661, 0.96, 0.12);
1155
1203
  this.lights.hemiLight.groundColor.setHSL(0.11, 0.96, 0.14);
1156
1204
  this.lights.hemiLight.position.set(0, 0, 50);
@@ -133,7 +133,7 @@ AnimationManager.prototype = {
133
133
  this.animationQueue
134
134
  .push(entry);
135
135
 
136
- tb.map.repaint = true;
136
+ this.threebox.map.repaint = true;
137
137
  }
138
138
 
139
139
  //if no duration set, stop object's existing animations and go to that state immediately
@@ -183,7 +183,7 @@ AnimationManager.prototype = {
183
183
  this.animationQueue
184
184
  .push(entry);
185
185
 
186
- tb.map.repaint = true;
186
+ this.threebox.map.repaint = true;
187
187
 
188
188
  return this;
189
189
  };
@@ -251,7 +251,7 @@ AnimationManager.prototype = {
251
251
  this.setReceiveShadowFloor();
252
252
 
253
253
  this.updateMatrixWorld();
254
- tb.map.repaint = true;
254
+ this.threebox.map.repaint = true;
255
255
 
256
256
  //const threeTarget = new THREE.EventDispatcher();
257
257
  //threeTarget.dispatchEvent({ type: 'event', detail: { object: this, action: { position: options.position, rotation: options.rotation, scale: options.scale } } });
@@ -283,7 +283,7 @@ AnimationManager.prototype = {
283
283
  this.animationQueue
284
284
  .push(entry);
285
285
 
286
- tb.map.repaint = true
286
+ this.threebox.map.repaint = true;
287
287
  return this;
288
288
  }
289
289
  }
@@ -343,7 +343,7 @@ AnimationManager.prototype = {
343
343
  // Update the animation mixer and render this frame
344
344
  obj.mixer.update(0.01);
345
345
  }
346
- tb.map.repaint = true;
346
+ this.threebox.map.repaint = true;
347
347
  return this;
348
348
  }
349
349
 
@@ -458,7 +458,7 @@ AnimationManager.prototype = {
458
458
  object.isPlaying = true;
459
459
  object.animationMethod = requestAnimationFrame(this.update);
460
460
  object.mixer.update(object.clock.getDelta());
461
- tb.map.repaint = true;
461
+ object.threebox.map.repaint = true;
462
462
  }
463
463
 
464
464
  }
@@ -13,11 +13,13 @@ function CameraSync(map, camera, world) {
13
13
  this.active = true;
14
14
 
15
15
  this.camera.matrixAutoUpdate = false; // We're in charge of the camera now!
16
+ this.camera.matrixWorldAutoUpdate = false; // Three.js r150+: prevent auto world matrix recalculation
16
17
 
17
18
  // Postion and configure the world group so we can scale it appropriately when the camera zooms
18
19
  this.world = world || new THREE.Group();
19
20
  this.world.position.x = this.world.position.y = ThreeboxConstants.WORLD_SIZE / 2
20
21
  this.world.matrixAutoUpdate = false;
22
+ this.world.matrixWorldAutoUpdate = false; // Three.js r150+: prevent auto world matrix recalculation
21
23
 
22
24
  // set up basic camera state
23
25
  this.state = {
@@ -101,7 +103,7 @@ CameraSync.prototype = {
101
103
  // someday @ansis set further near plane to fix precision for deckgl,so we should fix it to use mapbox-gl v1.3+ correctly
102
104
  // https://github.com/mapbox/mapbox-gl-js/commit/5cf6e5f523611bea61dae155db19a7cb19eb825c#diff-5dddfe9d7b5b4413ee54284bc1f7966d
103
105
  const nz = (t.height / 50); //min near z as coded by @ansis
104
- const nearZ = Math.max(nz * pitchAngle, nz); //on changes in the pitch nz could be too low
106
+ let nearZ = Math.max(nz * pitchAngle, nz); //on changes in the pitch nz could be too low
105
107
 
106
108
  const h = t.height;
107
109
  const w = t.width;
@@ -118,6 +120,7 @@ CameraSync.prototype = {
118
120
  let cameraWorldMatrix = this.calcCameraMatrix(t._pitch, t.angle);
119
121
  // When terrain layers are included, height of 3D layers must be modified from t_camera.z * worldSize
120
122
  if (t.elevation) cameraWorldMatrix.elements[14] = t._camera.position[2] * worldSize;
123
+
121
124
  //this.camera.matrixWorld.elements is equivalent to t._camera._transform
122
125
  this.camera.matrixWorld.copy(cameraWorldMatrix);
123
126
 
@@ -14,9 +14,14 @@ class BuildingShadows {
14
14
  this.map = map;
15
15
  // find layer source
16
16
  const sourceName = this.map.getLayer(this.buildingsLayerId).source;
17
- this.source = (this.map.style.sourceCaches || this.map.style._otherSourceCaches)[sourceName];
17
+ // Handle Mapbox v1, v2, and v3 internal API differences for source cache access
18
+ const style = this.map.style;
19
+ this.source = style.sourceCaches?.[sourceName] ||
20
+ style._otherSourceCaches?.[sourceName] ||
21
+ style._sourceCaches?.[sourceName];
22
+
18
23
  if (!this.source) {
19
- console.warn(`Can't find layer ${this.buildingsLayerId}'s source.`);
24
+ console.warn(`BuildingShadows: Can't find layer ${this.buildingsLayerId}'s source.`);
20
25
  }
21
26
 
22
27
  // vertex shader of fill-extrusion layer is different in mapbox v1 and v2.
@@ -30,13 +35,22 @@ class BuildingShadows {
30
35
  const vertexShader = gl.createShader(gl.VERTEX_SHADER);
31
36
  gl.shaderSource(vertexShader, vertexSource);
32
37
  gl.compileShader(vertexShader);
38
+ if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
39
+ console.error('BuildingShadows vertex shader error:', gl.getShaderInfoLog(vertexShader));
40
+ }
33
41
  const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
34
42
  gl.shaderSource(fragmentShader, fragmentSource);
35
43
  gl.compileShader(fragmentShader);
44
+ if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
45
+ console.error('BuildingShadows fragment shader error:', gl.getShaderInfoLog(fragmentShader));
46
+ }
36
47
  this.program = gl.createProgram();
37
48
  gl.attachShader(this.program, vertexShader);
38
49
  gl.attachShader(this.program, fragmentShader);
39
50
  gl.linkProgram(this.program);
51
+ if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) {
52
+ console.error('BuildingShadows program link error:', gl.getProgramInfoLog(this.program));
53
+ }
40
54
  gl.validateProgram(this.program);
41
55
  this.uMatrix = gl.getUniformLocation(this.program, "u_matrix");
42
56
  this.uHeightFactor = gl.getUniformLocation(this.program, "u_height_factor");
@@ -64,19 +78,31 @@ class BuildingShadows {
64
78
  const pos = this.tb.getSunPosition(this.tb.lightDateTime, [lng, lat]);
65
79
  gl.uniform1f(this.uAltitude, (pos.altitude > this.minAltitude ? pos.altitude : 0));
66
80
  gl.uniform1f(this.uAzimuth, pos.azimuth + 3 * Math.PI / 2);
67
- //this.opacity = Math.sin(Math.max(pos.altitude, 0)) * 0.6;
68
81
  gl.enable(gl.BLEND);
69
- //gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.DST_ALPHA, gl.SRC_ALPHA);
70
82
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
71
- var ext = gl.getExtension('EXT_blend_minmax');
72
- //gl.blendEquationSeparate(gl.FUNC_SUBTRACT, ext.MIN_EXT);
73
- //gl.blendEquation(gl.FUNC_ADD);
74
83
  gl.disable(gl.DEPTH_TEST);
75
84
  for (const coord of coords) {
76
85
  const tile = this.source.getTile(coord);
77
- const bucket = tile.getBucket(buildingsLayer);
86
+
87
+ let bucket = tile.getBucket(buildingsLayer);
88
+ // Mapbox v3 fallback: try direct bucket access by layer ID
89
+ if (!bucket && tile.buckets) {
90
+ bucket = tile.buckets[this.buildingsLayerId];
91
+ }
78
92
  if (!bucket) continue;
79
- const [heightBuffer, baseBuffer] = bucket.programConfigurations.programConfigurations[this.buildingsLayerId]._buffers;
93
+
94
+ // Handle Mapbox v3 internal API changes for buffer access
95
+ let heightBuffer, baseBuffer;
96
+ const programConfig = bucket.programConfigurations?.programConfigurations?.[this.buildingsLayerId];
97
+ if (programConfig?._buffers) {
98
+ [heightBuffer, baseBuffer] = programConfig._buffers;
99
+ } else if (programConfig?.getBuffers) {
100
+ const buffers = programConfig.getBuffers();
101
+ heightBuffer = buffers[0];
102
+ baseBuffer = buffers[1];
103
+ } else {
104
+ continue;
105
+ }
80
106
  gl.uniformMatrix4fv(this.uMatrix, false, (coord.posMatrix || coord.projMatrix));
81
107
  gl.uniform1f(this.uHeightFactor, Math.pow(2, coord.overscaledZ) / tile.tileSize / 8);
82
108
  for (const segment of bucket.segments.get()) {
@@ -160,4 +186,4 @@ class BuildingShadows {
160
186
  }
161
187
  }
162
188
 
163
- export default BuildingShadows;
189
+ export default BuildingShadows;
@@ -43,10 +43,16 @@ function loadObj(options, cb, promise) {
43
43
  break;
44
44
  }
45
45
 
46
- materialLoader.withCredentials = options.withCredentials;
47
- materialLoader.load(options.mtl, loadObject, () => (null), error => {
48
- console.warn("No material file found " + error.stack);
49
- });
46
+ // Only load MTL if specified, otherwise proceed directly
47
+ if (options.mtl) {
48
+ materialLoader.withCredentials = options.withCredentials;
49
+ materialLoader.load(options.mtl, loadObject, () => (null), error => {
50
+ console.warn("No material file found " + error.stack);
51
+ loadObject(null); // Proceed without materials on error
52
+ });
53
+ } else {
54
+ loadObject(null); // No MTL specified, proceed directly
55
+ }
50
56
 
51
57
  function loadObject(materials) {
52
58
 
@@ -80,6 +86,8 @@ function loadObj(options, cb, promise) {
80
86
  const s = utils.types.scale(options.scale, [1, 1, 1]);
81
87
  obj.rotation.set(r[0], r[1], r[2]);
82
88
  obj.scale.set(s[0], s[1], s[2]);
89
+ // Fix materials for Three.js r152+ color management and transparency
90
+ // fixMaterials(obj);
83
91
  // [jscastro] normalize specular/metalness/shininess from meshes in FBX and GLB model as it would need 5 lights to illuminate them properly
84
92
  if (options.normalize) { normalizeSpecular(obj); }
85
93
  obj.name = "model";
@@ -107,7 +115,44 @@ function loadObj(options, cb, promise) {
107
115
 
108
116
  };
109
117
 
118
+ // // Fix materials for Three.js r132+ transparency and r152+ color management
119
+ // function fixMaterials(model) {
120
+ // model.traverse(function (c) {
121
+ // if (c.isMesh) {
122
+ // let materials = Array.isArray(c.material) ? c.material : [c.material];
123
+ // materials.forEach(function (mat) {
124
+ // if (!mat) return;
125
+
126
+ // // Set colorSpace for color textures (Three.js r152+)
127
+ // if (mat.map) mat.map.colorSpace = THREE.SRGBColorSpace;
128
+ // if (mat.emissiveMap) mat.emissiveMap.colorSpace = THREE.SRGBColorSpace;
129
+
130
+ // // Fix GLTF transparency (Three.js r132+ issue)
131
+ // // Since r132, setting transparent=true on GLTF materials doesn't work properly
132
+ // // Need to use custom blending for transparency to display correctly
133
+ // let isTransparent = mat.transparent || mat.opacity < 1 || mat.alphaMap || mat.alphaTest > 0;
134
+
135
+ // // Also check for alpha in the base color texture
136
+ // if (mat.map && mat.map.format === THREE.RGBAFormat) {
137
+ // isTransparent = true;
138
+ // }
139
+
140
+ // if (isTransparent) {
141
+ // mat.transparent = true;
142
+ // mat.depthWrite = false;
143
+ // mat.blending = THREE.CustomBlending;
144
+ // mat.blendSrc = THREE.SrcAlphaFactor;
145
+ // mat.blendDst = THREE.OneMinusSrcAlphaFactor;
146
+ // mat.blendEquation = THREE.AddEquation;
147
+ // mat.needsUpdate = true;
148
+ // }
149
+ // });
150
+ // }
151
+ // });
152
+ // }
153
+
110
154
  //[jscastro] some FBX/GLTF models have too much specular effects for mapbox
155
+ // Multipliers adjusted for physically-based lighting (Three.js r155+)
111
156
  function normalizeSpecular(model) {
112
157
  model.traverse(function (c) {
113
158
 
@@ -116,13 +161,13 @@ function loadObj(options, cb, promise) {
116
161
  let specularColor;
117
162
  if (c.material.type == 'MeshStandardMaterial') {
118
163
 
119
- if (c.material.metalness) { c.material.metalness *= 0.1; }
120
- if (c.material.glossiness) { c.material.glossiness *= 0.25; }
121
- specularColor = new THREE.Color(12, 12, 12);
164
+ if (c.material.metalness) { c.material.metalness *= 0.3; }
165
+ if (c.material.glossiness) { c.material.glossiness *= 0.5; }
166
+ specularColor = new THREE.Color(0x0c0c0c);
122
167
 
123
168
  } else if (c.material.type == 'MeshPhongMaterial') {
124
- c.material.shininess = 0.1;
125
- specularColor = new THREE.Color(20, 20, 20);
169
+ c.material.shininess = 0.3;
170
+ specularColor = new THREE.Color(0x141414);
126
171
  }
127
172
  if (c.material.specular && c.material.specular.isColor) {
128
173
  c.material.specular = specularColor;
@@ -175,7 +175,7 @@ Objects.prototype = {
175
175
  model.position.add(point); // re-add the offset
176
176
  model.rotateOnAxis(axis, theta)
177
177
 
178
- tb.map.repaint = true;
178
+ obj.threebox.map.repaint = true;
179
179
  }
180
180
 
181
181
 
@@ -818,13 +818,14 @@ Objects.prototype = {
818
818
  })
819
819
 
820
820
  obj.scaleGroup.remove(o);
821
- tb.map.repaint = true;
821
+ obj.threebox.map.repaint = true;
822
822
  }
823
823
 
824
824
  //[jscastro] clone + assigning all the attributes
825
825
  obj.duplicate = function (options) {
826
826
 
827
827
  let dupe = obj.clone(true); //clone the whole threebox object
828
+ dupe.threebox = obj.threebox; // copy threebox reference to the duplicate
828
829
  dupe.getObjectByName("model").animations = obj.animations; //we must set this explicitly before addMethods
829
830
  if (dupe.userData.feature) {
830
831
  if (options && options.feature) dupe.userData.feature = options.feature;