MikaFil commited on
Commit
abca371
·
verified ·
1 Parent(s): 253f893

Update viewer.js

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