MikaFil commited on
Commit
9fa27bc
·
verified ·
1 Parent(s): 6dd7ab7

Update viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +494 -447
viewer.js CHANGED
@@ -10,479 +10,526 @@ let bouchonEntity = null;
10
  let viewerInitialized = false;
11
  let resizeObserver = null;
12
 
 
13
  let matTransparent = null;
14
  let matOpaque = null;
15
  let tubeTransparent = null;
16
  let tubeOpaque = null;
17
 
 
18
  let chosenCameraX, chosenCameraY, chosenCameraZ;
19
  let minZoom, maxZoom, minAngle, maxAngle, minAzimuth, maxAzimuth, minPivotY, minY;
20
  let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
21
  let glbUrl, glbUrl2, glbUrl3, aoUrl, opacityUrl, thicknessUrl;
22
 
23
- let tubeRenderTarget = null;
24
- let tubeTexture = null;
25
- let rtCameraEntity = null;
26
- let tubeBgPlane = null;
27
-
28
  function traverse(entity, callback) {
29
- callback(entity);
30
- if (entity.children) {
31
- entity.children.forEach(child => traverse(child, callback));
32
- }
33
  }
34
 
35
  export async function initializeViewer(config, instanceId) {
36
- if (viewerInitialized) return;
37
-
38
- const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
39
- const isMobile = isIOS || /Android/i.test(navigator.userAgent);
40
-
41
- glbUrl = config.glb_url;
42
- glbUrl2 = config.glb_url_2;
43
- glbUrl3 = config.glb_url_3;
44
- aoUrl = config.ao_url;
45
- thicknessUrl = config.thickness_url;
46
- opacityUrl = config.opacity_url;
47
- minZoom = 0.2;
48
- maxZoom = 2;
49
- minAngle = -Infinity;
50
- maxAngle = Infinity;
51
- minAzimuth = -Infinity;
52
- maxAzimuth = Infinity;
53
- minPivotY = 0;
54
- minY = -10;
55
-
56
- modelX = 0; modelY = 0; modelZ = 0;
57
- modelScale = 1;
58
- modelRotationX = 0; modelRotationY = 0; modelRotationZ = 0;
59
-
60
- const cameraX = 0, cameraY = 0, cameraZ = 1;
61
- chosenCameraX = isMobile ? cameraX : cameraX;
62
- chosenCameraY = isMobile ? cameraY : cameraY;
63
- chosenCameraZ = isMobile ? cameraZ : cameraZ;
64
-
65
- const canvasId = 'canvas-' + instanceId;
66
- const progressDialog = document.getElementById('progress-dialog-' + instanceId);
67
- const viewerContainer = document.getElementById('viewer-container-' + instanceId);
68
-
69
- let oldCanvas = document.getElementById(canvasId);
70
- if (oldCanvas) oldCanvas.remove();
71
-
72
- const canvas = document.createElement('canvas');
73
- canvas.id = canvasId;
74
- canvas.className = 'ply-canvas';
75
- canvas.style.width = "100%";
76
- canvas.style.height = "100%";
77
- canvas.setAttribute('tabindex', '0');
78
- if (progressDialog) viewerContainer.insertBefore(canvas, progressDialog);
79
- else viewerContainer.appendChild(canvas);
80
-
81
- canvas.style.touchAction = "none";
82
- canvas.style.webkitTouchCallout = "none";
83
- canvas.addEventListener('gesturestart', e => e.preventDefault());
84
- canvas.addEventListener('gesturechange', e => e.preventDefault());
85
- canvas.addEventListener('gestureend', e => e.preventDefault());
86
- canvas.addEventListener('dblclick', e => e.preventDefault());
87
- canvas.addEventListener('touchstart', e => { if (e.touches.length > 1) e.preventDefault(); }, { passive: false });
88
- canvas.addEventListener('wheel', (e) => { e.preventDefault(); }, { passive: false });
89
-
90
- if (progressDialog) progressDialog.style.display = 'block';
91
-
92
- if (!pc) {
93
- pc = await import("https://esm.run/playcanvas");
94
- window.pc = pc;
95
- }
96
-
97
- // --- Graphics Device/Options ---
98
- const device = await pc.createGraphicsDevice(canvas, {
99
- deviceTypes: [isIOS ? "webgl1" : "webgl2", "webgl1"], // Prefer WebGL1 on iOS
100
- glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
101
- twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
102
- antialias: false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  });
104
- device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
105
-
106
- const opts = new pc.AppOptions();
107
- opts.graphicsDevice = device;
108
- opts.mouse = new pc.Mouse(document.body);
109
- opts.touch = new pc.TouchDevice(document.body);
110
- opts.keyboard = new pc.Keyboard(document.body);
111
- opts.componentSystems = [
112
- pc.RenderComponentSystem,
113
- pc.CameraComponentSystem,
114
- pc.LightComponentSystem,
115
- pc.ScriptComponentSystem,
116
- pc.GSplatComponentSystem,
117
- pc.CollisionComponentSystem,
118
- pc.RigidbodyComponentSystem
119
- ];
120
- opts.resourceHandlers = [
121
- pc.TextureHandler,
122
- pc.ContainerHandler,
123
- pc.ScriptHandler,
124
- pc.GSplatHandler
125
- ];
126
-
127
- app = new pc.AppBase(canvas);
128
- app.init(opts);
129
-
130
- app.setCanvasFillMode(pc.FILLMODE_NONE);
131
- app.setCanvasResolution(pc.RESOLUTION_AUTO);
132
-
133
- // Resize logic
134
- const resize = () => app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
135
- window.addEventListener('resize', resize);
136
- app.on('destroy', () => { window.removeEventListener('resize', resize); });
137
-
138
- // --- Assets ---
139
- const assets = {
140
- orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
141
- model: new pc.Asset('glb', 'container', { url: glbUrl }),
142
- tube: new pc.Asset('glb', 'container', { url: glbUrl2 }),
143
- bouchon: new pc.Asset('glb', 'container', { url: glbUrl3 }),
144
- ao_map: new pc.Asset('color', 'texture', { url: aoUrl }),
145
- op_map: new pc.Asset('color', 'texture', { url: opacityUrl }),
146
- bg_tex: new pc.Asset('color', 'texture', { url: "https://huggingface.co/datasets/MikaFil/3D_models/resolve/main/EARCARE/banniere_earcare.png" }),
147
- thickness_map: new pc.Asset('color', 'texture', { url: thicknessUrl }),
148
- helipad: new pc.Asset(
149
- 'helipad-env-atlas', 'texture',
150
- { url: "https://huggingface.co/datasets/MikaFil/3D_models/resolve/main/EARCARE/hdr/ciel_nuageux_1k.png" },
151
- { type: pc.TEXTURETYPE_RGBP, mipmaps: false }
152
- )
153
- };
154
- for (const key in assets) app.assets.add(assets[key]);
155
-
156
- // Load HDR *before* loading any models
157
- assets.helipad.ready(() => {
158
- app.scene.envAtlas = assets.helipad.resource;
159
- app.scene.skyboxRotation = new pc.Quat().setFromEulerAngles(30, 20, 0);
160
- app.scene.skyboxIntensity = 4;
161
- app.scene.skyboxMip = 0;
162
-
163
- // Load all GLBs & textures
164
- const loader = new pc.AssetListLoader([
165
- assets.orbit, assets.model, assets.tube, assets.bouchon,
166
- assets.ao_map, assets.op_map, assets.bg_tex, assets.thickness_map
167
- ], app.assets);
168
-
169
- loader.load(() => {
170
- app.start();
171
- if (progressDialog) progressDialog.style.display = 'none';
172
-
173
- // Reorder depth layer
174
- const depthLayer = app.scene.layers.getLayerById(pc.LAYERID_DEPTH);
175
- app.scene.layers.remove(depthLayer);
176
- app.scene.layers.insertOpaque(depthLayer, 2);
177
-
178
- // Entities
179
- modelEntity = assets.model.resource.instantiateRenderEntity();
180
- tubeEntity = assets.tube.resource.instantiateRenderEntity();
181
- bouchonEntity = assets.bouchon.resource.instantiateRenderEntity();
182
-
183
- app.root.addChild(modelEntity);
184
- app.root.addChild(tubeEntity);
185
-
186
- // --- Render Target For Tube ---
187
- const rtWidth = device.width;
188
- const rtHeight = device.height;
189
- tubeTexture = new pc.Texture(device, {
190
- width: rtWidth,
191
- height: rtHeight,
192
- format: pc.PIXELFORMAT_R8_G8_B8_A8,
193
- autoMipmap: false
194
- });
195
- tubeRenderTarget = new pc.RenderTarget({
196
- colorBuffer: tubeTexture,
197
- depth: true
198
- });
199
-
200
- // 2. Materials
201
- const ao = assets.ao_map.resource;
202
- const op = assets.op_map.resource;
203
- const thickness = assets.thickness_map.resource;
204
-
205
- matTransparent = new pc.StandardMaterial();
206
- matTransparent.blendType = pc.BLEND_NORMAL;
207
- matTransparent.diffuse = new pc.Color(0.7, 0.05, 0.05);
208
- matTransparent.specular = new pc.Color(0.01, 0.01, 0.01);
209
- matTransparent.shininess = 90;
210
- matTransparent.gloss = 1;
211
- matTransparent.metalness = 0.0;
212
- matTransparent.useMetalness = true;
213
- matTransparent.depthWrite = true;
214
- matTransparent.refraction = 0.8;
215
- matTransparent.refractionIndex = 1;
216
- matTransparent.thickness = 0.02;
217
- matTransparent.thicknessMap = thickness;
218
- matTransparent.opacityMap = op;
219
- matTransparent.opacityMapChannel = "r";
220
- matTransparent.opacity = 0.97;
221
- matTransparent.emissive = new pc.Color(0.372, 0.03, 0.003);
222
- matTransparent.emissiveMap = ao;
223
- matTransparent.emissiveIntensity = 0.1;
224
- matTransparent.update();
225
-
226
- matOpaque = new pc.StandardMaterial();
227
- matOpaque.blendType = pc.BLEND_NORMAL;
228
- matOpaque.diffuse = new pc.Color(0.7, 0.05, 0.05);
229
- matOpaque.specular = new pc.Color(0.01, 0.01, 0.01);
230
- matOpaque.shininess = 90;
231
- matOpaque.gloss = 1;
232
- matOpaque.metalness = 0.0;
233
- matOpaque.opacityMap = op;
234
- matOpaque.opacityMapChannel = "r";
235
- matOpaque.opacity = 1;
236
- matOpaque.emissive = new pc.Color(0.372, 0.03, 0.003);
237
- matOpaque.emissiveMap = ao;
238
- matOpaque.emissiveIntensity = 2;
239
- matOpaque.update();
240
-
241
- tubeTransparent = new pc.StandardMaterial();
242
- tubeTransparent.diffuse = new pc.Color(1, 1, 1);
243
- tubeTransparent.blendType = pc.BLEND_NORMAL;
244
- tubeTransparent.opacity = 0.1;
245
- tubeTransparent.depthTest = false;
246
- tubeTransparent.depthWrite = false;
247
- tubeTransparent.useMetalness = true;
248
- tubeTransparent.thickness = 4;
249
- tubeTransparent.update();
250
-
251
- tubeOpaque = new pc.StandardMaterial();
252
- tubeOpaque.diffuse = new pc.Color(1, 1, 1);
253
- tubeOpaque.opacity = 0.97;
254
- tubeOpaque.update();
255
-
256
- traverse(modelEntity, node => {
257
- if (node.render && node.render.meshInstances) {
258
- for (let mi of node.render.meshInstances) {
259
- mi.material = matOpaque;
260
- }
261
- }
262
- });
263
-
264
- traverse(tubeEntity, node => {
265
- if (node.render && node.render.meshInstances) {
266
- for (let mi of node.render.meshInstances) {
267
- mi.material = tubeTransparent;
268
- }
269
- }
270
- });
271
-
272
- // --- 3. Position/scale/orientation
273
- modelEntity.setPosition(modelX, modelY, modelZ);
274
- modelEntity.setLocalScale(modelScale, modelScale, modelScale);
275
- modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
276
-
277
- tubeEntity.setPosition(modelX, modelY, modelZ);
278
- tubeEntity.setLocalScale(modelScale, modelScale, modelScale);
279
- tubeEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
280
-
281
- bouchonEntity.setPosition(modelX, modelY, modelZ);
282
- bouchonEntity.setLocalScale(modelScale, modelScale, modelScale);
283
- bouchonEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
284
-
285
- // --- 4. Camera & orbit
286
- // (A) RENDER-TARGET CAMERA for Tube
287
- rtCameraEntity = new pc.Entity('rt-camera');
288
- rtCameraEntity.addComponent('camera', {
289
- clearColor: new pc.Color(1, 1, 1, 0), // Transparent BG
290
- fov: 60,
291
- frustumCulling: true,
292
- });
293
- rtCameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
294
- rtCameraEntity.lookAt(modelEntity.getPosition());
295
- app.root.addChild(rtCameraEntity);
296
-
297
- // (B) MAIN CAMERA for Scene (and shell)
298
- cameraEntity = new pc.Entity('camera');
299
- cameraEntity.addComponent('camera', {
300
- clearColor: new pc.Color(1, 1, 1, 1),
301
- toneMapping: pc.TONEMAP_NEUTRAL
302
- });
303
- cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
304
- cameraEntity.lookAt(modelEntity.getPosition());
305
- cameraEntity.addComponent('script');
306
- cameraEntity.camera.requestSceneColorMap(true);
307
- cameraEntity.script.create('orbitCamera', {
308
- attributes: {
309
- focusEntity: modelEntity,
310
- inertiaFactor: 0.2,
311
- distanceMax: maxZoom,
312
- distanceMin: minZoom,
313
- pitchAngleMax: maxAngle,
314
- pitchAngleMin: minAngle,
315
- yawAngleMax: maxAzimuth,
316
- yawAngleMin: minAzimuth,
317
- minY: minY,
318
- frameOnStart: false
319
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
  });
321
- cameraEntity.script.create('orbitCameraInputMouse');
322
- cameraEntity.script.create('orbitCameraInputTouch');
323
- app.root.addChild(cameraEntity);
324
-
325
- // Remove the Skybox layer from the camera
326
- const skyboxLayer = app.scene.layers.getLayerByName("Skybox");
327
- const camLayers = cameraEntity.camera.layers.slice();
328
- const idx = camLayers.indexOf(skyboxLayer.id);
329
- if (idx !== -1) {
330
- camLayers.splice(idx, 1);
331
- cameraEntity.camera.layers = camLayers;
332
- }
333
 
334
- // --- 5. Tube Render Texture as Background
335
- // Create a plane that will display the tube render
336
- tubeBgPlane = new pc.Entity("TubeBG");
337
- tubeBgPlane.addComponent("model", { type: "plane" });
338
- tubeBgPlane.setLocalScale(10, 1, 10);
339
- tubeBgPlane.setLocalPosition(0, 0, -0.2);
340
- tubeBgPlane.setLocalEulerAngles(90, 0, 0);
341
- const tubeBgMat = new pc.StandardMaterial();
342
- tubeBgMat.diffuse = new pc.Color(1, 1, 1);
343
- tubeBgMat.diffuseMap = tubeTexture;
344
- tubeBgMat.opacityMap = tubeTexture;
345
- tubeBgMat.blendType = pc.BLEND_NORMAL;
346
- tubeBgMat.opacity = 1.0;
347
- tubeBgMat.update();
348
- tubeBgPlane.model.material = tubeBgMat;
349
- cameraEntity.addChild(tubeBgPlane);
350
-
351
- // 6. Light
352
- const light = new pc.Entity("mainLight");
353
- light.addComponent('light', {
354
- type: "directional",
355
- color: new pc.Color(1, 1, 1),
356
- intensity: 0.9,
357
- });
358
- light.setPosition(0, 1, -1);
359
- light.lookAt(0, 0, 0);
360
- app.root.addChild(light);
361
-
362
- app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
363
-
364
- // --- Render Loop Hook ---
365
- app.on('prerender', function () {
366
- // Hide shell, render tube only to RT
367
- modelEntity.enabled = false;
368
- tubeEntity.enabled = true;
369
- rtCameraEntity.enabled = true;
370
- cameraEntity.enabled = false;
371
- app.graphicsDevice.renderTarget = tubeRenderTarget;
372
- app.render();
373
- app.graphicsDevice.renderTarget = null;
374
- // Restore shell, render shell (and tube RT) to main camera
375
- modelEntity.enabled = true;
376
- tubeEntity.enabled = false;
377
- rtCameraEntity.enabled = false;
378
- cameraEntity.enabled = true;
379
- });
380
 
381
- // --- Reset camera after update
382
- app.once('update', () => resetViewerCamera());
383
-
384
- // Tooltips supported if tooltips_url set
385
- try {
386
- if (config.tooltips_url) {
387
- import('./tooltips.js').then(tooltipsModule => {
388
- tooltipsModule.initializeTooltips({
389
- app,
390
- cameraEntity,
391
- modelEntity,
392
- tooltipsUrl: config.tooltips_url,
393
- defaultVisible: !!config.showTooltipsDefault,
394
- moveDuration: config.tooltipMoveDuration || 0.6
395
- });
396
- }).catch(e => { /* ignore */ });
397
- }
398
- } catch (e) {}
399
-
400
- viewerInitialized = true;
401
- });
402
- });
403
- app.assets.load(assets.helipad);
404
  }
405
 
406
- // -- Reset camera etc...
407
  export function resetViewerCamera() {
408
- try {
409
- if (!cameraEntity || !modelEntity || !app) return;
410
- const orbitCam = cameraEntity.script.orbitCamera;
411
- if (!orbitCam) return;
412
- const modelPos = modelEntity.getPosition();
413
- const tempEnt = new pc.Entity();
414
- tempEnt.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
415
- tempEnt.lookAt(modelPos);
416
- const dist = new pc.Vec3().sub2(
417
- new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ),
418
- modelPos
419
- ).length();
420
- cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
421
- cameraEntity.lookAt(modelPos);
422
- orbitCam.pivotPoint = modelPos.clone();
423
- orbitCam._targetDistance = dist;
424
- orbitCam._distance = dist;
425
- const rot = tempEnt.getRotation();
426
- const fwd = new pc.Vec3();
427
- rot.transformVector(pc.Vec3.FORWARD, fwd);
428
- const yaw = Math.atan2(-fwd.x, -fwd.z) * pc.math.RAD_TO_DEG;
429
- const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
430
- const rotNoYaw = new pc.Quat().mul2(yawQuat, rot);
431
- const fNoYaw = new pc.Vec3();
432
- rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
433
- const pitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
434
- orbitCam._targetYaw = yaw;
435
- orbitCam._yaw = yaw;
436
- orbitCam._targetPitch = pitch;
437
- orbitCam._pitch = pitch;
438
- if (orbitCam._updatePosition) orbitCam._updatePosition();
439
- tempEnt.destroy();
440
- } catch (e) { }
 
 
 
 
 
 
 
 
 
 
441
  }
