@inweb/viewer-three 25.12.1 → 25.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,585 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2002-2024, Open Design Alliance (the "Alliance").
3
+ // All rights reserved.
4
+ //
5
+ // This software and its documentation and related materials are owned by
6
+ // the Alliance. The software may only be incorporated into application
7
+ // programs owned by members of the Alliance, subject to a signed
8
+ // Membership Agreement and Supplemental Software License Agreement with the
9
+ // Alliance. The structure and organization of this software are the valuable
10
+ // trade secrets of the Alliance and its suppliers. The software is also
11
+ // protected by copyright law and international treaty provisions. Application
12
+ // programs incorporating this software must include the following statement
13
+ // with their copyright notices:
14
+ //
15
+ // This application incorporates Open Design Alliance software pursuant to a
16
+ // license agreement with Open Design Alliance.
17
+ // Open Design Alliance Copyright (C) 2002-2024 by Open Design Alliance.
18
+ // All rights reserved.
19
+ //
20
+ // By use of this software, its documentation or related materials, you
21
+ // acknowledge and accept the above terms.
22
+ ///////////////////////////////////////////////////////////////////////////////
23
+
24
+ /* eslint-disable no-extra-semi */
25
+ /* eslint-disable no-prototype-builtins */
26
+ /* eslint-disable @typescript-eslint/no-unused-vars */
27
+ /* eslint-disable prefer-const */
28
+
29
+ import {
30
+ Box3,
31
+ BufferAttribute,
32
+ BufferGeometry,
33
+ Color,
34
+ Group,
35
+ Line,
36
+ LineBasicMaterial,
37
+ Matrix4,
38
+ Mesh,
39
+ MeshBasicMaterial,
40
+ PerspectiveCamera,
41
+ Scene,
42
+ Vector3,
43
+ } from "three";
44
+
45
+ const THREE = {
46
+ Box3,
47
+ BufferAttribute,
48
+ BufferGeometry,
49
+ Color,
50
+ Group,
51
+ Line,
52
+ LineBasicMaterial,
53
+ Matrix4,
54
+ Mesh,
55
+ MeshBasicMaterial,
56
+ PerspectiveCamera,
57
+ Scene,
58
+ Vector3,
59
+ };
60
+
61
+ // https://github.com/buildingSMART/IFC5-development/docs/viewer/render.mjs
62
+
63
+ // (C) buildingSMART International
64
+ // published under MIT license
65
+
66
+ let controls, renderer, scene, camera;
67
+ let datas = [];
68
+ let autoCamera = true;
69
+
70
+ function init() {
71
+ scene = new THREE.Scene();
72
+ camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);
73
+
74
+ camera.up.set(0, 0, 1);
75
+ camera.position.set(50, 50, 50);
76
+ camera.lookAt(0, 0, 0);
77
+
78
+ // const nd = document.querySelector('.viewport');
79
+ // renderer = new THREE.WebGLRenderer({
80
+ // alpha: true
81
+ // });
82
+
83
+ // renderer.setSize(nd.offsetWidth, nd.offsetHeight);
84
+
85
+ // controls = new THREE.OrbitControls(camera, renderer.domElement);
86
+ // controls.enableDamping = true;
87
+ // controls.dampingFactor = 0.25;
88
+
89
+ // nd.appendChild(renderer.domElement);
90
+
91
+ return scene;
92
+ }
93
+
94
+ function getChildByName(root, childName, skip=0) {
95
+ let fragments = childName.replace(/^<\/|^\/|>$/g, '').split('/');
96
+ for (let i = 0; i < skip; ++i) {
97
+ fragments.shift();
98
+ }
99
+ while (fragments.length && root) {
100
+ let f = fragments.shift();
101
+ root = root.children.find(i => i.name.split('/').reverse()[0] === f);
102
+ }
103
+ return root;
104
+ }
105
+
106
+ function createMaterialFromParent(parent, root) {
107
+ let reference = parent.attributes['UsdShade:MaterialBindingAPI:material:binding'];
108
+ let material = {
109
+ color: new THREE.Color(0.6, 0.6, 0.6)
110
+ };
111
+ if (reference) {
112
+ const materialNode = getChildByName(root, reference.ref);
113
+ let shader = materialNode.children.find(i => i.type === 'UsdShade:Shader');
114
+ let color = shader.attributes['inputs:diffuseColor'];
115
+ material.color = new THREE.Color(...color);
116
+ if (shader.attributes['inputs:opacity']) {
117
+ material.transparent = true;
118
+ material.opacity = shader.attributes['inputs:opacity'];
119
+ }
120
+ }
121
+ return material;
122
+ }
123
+
124
+ function createCurveFromJson(node, parent, root) {
125
+ let points = new Float32Array(node.attributes['UsdGeom:BasisCurves:points'].flat());
126
+ const geometry = new THREE.BufferGeometry();
127
+ geometry.setAttribute('position', new THREE.BufferAttribute(points, 3));
128
+ const material = createMaterialFromParent(parent, root);
129
+ let lineMaterial = new THREE.LineBasicMaterial({...material});
130
+ // Make lines a little darker, otherwise they have the same color as meshes
131
+ lineMaterial.color.multiplyScalar(0.8)
132
+ return new THREE.Line(geometry, lineMaterial);
133
+ }
134
+
135
+
136
+ function createMeshFromJson(node, parent, root) {
137
+ let points = new Float32Array(node.attributes['UsdGeom:Mesh:points'].flat());
138
+ let indices = new Uint16Array(node.attributes['UsdGeom:Mesh:faceVertexIndices']);
139
+
140
+ const geometry = new THREE.BufferGeometry();
141
+ geometry.setAttribute('position', new THREE.BufferAttribute(points, 3));
142
+ geometry.setIndex(new THREE.BufferAttribute(indices, 1));
143
+ geometry.computeVertexNormals();
144
+
145
+ const material = createMaterialFromParent(parent, root);
146
+ let meshMaterial = new THREE.MeshBasicMaterial({...material});
147
+
148
+ return new THREE.Mesh(geometry, meshMaterial);
149
+ }
150
+
151
+ function traverseTree(node, parent, root, parentNode) {
152
+ let elem;
153
+ if (node.type === "UsdGeom:Xform") {
154
+ elem = new THREE.Group();
155
+ } else if (node.type === "UsdGeom:Mesh" || node.type === "UsdGeom:BasisCurves") {
156
+ if (node.attributes["UsdGeom:VisibilityAPI:visibility:visibility"] === 'invisible') {
157
+ return;
158
+ }
159
+ if (node.type === "UsdGeom:Mesh") {
160
+ elem = createMeshFromJson(node, parentNode, root);
161
+ } else {
162
+ elem = createCurveFromJson(node, parentNode, root);
163
+ }
164
+ } else if (node !== root) {
165
+ return;
166
+ }
167
+
168
+ if (node !== root) {
169
+ parent.add(elem);
170
+ elem.matrixAutoUpdate = false;
171
+
172
+ let matrixNode = node.attributes && node.attributes['xformOp:transform'] ? node.attributes['xformOp:transform'].flat() : null;
173
+ if (matrixNode) {
174
+ let matrix = new THREE.Matrix4();
175
+ matrix.set(...matrixNode);
176
+ matrix.transpose();
177
+ elem.matrix = matrix;
178
+ }
179
+ }
180
+
181
+ (node.children || []).forEach(child => traverseTree(child, elem || parent, root, node));
182
+ }
183
+
184
+ function* collectNames(node) {
185
+ yield node.name;
186
+ // @todo assert node.name matches path
187
+ for (const child of node.children || []) {
188
+ yield* collectNames(child);
189
+ }
190
+ }
191
+
192
+ function compose(datas) {
193
+ // Composition, the naive way:
194
+ // - flatten tree to list of <path, object> pairs
195
+ // - group objects with among layers with the same path
196
+ // - apply inheritance relationships
197
+ // - recompose into hierarchical structure
198
+
199
+ let compositionEdges = {};
200
+
201
+ // Undo the attribute namespace of over prims introduced in 'ECS'.
202
+ function flattenAttributes(prim) {
203
+ if (prim.name !== 'Shader' && prim.attributes) {
204
+ const [k, vs] = Object.entries(prim.attributes)[0];
205
+ const attrs = Object.fromEntries(Object.entries(vs).map(([kk, vv]) => [`${k}:${kk}`, vv]));
206
+ return {
207
+ ...prim,
208
+ attributes: attrs
209
+ };
210
+ } else {
211
+ return {
212
+ ...prim
213
+ };
214
+ }
215
+ }
216
+
217
+ function addEdge(a, b) {
218
+ (compositionEdges[a] = compositionEdges[a] || []).push(b);
219
+ }
220
+
221
+ // Traverse forest and yield paths as a map of str -> dict
222
+ function collectPaths(nodes) {
223
+ const paths = {};
224
+
225
+ function traverse(node, parentPathStr) {
226
+ if (node.name) {
227
+ // Fully qualified means an over on a full path like /Project/Site/Something/Body. These
228
+ // are applied differntly. on non-root nodes we don't assemble immutably bottom up, but rather mutate top down.
229
+ const isFullyQualified = node.name.split('/').length > 2;
230
+ const reverseWhenFullyQualified = isFullyQualified ? (a => a.reverse()) : (a => a);
231
+
232
+ const pathStr = `${parentPathStr}/${node.name.replace(/^\//, '')}`
233
+
234
+ let nodeId = pathStr;
235
+
236
+ // 'complete' refers to the composition lifecycle when all overs are applied and the prim is ready
237
+ // to be inherited from or used as a subprim.
238
+ let nodeIdComplete = `${pathStr} complete`;
239
+
240
+ const N = flattenAttributes(node);
241
+ N.name = pathStr;
242
+
243
+ if (node.def === 'over') {
244
+ nodeId = `${pathStr} over`;
245
+ addEdge(...reverseWhenFullyQualified([nodeId, pathStr]));
246
+ addEdge(nodeIdComplete, nodeId);
247
+ }
248
+
249
+ addEdge(nodeIdComplete, pathStr);
250
+
251
+ // Store in map
252
+ (paths[nodeId] = paths[nodeId] || []).push(N);
253
+
254
+ // Add inheritance edges
255
+ for (let ih of node.inherits || []) {
256
+ const target = ih.substring(1, ih.length - 1);
257
+ addEdge(nodeId, `${target} complete`)
258
+ }
259
+
260
+ // Add subprim edges
261
+ (node.children || []).forEach(child => {
262
+ // We only instantiate def'ed children, not classes
263
+ if (child.name && child.def === 'def') {
264
+ const childName = `${pathStr}/${child.name}`;
265
+ addEdge(...reverseWhenFullyQualified([pathStr, `${childName} complete`]));
266
+ if (nodeId.endsWith('over')) {
267
+ // when we have an over on a deeper namespace we need to make sure the root is already built
268
+ if (pathStr.split('/').length > 2) {
269
+ addEdge(childName, `/${pathStr.split('/')[1]}`);
270
+ }
271
+ }
272
+ }
273
+ traverse(child, pathStr);
274
+ });
275
+ }
276
+ }
277
+
278
+ // Create the pseudo root and connect to its children
279
+ nodes.forEach((n) => traverse(n, ''));
280
+ nodes.filter(n => n.name && n.def === 'def').forEach(n => {
281
+ addEdge('', `/${n.name} complete`);
282
+ });
283
+
284
+ return paths;
285
+ }
286
+
287
+ // This is primarily for children, loading the same layer multiple times should not have an effect
288
+ // so the child composition edges should not be duplicated. Applying overs should be idempotent.
289
+ function removeDuplicates(map_of_arrays) {
290
+ return Object.fromEntries(Object.entries(map_of_arrays).map(([k, vs]) => [k, vs.filter((value, index, array) =>
291
+ array.indexOf(value) === index
292
+ )]));
293
+ }
294
+
295
+ // Prim storage based on path for the various lauers
296
+ const maps = datas.map(collectPaths);
297
+
298
+ let compositionEdgesOrig = removeDuplicates(compositionEdges);
299
+
300
+ // Reduction function to override prim attributes
301
+ // Assumes 'unpacked' attribute namespaces
302
+ function composePrim(right, left) {
303
+ return {
304
+ def: left.def || (right !== null ? right.def : null),
305
+ type: left.type || (right !== null ? right.type : null),
306
+ name: right ? right.name : left.name,
307
+ attributes: {
308
+ ...((right !== null) ? right.attributes : {}),
309
+ ...((left !== null) ? left.attributes : {})
310
+ },
311
+ children: (left.children || []).concat(right ? (right.children || []) : [])
312
+ }
313
+ }
314
+
315
+ // Copy the input to avoid modifying it.
316
+ // Discard self-dependencies and copy two levels deep.
317
+ compositionEdges = Object.fromEntries(
318
+ Object.entries(compositionEdgesOrig).map(([item, dep]) => [
319
+ item,
320
+ new Set([...dep].filter((e) => e !== item)),
321
+ ])
322
+ );
323
+
324
+ // Find all items that don't depend on anything.
325
+ const extraItemsInDeps = new Set(
326
+ [...Object.values(compositionEdges).map(st => Array.from(st)).flat()].filter((value) => !compositionEdges.hasOwnProperty(value))
327
+ );
328
+
329
+ // Add empty dependencies where needed.
330
+ extraItemsInDeps.forEach((item) => {
331
+ if (maps.map(m => m[item]).some(i => i)) {
332
+ // only add defined things, not overs on concatenated paths that don't exist yet which need to be the result of actual composition steps
333
+ compositionEdges[item] = new Set();
334
+ }
335
+ });
336
+
337
+ const composed = {};
338
+ const built = new Set();
339
+
340
+ Object.keys(compositionEdges).forEach(p => {
341
+ const opinions = maps.map(m => m[p]).filter(a => a).flat(1);
342
+ if (p == '') {
343
+ composed[p] = {name: p};
344
+ } else if (opinions.length === 0) {
345
+ return;
346
+ } else if (opinions.length == 1) {
347
+ composed[p] = composePrim(null, opinions[0]);
348
+ } else {
349
+ composed[p] = opinions.reverse().reduce(composePrim);
350
+ }
351
+ delete composed[p].children;
352
+ });
353
+
354
+ const updateName = (oldPrefix, newPrefix, prim) => {
355
+ return {
356
+ ...prim,
357
+ name: prim.name.replace(new RegExp(`^${oldPrefix}(?=/)`), newPrefix),
358
+ children: prim.children.map(c => updateName(oldPrefix, newPrefix, c))
359
+ }
360
+ };
361
+
362
+ // Essentially we do an 'interactive' topological sort. Where we process the composition edges for
363
+ // those prims that do not have any dependencies left. However, as a consequence of composition,
364
+ // novel prim paths can also be formed which can resolve the dependencies for other prims.
365
+ let maxIterations = 100;
366
+ while (maxIterations --) {
367
+ const bottomRankNodes = Object.entries(compositionEdges).filter(([_, dep]) => dep.size === 0 && (composed[_] || built.has(_) || _.endsWith(' complete'))).map(([k, v]) => k);
368
+ console.log('Bottom rank prims to resolve:', ...bottomRankNodes);
369
+
370
+ if (bottomRankNodes.length === 0) {
371
+ break;
372
+ }
373
+
374
+ const definedPrims = new Set();
375
+
376
+ // Apply edges in dependency order
377
+ bottomRankNodes.forEach(k => {
378
+ (Array.from(compositionEdgesOrig[k] || [])).forEach(v => {
379
+ // We don't have typed edges because of the somewhat primitive type system in JS.
380
+ // (An array does not really function as a tuple). So we need to reverse engineer
381
+ // the type of the edge (and therefore what composition action to apply) based on
382
+ // the names of the vertices.
383
+ console.log('Processing edge:', k, ' --- ', v);
384
+ if (k.endsWith(' complete') && v.endsWith(' over')) {
385
+ // Only for life cycle dependency management, no action associated
386
+ } else if (v.startsWith(k + '/')) {
387
+ // If k is a subpath of v it's a subPrim relationship
388
+ if (k.split('/').length > 2) {
389
+ // this should not occur
390
+ } else {
391
+ v = v.replace(/ complete$/, '');
392
+
393
+ composed[k].children = composed[k].children || [];
394
+ composed[k].children.push(composed[v]);
395
+ Array.from(collectNames(composed[k])).forEach(a => definedPrims.add(a.substring(k.length)));
396
+ }
397
+ } else if (k.startsWith(v + '/')) {
398
+ // reversed child definition for top-down over application
399
+ if (k.endsWith(' complete')) {
400
+ // @todo immutability
401
+ let child = getChildByName(composed[`/${v.split('/')[1]}`], v, /*skip=*/ 1);
402
+ if (child) {
403
+ k = k.replace(/ complete$/, '');
404
+ child.children.push(composed[k]);
405
+ } else {
406
+ console.error(v, '-->', k, 'not applied');
407
+ }
408
+ Array.from(collectNames(child)).forEach(a => definedPrims.add(a.substring(child.name.length)));
409
+ }
410
+ } else if (k.search(/over$/) !== -1) {
411
+ if (k.split('/').length > 2) {
412
+ // @todo immutability
413
+ let child = getChildByName(composed[`/${v.split('/')[1]}`], k.split(' ')[0], /*skip=*/ 1);
414
+ if (child) {
415
+ Object.assign(child.attributes, composed[k].attributes);
416
+ } else {
417
+ console.error(k, '-->', v, 'not applied');
418
+ }
419
+ } else {
420
+ composed[v] = composePrim(composed[v], composed[k]);
421
+ }
422
+ } else if (v.search(/over$/) !== -1) {
423
+ // reversed top-down over
424
+ if (v.split('/').length > 2) {
425
+ // @todo immutability
426
+ let child = getChildByName(composed[`/${k.split('/')[1]}`], v.split(' ')[0], /*skip=*/ 1);
427
+ if (child) {
428
+ Object.assign(child.attributes, composed[v].attributes);
429
+ } else {
430
+ console.error(v, '-->', k, 'not registered');
431
+ }
432
+ } else {
433
+ composed[k] = composePrim(composed[k], composed[v]);
434
+ }
435
+ } else {
436
+ // Else it's an inheritance relationship
437
+ if (v.endsWith('complete')) {
438
+ // only when ends with complete, otherwise it could be the dependency edge between a concatenated-prim over and its root.
439
+ v = v.replace(/ complete$/, '');
440
+ composed[k] = updateName(composed[v].name, composed[k].name, composePrim(composed[k], composed[v]));
441
+ Array.from(collectNames(composed[k])).forEach(a => definedPrims.add(a.substring(k.length)));
442
+ }
443
+ }
444
+ });
445
+ });
446
+
447
+ console.log('Constructed prims:', ...definedPrims);
448
+
449
+ Array.from(definedPrims).forEach(a => built.add(a));
450
+
451
+ let orderedSet = new Set(bottomRankNodes);
452
+ compositionEdges = Object.fromEntries(
453
+ Object.entries(compositionEdges)
454
+ .filter(([item]) => !orderedSet.has(item))
455
+ .map(([item, dep]) => [item, new Set([...dep].filter((d) => (!orderedSet.has(d) && !definedPrims.has(d))))])
456
+ );
457
+ }
458
+
459
+ if (Object.keys(compositionEdges).length !== 0) {
460
+ console.error("Unresolved nodes:", ...Object.keys(compositionEdges));
461
+ }
462
+
463
+ console.log(composed['']);
464
+ return composed[''];
465
+ }
466
+
467
+ function encodeHtmlEntities(str) {
468
+ const div = document.createElement('div');
469
+ div.textContent = str;
470
+ return div.innerHTML;
471
+ };
472
+
473
+ const icons = {
474
+ 'UsdGeom:Mesh:points': 'deployed_code',
475
+ 'UsdGeom:BasisCurves:points': 'line_curve',
476
+ 'UsdShade:Material:outputs:surface.connect': 'line_style'
477
+ };
478
+
479
+ function buildDomTree(prim, node) {
480
+ const elem = document.createElement('div');
481
+ let span;
482
+ elem.appendChild(document.createTextNode(prim.name ? prim.name.split('/').reverse()[0] : 'root'));
483
+ elem.appendChild(span = document.createElement('span'));
484
+ Object.entries(icons).forEach(([k, v]) => span.innerText += (prim.attributes || {})[k] ? v : ' ');
485
+ span.className = "material-symbols-outlined";
486
+ elem.onclick = (evt) => {
487
+ let rows = [['name', prim.name]].concat(Object.entries(prim.attributes)).map(([k, v]) => `<tr><td>${encodeHtmlEntities(k)}</td><td>${encodeHtmlEntities(typeof v === 'object' ? JSON.stringify(v) : v)}</td>`).join('');
488
+ document.querySelector('.attributes .table').innerHTML = `<table border="0">${rows}</table>`;
489
+ evt.stopPropagation();
490
+ };
491
+ node.appendChild(elem);
492
+ (prim.children || []).forEach(p => buildDomTree(p, elem));
493
+ }
494
+
495
+ export function composeAndRender() {
496
+ if (scene) {
497
+ // @todo does this actually free up resources?
498
+ scene.children = [];
499
+ }
500
+
501
+ // document.querySelector('.tree').innerHTML = '';
502
+
503
+ if (datas.length === 0) {
504
+ return;
505
+ }
506
+
507
+ const tree = compose(datas.map(arr => arr[1]));
508
+ if (!tree) {
509
+ console.error("No result from composition");
510
+ return;
511
+ }
512
+
513
+ traverseTree(tree, scene || init(), tree);
514
+
515
+ if (autoCamera) {
516
+ const boundingBox = new THREE.Box3();
517
+ boundingBox.setFromObject(scene);
518
+ if (!boundingBox.isEmpty()) {
519
+ let avg = boundingBox.min.clone().add(boundingBox.max).multiplyScalar(0.5);
520
+ let ext = boundingBox.max.clone().sub(boundingBox.min).length();
521
+ camera.position.copy(avg.clone().add(new THREE.Vector3(1,1,1).normalize().multiplyScalar(ext)));
522
+ camera.far = ext * 3;
523
+ camera.updateProjectionMatrix();
524
+ // controls.target.copy(avg);
525
+ // controls.update();
526
+
527
+ // only on first successful load
528
+ autoCamera = false;
529
+ }
530
+ }
531
+
532
+
533
+ // buildDomTree(tree, document.querySelector('.tree'));
534
+ // animate();
535
+ }
536
+
537
+ function createLayerDom() {
538
+ document.querySelector('.layers div').innerHTML = '';
539
+ datas.forEach(([name, _], index) => {
540
+ const elem = document.createElement('div');
541
+ elem.appendChild(document.createTextNode(name));
542
+ ['\u25B3', '\u25BD', '\u00D7'].reverse().forEach((lbl, cmd) => {
543
+ const btn = document.createElement('span');
544
+ btn.onclick = (evt) => {
545
+ evt.stopPropagation();
546
+ if (cmd === 2) {
547
+ if (index > 0) {
548
+ [datas[index], datas[index - 1]] = [datas[index - 1], datas[index]];
549
+ }
550
+ } else if (cmd === 1) {
551
+ if (index < datas.length - 1) {
552
+ [datas[index], datas[index + 1]] = [datas[index + 1], datas[index]];
553
+ }
554
+ } else if (cmd === 0) {
555
+ datas.splice(index, 1);
556
+ }
557
+ composeAndRender();
558
+ createLayerDom();
559
+ }
560
+ btn.appendChild(document.createTextNode(lbl));
561
+ elem.appendChild(btn);
562
+ });
563
+ document.querySelector('.layers div').appendChild(elem);
564
+ });
565
+ }
566
+
567
+ export default function addModel(name, m) {
568
+ datas.push([name, m]);
569
+ // createLayerDom();
570
+ composeAndRender();
571
+ return scene;
572
+ }
573
+
574
+ function animate() {
575
+ // requestAnimationFrame(animate);
576
+ // controls.update();
577
+ // renderer.render(scene, camera);
578
+ }
579
+
580
+ export function clear() {
581
+ scene = undefined;
582
+ camera = undefined;
583
+ datas.length = 0;
584
+ autoCamera = true;
585
+ }