MikaFil commited on
Commit
6dd7ab7
·
verified ·
1 Parent(s): 2176ab1

Update viewer.js

Browse files
Files changed (1) hide show
  1. viewer.js +447 -494
viewer.js CHANGED
@@ -10,526 +10,479 @@ let bouchonEntity = null;
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.97;
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
  }
 
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
  }