442
 
 
 
 
 
443
  export function changeColor(dr, dg, db, er, eg, eb, ei, op, boolTrans) {
444
- if (boolTrans) {
445
- if (!matTransparent) return;
446
- matTransparent.diffuse.set(dr, dg, db);
447
- matTransparent.emissive.set(er, eg, eb);
448
- matTransparent.emissiveIntensity = ei;
449
- matTransparent.opacity = op;
450
- matTransparent.update();
451
- tubeTransparent.diffuse.set(dr, dg, db);
452
- tubeTransparent.update();
453
- traverse(modelEntity, node => {
454
- if (node.render && node.render.meshInstances) {
455
- for (let mi of node.render.meshInstances) {
456
- mi.material = matTransparent;
457
- }
 
 
 
 
458
  }
459
- });
460
- traverse(tubeEntity, node => {
461
- if (node.render && node.render.meshInstances) {
462
- for (let mi of node.render.meshInstances) {
463
- mi.material = tubeTransparent;
464
- }
 
465
  }
466
- });
467
- } else {
468
- if (!matOpaque) return;
469
- matOpaque.diffuse.set(dr, dg, db);
470
- matOpaque.emissive.set(er, eg, eb);
471
- matOpaque.emissiveIntensity = ei;
472
- matOpaque.update();
473
- traverse(modelEntity, node => {
474
- if (node.render && node.render.meshInstances) {
475
- for (let mi of node.render.meshInstances) {
476
- mi.material = matOpaque;
477
- }
 
 
478
  }
479
- });
480
- traverse(tubeEntity, node => {
481
- if (node.render && node.render.meshInstances) {
482
- for (let mi of node.render.meshInstances) {
483
- mi.material = tubeOpaque;
484
- }
 
485
  }
486
- });
487
- }
 
488
  }
 
10
  let viewerInitialized = false;
11
  let resizeObserver = null;
12
 
13
+ // These will hold references to the material(s) you want to update.
14
  let matTransparent = null;
15
  let matOpaque = null;
16
  let tubeTransparent = null;
17
  let tubeOpaque = null;
18
 
19
+
20
  let chosenCameraX, chosenCameraY, chosenCameraZ;
21
  let minZoom, maxZoom, minAngle, maxAngle, minAzimuth, maxAzimuth, minPivotY, minY;
22
  let modelX, modelY, modelZ, modelScale, modelRotationX, modelRotationY, modelRotationZ;
23
  let glbUrl, glbUrl2, glbUrl3, aoUrl, opacityUrl, thicknessUrl;
24
 
25
+ // RECURSIVE ENTITY TRAVERSAL (needed for material assignment)
 
 
 
 
26
  function traverse(entity, callback) {
27
+ callback(entity);
28
+ if (entity.children) {
29
+ entity.children.forEach(child => traverse(child, callback));
30
+ }
31
  }
32
 
33
  export async function initializeViewer(config, instanceId) {
34
+ if (viewerInitialized) return;
35
+
36
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
37
+ const isMobile = isIOS || /Android/i.test(navigator.userAgent);
38
+
39
+ glbUrl = config.glb_url;
40
+ glbUrl2 = config.glb_url_2;
41
+ glbUrl3 = config.glb_url_3;
42
+ aoUrl = config.ao_url;
43
+ thicknessUrl = config.thickness_url;
44
+ opacityUrl = config.opacity_url;
45
+ minZoom = 0.2;
46
+ maxZoom = 2;
47
+ minAngle = -Infinity;
48
+ maxAngle = Infinity;
49
+ minAzimuth = -Infinity;
50
+ maxAzimuth = Infinity;
51
+ minPivotY = 0;
52
+ minY = -10;
53
+
54
+ modelX = 0;
55
+ modelY = 0;
56
+ modelZ = 0;
57
+ modelScale = 1;
58
+ modelRotationX = 0;
59
+ modelRotationY = 0;
60
+ modelRotationZ = 0;
61
+
62
+ const cameraX = 0;
63
+ const cameraY = 0;
64
+ const cameraZ = 1;
65
+ const cameraXPhone = 0;
66
+ const cameraYPhone = 0;
67
+ const cameraZPhone = 1;
68
+
69
+ chosenCameraX = isMobile ? cameraXPhone : cameraX;
70
+ chosenCameraY = isMobile ? cameraYPhone : cameraY;
71
+ chosenCameraZ = isMobile ? cameraZPhone : cameraZ;
72
+
73
+
74
+
75
+ const canvasId = 'canvas-' + instanceId;
76
+ const progressDialog = document.getElementById('progress-dialog-' + instanceId);
77
+ const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
78
+ const viewerContainer = document.getElementById('viewer-container-' + instanceId);
79
+
80
+ let oldCanvas = document.getElementById(canvasId);
81
+ if (oldCanvas) oldCanvas.remove();
82
+
83
+ const canvas = document.createElement('canvas');
84
+ canvas.id = canvasId;
85
+ canvas.className = 'ply-canvas';
86
+ canvas.style.width = "100%";
87
+ canvas.style.height = "100%";
88
+ canvas.setAttribute('tabindex', '0');
89
+ // Safer insert:
90
+ if (progressDialog) {
91
+ viewerContainer.insertBefore(canvas, progressDialog);
92
+ } else {
93
+ viewerContainer.appendChild(canvas);
94
+ }
95
+
96
+ canvas.style.touchAction = "none";
97
+ canvas.style.webkitTouchCallout = "none";
98
+ canvas.addEventListener('gesturestart', e => e.preventDefault());
99
+ canvas.addEventListener('gesturechange', e => e.preventDefault());
100
+ canvas.addEventListener('gestureend', e => e.preventDefault());
101
+ canvas.addEventListener('dblclick', e => e.preventDefault());
102
+ canvas.addEventListener('touchstart', e => { if (e.touches.length > 1) e.preventDefault(); }, { passive: false });
103
+
104
+ canvas.addEventListener('wheel', (e) => {
105
+ e.preventDefault();
106
+ }, { passive: false });
107
+
108
+ if (progressDialog) progressDialog.style.display = 'block';
109
+
110
+ if (!pc) {
111
+ pc = await import("https://esm.run/playcanvas");
112
+ window.pc = pc;
113
+ }
114
+
115
+ // Create app first
116
+ const device = await pc.createGraphicsDevice(canvas, {
117
+ deviceTypes: ["webgl2"],
118
+ glslangUrl: "https://playcanvas.vercel.app/static/lib/glslang/glslang.js",
119
+ twgslUrl: "https://playcanvas.vercel.app/static/lib/twgsl/twgsl.js",
120
+ antialias: false
121
+ });
122
+ device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
123
+
124
+ const opts = new pc.AppOptions();
125
+ opts.graphicsDevice = device;
126
+ opts.mouse = new pc.Mouse(canvas);
127
+ opts.touch = new pc.TouchDevice(canvas);
128
+ opts.componentSystems = [
129
+ pc.RenderComponentSystem,
130
+ pc.CameraComponentSystem,
131
+ pc.LightComponentSystem,
132
+ pc.ScriptComponentSystem,
133
+ pc.GSplatComponentSystem,
134
+ pc.CollisionComponentSystem,
135
+ pc.RigidbodyComponentSystem
136
+ ];
137
+ opts.resourceHandlers = [
138
+ pc.TextureHandler,
139
+ pc.ContainerHandler,
140
+ pc.ScriptHandler,
141
+ pc.GSplatHandler
142
+ ];
143
+
144
+ app = new pc.Application(canvas, opts);
145
+ app.setCanvasFillMode(pc.FILLMODE_NONE);
146
+ app.setCanvasResolution(pc.RESOLUTION_AUTO);
147
+
148
+ resizeObserver = new ResizeObserver(entries => {
149
+ entries.forEach(entry => {
150
+ app.resizeCanvas(entry.contentRect.width, entry.contentRect.height);
151
  });
152
+ });
153
+ resizeObserver.observe(viewerContainer);
154
+
155
+ window.addEventListener('resize', () => app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight));
156
+ app.on('destroy', () => resizeObserver.disconnect());
157
+
158
+ // --- Assets ---
159
+ const assets = {
160
+ orbit: new pc.Asset('script', 'script', { url: "https://mikafil-viewer-gs.static.hf.space/orbit-camera.js" }),
161
+ model: new pc.Asset('glb', 'container', { url: glbUrl }),
162
+ tube: new pc.Asset('glb', 'container', { url: glbUrl2 }),
163
+ bouchon: new pc.Asset('glb', 'container', { url: glbUrl3 }),
164
+ ao_map: new pc.Asset('color', 'texture', { url: aoUrl }),
165
+ op_map: new pc.Asset('color', 'texture', { url: opacityUrl }),
166
+ bg_tex: new pc.Asset('color', 'texture', {url: "https://huggingface.co/datasets/MikaFil/3D_models/resolve/main/EARCARE/banniere_earcare.png"}),
167
+ thickness_map: new pc.Asset('color', 'texture', { url: thicknessUrl }),
168
+ helipad: new pc.Asset(
169
+ 'helipad-env-atlas',
170
+ 'texture',
171
+ { url: "https://huggingface.co/datasets/MikaFil/3D_models/resolve/main/EARCARE/hdr/ciel_nuageux_1k.png" },
172
+ { type: pc.TEXTURETYPE_RGBP, mipmaps: false }
173
+ )
174
+ };
175
+
176
+ for (const key in assets) app.assets.add(assets[key]);
177
+
178
+ // Load HDR *before* loading any models
179
+ assets.helipad.ready(() => {
180
+ app.scene.envAtlas = assets.helipad.resource;
181
+ app.scene.skyboxRotation = new pc.Quat().setFromEulerAngles(30, 20, 0);
182
+ app.scene.skyboxIntensity = 4;
183
+ app.scene.skyboxMip = 0;
184
+
185
+
186
+ // Now load all GLBs and other assets
187
+ const loader = new pc.AssetListLoader([
188
+ assets.orbit,
189
+ assets.model,
190
+ assets.tube,
191
+ assets.bouchon,
192
+ assets.ao_map,
193
+ assets.op_map,
194
+ assets.bg_tex,
195
+ assets.thickness_map
196
+ ], app.assets);
197
+
198
+ loader.load(() => {
199
+ app.start();
200
+ if (progressDialog) progressDialog.style.display = 'none';
201
+
202
+ // Reorder depth layer for transmission
203
+ const depthLayer = app.scene.layers.getLayerById(pc.LAYERID_DEPTH);
204
+ app.scene.layers.remove(depthLayer);
205
+ app.scene.layers.insertOpaque(depthLayer, 2);
206
+
207
+ // 1. Models
208
+ modelEntity = assets.model.resource.instantiateRenderEntity();
209
+ tubeEntity = assets.tube.resource.instantiateRenderEntity();
210
+ bouchonEntity = assets.bouchon.resource.instantiateRenderEntity();
211
+
212
+ app.root.addChild(modelEntity);
213
+ app.root.addChild(tubeEntity);
214
+ //app.root.addChild(bouchonEntity);
215
+
216
+ // 2. Materials for main model & tube
217
+
218
+
219
+ //the 2 materials for the main model :
220
+ const ao = assets.ao_map.resource;
221
+ const op = assets.op_map.resource;
222
+ const thickness = assets.thickness_map.resource;
223
+
224
+ //mat transparent
225
+ matTransparent = new pc.StandardMaterial();
226
+ matTransparent.blendType = pc.BLEND_NORMAL;
227
+ matTransparent.diffuse = new pc.Color(0.7, 0.05, 0.05);
228
+ matTransparent.specular = new pc.Color(0.01, 0.01, 0.01);
229
+ matTransparent.shininess = 90;
230
+ matTransparent.gloss = 1;
231
+ matTransparent.metalness = 0.0;
232
+ matTransparent.useMetalness = true;
233
+ matTransparent.useDynamicRefraction = true;
234
+ matTransparent.depthWrite = true;
235
+ matTransparent.refraction= 0.8;
236
+ matTransparent.refractionIndex = 1;
237
+ matTransparent.thickness = 0.02;
238
+ matTransparent.thicknessMap = thickness;
239
+ matTransparent.opacityMap = op;
240
+ matTransparent.opacityMapChannel = "r";
241
+ matTransparent.opacity = 0.97;
242
+ matTransparent.emissive = new pc.Color(0.372, 0.03, 0.003);
243
+ matTransparent.emissiveMap = ao;
244
+ matTransparent.emissiveIntensity = 0.1;
245
+ matTransparent.update();
246
+
247
+
248
+ //mat opaque
249
+ matOpaque = new pc.StandardMaterial();
250
+ matOpaque.blendType = pc.BLEND_NORMAL;
251
+ matOpaque.diffuse = new pc.Color(0.7, 0.05, 0.05);
252
+ matOpaque.specular = new pc.Color(0.01, 0.01, 0.01);
253
+ matOpaque.shininess = 90;
254
+ matOpaque.gloss = 1;
255
+ matOpaque.metalness = 0.0;
256
+ matOpaque.opacityMap = op;
257
+ matOpaque.opacityMapChannel = "r";
258
+ matOpaque.opacity = 1;
259
+ matOpaque.emissive = new pc.Color(0.372, 0.03, 0.003);
260
+ matOpaque.emissiveMap = ao;
261
+ matOpaque.emissiveIntensity = 2;
262
+ matOpaque.update();
263
+
264
+
265
+
266
+
267
+ //material for the tube
268
+ //transparent
269
+ tubeTransparent = new pc.StandardMaterial();
270
+ tubeTransparent.diffuse = new pc.Color(1, 1, 1);
271
+ tubeTransparent.blendType = pc.BLEND_NORMAL;
272
+ tubeTransparent.opacity = 0.1;
273
+ tubeTransparent.depthTest = false;
274
+ tubeTransparent.depthWrite = false;
275
+ tubeTransparent.useMetalness = true;
276
+ tubeTransparent.useDynamicRefraction = true;
277
+ tubeTransparent.thickness = 4;
278
+ tubeTransparent.update();
279
+
280
+
281
+ //opaque
282
+ tubeOpaque = new pc.StandardMaterial();
283
+ tubeOpaque.diffuse = new pc.Color(1, 1, 1);
284
+ tubeOpaque.opacity = 0.9;
285
+ tubeOpaque.update();
286
+
287
+ traverse(modelEntity, node => {
288
+ if (node.render && node.render.meshInstances) {
289
+ for (let mi of node.render.meshInstances) {
290
+ mi.material = matOpaque;
291
+ }
292
+ }
293
+ });
294
+
295
+
296
+ traverse(tubeEntity, node => {
297
+ if (node.render && node.render.meshInstances) {
298
+ for (let mi of node.render.meshInstances) {
299
+ mi.material = tubeOpaque;
300
+ }
301
+ }
302
+ });
303
+
304
+
305
+
306
+ // Do NOT override materials for bouchonEntity!
307
+
308
+ // 3. Position/scale/orientation
309
+ modelEntity.setPosition(modelX, modelY, modelZ);
310
+ modelEntity.setLocalScale(modelScale, modelScale, modelScale);
311
+ modelEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
312
+
313
+ tubeEntity.setPosition(modelX, modelY, modelZ);
314
+ tubeEntity.setLocalScale(modelScale, modelScale, modelScale);
315
+ tubeEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
316
+
317
+ bouchonEntity.setPosition(modelX, modelY, modelZ);
318
+ bouchonEntity.setLocalScale(modelScale, modelScale, modelScale);
319
+ bouchonEntity.setLocalEulerAngles(modelRotationX, modelRotationY, modelRotationZ);
320
+
321
+ // 4. Camera & orbit
322
+
323
+ cameraEntity = new pc.Entity('camera');
324
+ cameraEntity.addComponent('camera', {
325
+ clearColor: new pc.Color(1, 1, 1, 1),
326
+ toneMapping: pc.TONEMAP_NEUTRAL
327
+ });
328
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
329
+ cameraEntity.lookAt(modelEntity.getPosition());
330
+ cameraEntity.addComponent('script');
331
+
332
+ // --- **CRITICAL** for KHR_materials_transmission! ---
333
+ cameraEntity.camera.requestSceneColorMap(true);
334
+
335
+ cameraEntity.script.create('orbitCamera', {
336
+ attributes: {
337
+ focusEntity: modelEntity,
338
+ inertiaFactor: 0.2,
339
+ distanceMax: maxZoom,
340
+ distanceMin: minZoom,
341
+ pitchAngleMax: maxAngle,
342
+ pitchAngleMin: minAngle,
343
+ yawAngleMax: maxAzimuth,
344
+ yawAngleMin: minAzimuth,
345
+ minY: minY,
346
+ frameOnStart: false
347
+ }
348
+ });
349
+ cameraEntity.script.create('orbitCameraInputMouse');
350
+ cameraEntity.script.create('orbitCameraInputTouch');
351
+ app.root.addChild(cameraEntity);
352
+
353
+ // Remove the Skybox layer from the camera
354
+ const skyboxLayer = app.scene.layers.getLayerByName("Skybox");
355
+ const camLayers = cameraEntity.camera.layers.slice();
356
+ const idx = camLayers.indexOf(skyboxLayer.id);
357
+ if (idx !== -1) {
358
+ camLayers.splice(idx, 1);
359
+ cameraEntity.camera.layers = camLayers;
360
+ }
361
+
362
+ // --- Add this block for a cube parented to the camera ---
363
+ // Create the cube
364
+
365
+ const bgTex = assets.bg_tex.resource;
366
+ const bgPlane = new pc.Entity("Plane");
367
+ bgPlane.addComponent("model", { type: "plane" });
368
+ // Set the cube's local position relative to the camera
369
+ bgPlane.setLocalPosition(0, 0, -10); // 2 units in front of camera
370
+ // Optionally, scale or color the cube
371
+ bgPlane.setLocalScale(11, 1, 5.5);
372
+ bgPlane.setLocalEulerAngles(90, 0, 0);
373
+
374
+ // Simple material for visibility
375
+ const mat = new pc.StandardMaterial();
376
+ mat.diffuse = new pc.Color(1, 1, 1);
377
+ mat.diffuseMap = bgTex;
378
+ mat.emissive = new pc.Color(1, 1, 1);
379
+ mat.emissiveMap = bgTex;
380
+ mat.emissiveIntensity = 1;
381
+ mat.useLighting = false;
382
+ mat.update();
383
+ bgPlane.model.material = mat;
384
+
385
+ // Parent to the camera
386
+ cameraEntity.addChild(bgPlane);
387
+
388
+ // 5. Light
389
+ const light = new pc.Entity("mainLight");
390
+ light.addComponent('light',{
391
+ type: "directional",
392
+ color: new pc.Color(1, 1, 1),
393
+ intensity: 0.9,
394
+ });
395
+ light.setPosition(0, 1, -1);
396
+ light.lookAt(0, 0, 0);
397
+ app.root.addChild(light);
398
+
399
+ app.resizeCanvas(viewerContainer.clientWidth, viewerContainer.clientHeight);
400
+
401
+ app.once('update', () => resetViewerCamera());
402
+
403
+ // Tooltips supported if tooltips_url set
404
+ try {
405
+ if (config.tooltips_url) {
406
+ import('./tooltips.js').then(tooltipsModule => {
407
+ tooltipsModule.initializeTooltips({
408
+ app,
409
+ cameraEntity,
410
+ modelEntity,
411
+ tooltipsUrl: config.tooltips_url,
412
+ defaultVisible: !!config.showTooltipsDefault,
413
+ moveDuration: config.tooltipMoveDuration || 0.6
414
  });
415
+ }).catch(e => {
416
+ // Tooltips optional: fail silently if missing
417
+ });
418
+ }
419
+ } catch (e) {
420
+ // Tooltips optional, fail silently
421
+ }
422
+
423
+ viewerInitialized = true;
424
+ });
425
+ });
 
426
 
427
+ app.assets.load(assets.helipad); // Start by loading the HDR
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
  }
430
 
 
431
  export function resetViewerCamera() {
432
+ try {
433
+ if (!cameraEntity || !modelEntity || !app) return;
434
+ const orbitCam = cameraEntity.script.orbitCamera;
435
+ if (!orbitCam) return;
436
+
437
+ const modelPos = modelEntity.getPosition();
438
+ const tempEnt = new pc.Entity();
439
+ tempEnt.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
440
+ tempEnt.lookAt(modelPos);
441
+
442
+ const dist = new pc.Vec3().sub2(
443
+ new pc.Vec3(chosenCameraX, chosenCameraY, chosenCameraZ),
444
+ modelPos
445
+ ).length();
446
+
447
+ cameraEntity.setPosition(chosenCameraX, chosenCameraY, chosenCameraZ);
448
+ cameraEntity.lookAt(modelPos);
449
+
450
+ orbitCam.pivotPoint = modelPos.clone();
451
+ orbitCam._targetDistance = dist;
452
+ orbitCam._distance = dist;
453
+
454
+ const rot = tempEnt.getRotation();
455
+ const fwd = new pc.Vec3();
456
+ rot.transformVector(pc.Vec3.FORWARD, fwd);
457
+
458
+ const yaw = Math.atan2(-fwd.x, -fwd.z) * pc.math.RAD_TO_DEG;
459
+ const yawQuat = new pc.Quat().setFromEulerAngles(0, -yaw, 0);
460
+ const rotNoYaw = new pc.Quat().mul2(yawQuat, rot);
461
+ const fNoYaw = new pc.Vec3();
462
+ rotNoYaw.transformVector(pc.Vec3.FORWARD, fNoYaw);
463
+ const pitch = Math.atan2(fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
464
+
465
+ orbitCam._targetYaw = yaw;
466
+ orbitCam._yaw = yaw;
467
+ orbitCam._targetPitch = pitch;
468
+ orbitCam._pitch = pitch;
469
+ if (orbitCam._updatePosition) orbitCam._updatePosition();
470
+
471
+ tempEnt.destroy();
472
+ } catch (e) {
473
+ // Silent fail
474
+ }
475
  }
476
 
477
+ /**
478
+ * Change the main model's diffuse color in real time.
479
+ * r,g,b are floats [0,1]. This is a reusable function for new colors.
480
+ */
481
  export function changeColor(dr, dg, db, er, eg, eb, ei, op, boolTrans) {
482
+
483
+ //cameraEntity.camera.clearColor = new pc.Color(colorBgR, colorBgG, colorBgB);
484
+ if(boolTrans){
485
+ if (!matTransparent) return;
486
+
487
+ matTransparent.diffuse.set(dr, dg, db);
488
+ matTransparent.emissive.set(er, eg, eb);
489
+ matTransparent.emissiveIntensity = ei;
490
+ matTransparent.opacity = op;
491
+ matTransparent.update();
492
+
493
+ tubeTransparent.diffuse.set(dr, dg, db);
494
+ tubeTransparent.update();
495
+
496
+ traverse(modelEntity, node => {
497
+ if (node.render && node.render.meshInstances) {
498
+ for (let mi of node.render.meshInstances) {
499
+ mi.material = matTransparent;
500
  }
501
+ }
502
+ });
503
+
504
+ traverse(tubeEntity, node => {
505
+ if (node.render && node.render.meshInstances) {
506
+ for (let mi of node.render.meshInstances) {
507
+ mi.material = tubeTransparent;
508
  }
509
+ }
510
+ });
511
+ }else{
512
+ if (!matOpaque) return;
513
+
514
+ matOpaque.diffuse.set(dr, dg, db);
515
+ matOpaque.emissive.set(er, eg, eb);
516
+ matOpaque.emissiveIntensity = ei;
517
+ matOpaque.update();
518
+
519
+ traverse(modelEntity, node => {
520
+ if (node.render && node.render.meshInstances) {
521
+ for (let mi of node.render.meshInstances) {
522
+ mi.material = matOpaque;
523
  }
524
+ }
525
+ });
526
+
527
+ traverse(tubeEntity, node => {
528
+ if (node.render && node.render.meshInstances) {
529
+ for (let mi of node.render.meshInstances) {
530
+ mi.material = tubeOpaque;
531
  }
532
+ }
533
+ });
534
+ }
535
  }