bbmodel-viewer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +148 -0
- package/dist/index.cjs +606 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +221 -0
- package/dist/index.d.ts +221 -0
- package/dist/index.js +581 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var THREE4 = require('three');
|
|
4
|
+
var OrbitControls_js = require('three/addons/controls/OrbitControls.js');
|
|
5
|
+
|
|
6
|
+
function _interopNamespace(e) {
|
|
7
|
+
if (e && e.__esModule) return e;
|
|
8
|
+
var n = Object.create(null);
|
|
9
|
+
if (e) {
|
|
10
|
+
Object.keys(e).forEach(function (k) {
|
|
11
|
+
if (k !== 'default') {
|
|
12
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
13
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
get: function () { return e[k]; }
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
n.default = e;
|
|
21
|
+
return Object.freeze(n);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
var THREE4__namespace = /*#__PURE__*/_interopNamespace(THREE4);
|
|
25
|
+
|
|
26
|
+
// src/viewer.ts
|
|
27
|
+
function cubeFaceCorners(dir, x1, y1, z1, x2, y2, z2) {
|
|
28
|
+
switch (dir) {
|
|
29
|
+
case "south":
|
|
30
|
+
return { tl: [x1, y2, z2], tr: [x2, y2, z2], bl: [x1, y1, z2], br: [x2, y1, z2] };
|
|
31
|
+
case "north":
|
|
32
|
+
return { tl: [x2, y2, z1], tr: [x1, y2, z1], bl: [x2, y1, z1], br: [x1, y1, z1] };
|
|
33
|
+
case "east":
|
|
34
|
+
return { tl: [x2, y2, z2], tr: [x2, y2, z1], bl: [x2, y1, z2], br: [x2, y1, z1] };
|
|
35
|
+
case "west":
|
|
36
|
+
return { tl: [x1, y2, z1], tr: [x1, y2, z2], bl: [x1, y1, z1], br: [x1, y1, z2] };
|
|
37
|
+
case "up":
|
|
38
|
+
return { tl: [x1, y2, z1], tr: [x2, y2, z1], bl: [x1, y2, z2], br: [x2, y2, z2] };
|
|
39
|
+
case "down":
|
|
40
|
+
return { tl: [x1, y1, z2], tr: [x2, y1, z2], bl: [x1, y1, z1], br: [x2, y1, z1] };
|
|
41
|
+
default:
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
var CUBE_FACE_ORDER = ["north", "east", "south", "west", "up", "down"];
|
|
46
|
+
function boxUvRect(dir, u, v, sx, sy, sz) {
|
|
47
|
+
switch (dir) {
|
|
48
|
+
case "up":
|
|
49
|
+
return [u + sz, v, u + sz + sx, v + sz];
|
|
50
|
+
case "down":
|
|
51
|
+
return [u + sz + sx, v, u + sz + 2 * sx, v + sz];
|
|
52
|
+
case "east":
|
|
53
|
+
return [u, v + sz, u + sz, v + sz + sy];
|
|
54
|
+
case "north":
|
|
55
|
+
return [u + sz, v + sz, u + sz + sx, v + sz + sy];
|
|
56
|
+
case "west":
|
|
57
|
+
return [u + sz + sx, v + sz, u + 2 * sz + sx, v + sz + sy];
|
|
58
|
+
case "south":
|
|
59
|
+
return [u + 2 * sz + sx, v + sz, u + 2 * sz + 2 * sx, v + sz + sy];
|
|
60
|
+
default:
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function rectToCornerUvs(rect, uvWidth, uvHeight, rotation = 0) {
|
|
65
|
+
const [u1, v1, u2, v2] = rect;
|
|
66
|
+
const uMin = u1 / uvWidth;
|
|
67
|
+
const uMax = u2 / uvWidth;
|
|
68
|
+
const vTop = 1 - v1 / uvHeight;
|
|
69
|
+
const vBot = 1 - v2 / uvHeight;
|
|
70
|
+
let corners = {
|
|
71
|
+
tl: [uMin, vTop],
|
|
72
|
+
tr: [uMax, vTop],
|
|
73
|
+
bl: [uMin, vBot],
|
|
74
|
+
br: [uMax, vBot]
|
|
75
|
+
};
|
|
76
|
+
const steps = (rotation / 90 % 4 + 4) % 4;
|
|
77
|
+
for (let i = 0; i < steps; i++) {
|
|
78
|
+
corners = { tl: corners.bl, tr: corners.tl, br: corners.tr, bl: corners.br };
|
|
79
|
+
}
|
|
80
|
+
return corners;
|
|
81
|
+
}
|
|
82
|
+
var MeshAssembler = class {
|
|
83
|
+
constructor() {
|
|
84
|
+
this.positions = [];
|
|
85
|
+
this.uvs = [];
|
|
86
|
+
this.materials = [];
|
|
87
|
+
this.materialIndex = /* @__PURE__ */ new Map();
|
|
88
|
+
this.groups = [];
|
|
89
|
+
this.vertexCount = 0;
|
|
90
|
+
}
|
|
91
|
+
indexOf(material) {
|
|
92
|
+
let idx = this.materialIndex.get(material);
|
|
93
|
+
if (idx === void 0) {
|
|
94
|
+
idx = this.materials.length;
|
|
95
|
+
this.materials.push(material);
|
|
96
|
+
this.materialIndex.set(material, idx);
|
|
97
|
+
}
|
|
98
|
+
return idx;
|
|
99
|
+
}
|
|
100
|
+
addQuad(c, uv, material) {
|
|
101
|
+
const start = this.vertexCount;
|
|
102
|
+
this.pushVertex(c.tl, uv.tl);
|
|
103
|
+
this.pushVertex(c.bl, uv.bl);
|
|
104
|
+
this.pushVertex(c.tr, uv.tr);
|
|
105
|
+
this.pushVertex(c.tr, uv.tr);
|
|
106
|
+
this.pushVertex(c.bl, uv.bl);
|
|
107
|
+
this.pushVertex(c.br, uv.br);
|
|
108
|
+
this.groups.push({ start, count: 6, index: this.indexOf(material) });
|
|
109
|
+
}
|
|
110
|
+
addTriangle(p, uv, material) {
|
|
111
|
+
const start = this.vertexCount;
|
|
112
|
+
this.pushVertex(p[0], uv[0]);
|
|
113
|
+
this.pushVertex(p[1], uv[1]);
|
|
114
|
+
this.pushVertex(p[2], uv[2]);
|
|
115
|
+
this.groups.push({ start, count: 3, index: this.indexOf(material) });
|
|
116
|
+
}
|
|
117
|
+
pushVertex(pos, uv) {
|
|
118
|
+
this.positions.push(pos[0], pos[1], pos[2]);
|
|
119
|
+
this.uvs.push(uv[0], uv[1]);
|
|
120
|
+
this.vertexCount++;
|
|
121
|
+
}
|
|
122
|
+
build(name) {
|
|
123
|
+
if (this.vertexCount === 0) return null;
|
|
124
|
+
const geometry = new THREE4__namespace.BufferGeometry();
|
|
125
|
+
geometry.setAttribute("position", new THREE4__namespace.Float32BufferAttribute(this.positions, 3));
|
|
126
|
+
geometry.setAttribute("uv", new THREE4__namespace.Float32BufferAttribute(this.uvs, 2));
|
|
127
|
+
for (const g of this.groups) geometry.addGroup(g.start, g.count, g.index);
|
|
128
|
+
geometry.computeVertexNormals();
|
|
129
|
+
const material = this.materials.length === 1 ? this.materials[0] : this.materials;
|
|
130
|
+
const mesh = new THREE4__namespace.Mesh(geometry, material);
|
|
131
|
+
mesh.name = name;
|
|
132
|
+
return mesh;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
function buildElementMesh(element, library) {
|
|
136
|
+
if (element.type === "mesh" && element.vertices) {
|
|
137
|
+
return buildMeshElement(element, library);
|
|
138
|
+
}
|
|
139
|
+
return buildCubeElement(element, library);
|
|
140
|
+
}
|
|
141
|
+
function buildCubeElement(element, library) {
|
|
142
|
+
const from = element.from ?? [0, 0, 0];
|
|
143
|
+
const to = element.to ?? [0, 0, 0];
|
|
144
|
+
const origin = element.origin ?? [0, 0, 0];
|
|
145
|
+
const inflate = element.inflate ?? 0;
|
|
146
|
+
const x1 = Math.min(from[0], to[0]) - inflate - origin[0];
|
|
147
|
+
const x2 = Math.max(from[0], to[0]) + inflate - origin[0];
|
|
148
|
+
const y1 = Math.min(from[1], to[1]) - inflate - origin[1];
|
|
149
|
+
const y2 = Math.max(from[1], to[1]) + inflate - origin[1];
|
|
150
|
+
const z1 = Math.min(from[2], to[2]) - inflate - origin[2];
|
|
151
|
+
const z2 = Math.max(from[2], to[2]) + inflate - origin[2];
|
|
152
|
+
const sx = Math.abs(to[0] - from[0]);
|
|
153
|
+
const sy = Math.abs(to[1] - from[1]);
|
|
154
|
+
const sz = Math.abs(to[2] - from[2]);
|
|
155
|
+
const uvOffset = element.uv_offset ?? [0, 0];
|
|
156
|
+
const faces = element.faces ?? {};
|
|
157
|
+
const assembler = new MeshAssembler();
|
|
158
|
+
for (const dir of CUBE_FACE_ORDER) {
|
|
159
|
+
const face = faces[dir];
|
|
160
|
+
const corners = cubeFaceCorners(dir, x1, y1, z1, x2, y2, z2);
|
|
161
|
+
if (!corners) continue;
|
|
162
|
+
if (element.faces && !face) continue;
|
|
163
|
+
const texMeta = library.resolve(face?.texture);
|
|
164
|
+
const material = texMeta ? texMeta.material : library.colorMaterial(element.color ?? 0);
|
|
165
|
+
const uvWidth = texMeta ? texMeta.uvWidth : library.fallbackUvSpace.width;
|
|
166
|
+
const uvHeight = texMeta ? texMeta.uvHeight : library.fallbackUvSpace.height;
|
|
167
|
+
const rect = face?.uv ?? boxUvRect(dir, uvOffset[0], uvOffset[1], sx, sy, sz) ?? [0, 0, uvWidth, uvHeight];
|
|
168
|
+
const uv = rectToCornerUvs(rect, uvWidth, uvHeight, face?.rotation ?? 0);
|
|
169
|
+
assembler.addQuad(corners, uv, material);
|
|
170
|
+
}
|
|
171
|
+
return assembler.build(element.name ?? "cube");
|
|
172
|
+
}
|
|
173
|
+
function buildMeshElement(element, library) {
|
|
174
|
+
const vertices = element.vertices ?? {};
|
|
175
|
+
const faces = element.faces ?? {};
|
|
176
|
+
const assembler = new MeshAssembler();
|
|
177
|
+
const posOf = (key) => {
|
|
178
|
+
const v = vertices[key];
|
|
179
|
+
return v ? [v[0], v[1], v[2]] : null;
|
|
180
|
+
};
|
|
181
|
+
for (const face of Object.values(faces)) {
|
|
182
|
+
const keys = face.vertices ?? [];
|
|
183
|
+
if (keys.length < 3) continue;
|
|
184
|
+
const texMeta = library.resolve(face.texture);
|
|
185
|
+
const material = texMeta ? texMeta.material : library.colorMaterial(element.color ?? 0);
|
|
186
|
+
const uvWidth = texMeta ? texMeta.uvWidth : library.fallbackUvSpace.width;
|
|
187
|
+
const uvHeight = texMeta ? texMeta.uvHeight : library.fallbackUvSpace.height;
|
|
188
|
+
const uvOf = (key) => {
|
|
189
|
+
const raw = face.uv?.[key];
|
|
190
|
+
if (!raw) return [0, 0];
|
|
191
|
+
return [raw[0] / uvWidth, 1 - raw[1] / uvHeight];
|
|
192
|
+
};
|
|
193
|
+
for (let i = 1; i < keys.length - 1; i++) {
|
|
194
|
+
const a = posOf(keys[0]);
|
|
195
|
+
const b = posOf(keys[i]);
|
|
196
|
+
const c = posOf(keys[i + 1]);
|
|
197
|
+
if (!a || !b || !c) continue;
|
|
198
|
+
assembler.addTriangle(
|
|
199
|
+
[a, b, c],
|
|
200
|
+
[uvOf(keys[0]), uvOf(keys[i]), uvOf(keys[i + 1])],
|
|
201
|
+
material
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return assembler.build(element.name ?? "mesh");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/builder.ts
|
|
209
|
+
var EULER_ORDER = "ZYX";
|
|
210
|
+
var ORIGIN = [0, 0, 0];
|
|
211
|
+
function buildModelObject(model, library) {
|
|
212
|
+
const root = new THREE4__namespace.Group();
|
|
213
|
+
root.name = model.name ?? "bbmodel";
|
|
214
|
+
const elementsByUuid = /* @__PURE__ */ new Map();
|
|
215
|
+
for (const el of model.elements ?? []) {
|
|
216
|
+
if (el.uuid) elementsByUuid.set(el.uuid, el);
|
|
217
|
+
}
|
|
218
|
+
const groupsByUuid = /* @__PURE__ */ new Map();
|
|
219
|
+
for (const g of model.groups ?? []) {
|
|
220
|
+
if (g.uuid) groupsByUuid.set(g.uuid, g);
|
|
221
|
+
}
|
|
222
|
+
const usedElements = /* @__PURE__ */ new Set();
|
|
223
|
+
const visitedGroups = /* @__PURE__ */ new Set();
|
|
224
|
+
const outliner = model.outliner ?? [];
|
|
225
|
+
for (const node of outliner) {
|
|
226
|
+
const built = buildNode(node, ORIGIN);
|
|
227
|
+
if (built) root.add(built);
|
|
228
|
+
}
|
|
229
|
+
for (const el of model.elements ?? []) {
|
|
230
|
+
if (el.uuid && usedElements.has(el.uuid)) continue;
|
|
231
|
+
const built = buildElementNode(el, ORIGIN);
|
|
232
|
+
if (built) root.add(built);
|
|
233
|
+
}
|
|
234
|
+
return root;
|
|
235
|
+
function buildNode(node, parentOrigin) {
|
|
236
|
+
if (typeof node === "string") {
|
|
237
|
+
const element = elementsByUuid.get(node);
|
|
238
|
+
if (element) {
|
|
239
|
+
usedElements.add(node);
|
|
240
|
+
return buildElementNode(element, parentOrigin);
|
|
241
|
+
}
|
|
242
|
+
const group = groupsByUuid.get(node);
|
|
243
|
+
if (group) return buildGroupNode(group, parentOrigin);
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
return buildGroupNode(node, parentOrigin);
|
|
247
|
+
}
|
|
248
|
+
function buildGroupNode(group, parentOrigin) {
|
|
249
|
+
if (group.visibility === false) return null;
|
|
250
|
+
if (group.uuid) {
|
|
251
|
+
if (visitedGroups.has(group.uuid)) return null;
|
|
252
|
+
visitedGroups.add(group.uuid);
|
|
253
|
+
}
|
|
254
|
+
const origin = group.origin ?? parentOrigin;
|
|
255
|
+
const node = new THREE4__namespace.Group();
|
|
256
|
+
node.name = group.name ?? "group";
|
|
257
|
+
applyTransform(node, origin, parentOrigin, group.rotation);
|
|
258
|
+
for (const child of group.children ?? []) {
|
|
259
|
+
const built = buildNode(child, origin);
|
|
260
|
+
if (built) node.add(built);
|
|
261
|
+
}
|
|
262
|
+
return node;
|
|
263
|
+
}
|
|
264
|
+
function buildElementNode(element, parentOrigin) {
|
|
265
|
+
if (element.visibility === false) return null;
|
|
266
|
+
const mesh = buildElementMesh(element, library);
|
|
267
|
+
if (!mesh) return null;
|
|
268
|
+
const origin = element.origin ?? parentOrigin;
|
|
269
|
+
const node = new THREE4__namespace.Group();
|
|
270
|
+
node.name = element.name ?? "element";
|
|
271
|
+
applyTransform(node, origin, parentOrigin, element.rotation);
|
|
272
|
+
node.add(mesh);
|
|
273
|
+
return node;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function applyTransform(node, origin, parentOrigin, rotation) {
|
|
277
|
+
node.position.set(
|
|
278
|
+
origin[0] - parentOrigin[0],
|
|
279
|
+
origin[1] - parentOrigin[1],
|
|
280
|
+
origin[2] - parentOrigin[2]
|
|
281
|
+
);
|
|
282
|
+
if (rotation) {
|
|
283
|
+
node.rotation.set(
|
|
284
|
+
THREE4__namespace.MathUtils.degToRad(rotation[0]),
|
|
285
|
+
THREE4__namespace.MathUtils.degToRad(rotation[1]),
|
|
286
|
+
THREE4__namespace.MathUtils.degToRad(rotation[2]),
|
|
287
|
+
EULER_ORDER
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
var MARKER_COLORS = [
|
|
292
|
+
"#8f8f8f",
|
|
293
|
+
// 0 - neutral / none
|
|
294
|
+
"#e0475e",
|
|
295
|
+
// 1 - red
|
|
296
|
+
"#e0a63f",
|
|
297
|
+
// 2 - orange
|
|
298
|
+
"#e5d24a",
|
|
299
|
+
// 3 - yellow
|
|
300
|
+
"#7cd15a",
|
|
301
|
+
// 4 - green
|
|
302
|
+
"#4b9fe0",
|
|
303
|
+
// 5 - blue
|
|
304
|
+
"#8054e0",
|
|
305
|
+
// 6 - purple
|
|
306
|
+
"#e055b0"
|
|
307
|
+
// 7 - pink
|
|
308
|
+
];
|
|
309
|
+
var MaterialLibrary = class {
|
|
310
|
+
constructor(model, settings) {
|
|
311
|
+
this.model = model;
|
|
312
|
+
this.settings = settings;
|
|
313
|
+
this.byKey = /* @__PURE__ */ new Map();
|
|
314
|
+
this.colorMaterials = /* @__PURE__ */ new Map();
|
|
315
|
+
this.disposables = [];
|
|
316
|
+
this.fallbackUv = {
|
|
317
|
+
width: model.resolution?.width || 16,
|
|
318
|
+
height: model.resolution?.height || 16
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Creates a texture + material per model texture and starts loading their
|
|
323
|
+
* images. Resolves once every image has settled (loaded or failed).
|
|
324
|
+
*/
|
|
325
|
+
async load(onProgress) {
|
|
326
|
+
const textures = this.model.textures ?? [];
|
|
327
|
+
await Promise.all(
|
|
328
|
+
textures.map((tex, index) => this.createTexture(tex, index, onProgress))
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
async createTexture(tex, index, onProgress) {
|
|
332
|
+
const uvWidth = tex.uv_width || tex.width || this.fallbackUv.width;
|
|
333
|
+
const uvHeight = tex.uv_height || tex.height || this.fallbackUv.height;
|
|
334
|
+
const texture = new THREE4__namespace.Texture();
|
|
335
|
+
texture.magFilter = THREE4__namespace.NearestFilter;
|
|
336
|
+
texture.minFilter = THREE4__namespace.NearestFilter;
|
|
337
|
+
texture.generateMipmaps = false;
|
|
338
|
+
texture.colorSpace = THREE4__namespace.SRGBColorSpace;
|
|
339
|
+
texture.name = tex.name ?? `texture_${index}`;
|
|
340
|
+
const material = new THREE4__namespace.MeshLambertMaterial({
|
|
341
|
+
map: texture,
|
|
342
|
+
transparent: true,
|
|
343
|
+
alphaTest: this.settings.alphaTest,
|
|
344
|
+
side: this.settings.side
|
|
345
|
+
});
|
|
346
|
+
const meta = { material, uvWidth, uvHeight };
|
|
347
|
+
this.byKey.set(String(index), meta);
|
|
348
|
+
if (tex.uuid) this.byKey.set(tex.uuid, meta);
|
|
349
|
+
if (tex.id != null) this.byKey.set(String(tex.id), meta);
|
|
350
|
+
this.disposables.push(material, texture);
|
|
351
|
+
const src = tex.source || tex.path || tex.relative_path;
|
|
352
|
+
if (!src) return;
|
|
353
|
+
try {
|
|
354
|
+
const image = await loadImage(src);
|
|
355
|
+
texture.image = image;
|
|
356
|
+
texture.needsUpdate = true;
|
|
357
|
+
onProgress?.();
|
|
358
|
+
} catch {
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/** Resolves a face's texture reference to its material + UV space, if any. */
|
|
362
|
+
resolve(ref) {
|
|
363
|
+
if (ref == null || ref === false) return null;
|
|
364
|
+
return this.byKey.get(String(ref)) ?? null;
|
|
365
|
+
}
|
|
366
|
+
/** UV space to use when a face has no resolvable texture. */
|
|
367
|
+
get fallbackUvSpace() {
|
|
368
|
+
return this.fallbackUv;
|
|
369
|
+
}
|
|
370
|
+
/** Flat color material for untextured faces, keyed by marker color index. */
|
|
371
|
+
colorMaterial(colorIndex = 0) {
|
|
372
|
+
const key = colorIndex % MARKER_COLORS.length;
|
|
373
|
+
let material = this.colorMaterials.get(key);
|
|
374
|
+
if (!material) {
|
|
375
|
+
material = new THREE4__namespace.MeshLambertMaterial({
|
|
376
|
+
color: new THREE4__namespace.Color(MARKER_COLORS[key]),
|
|
377
|
+
side: this.settings.side
|
|
378
|
+
});
|
|
379
|
+
this.colorMaterials.set(key, material);
|
|
380
|
+
this.disposables.push(material);
|
|
381
|
+
}
|
|
382
|
+
return material;
|
|
383
|
+
}
|
|
384
|
+
dispose() {
|
|
385
|
+
for (const item of this.disposables) item.dispose();
|
|
386
|
+
this.disposables.length = 0;
|
|
387
|
+
this.byKey.clear();
|
|
388
|
+
this.colorMaterials.clear();
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
function loadImage(src) {
|
|
392
|
+
return new Promise((resolve, reject) => {
|
|
393
|
+
const img = new Image();
|
|
394
|
+
img.crossOrigin = "anonymous";
|
|
395
|
+
img.onload = () => resolve(img);
|
|
396
|
+
img.onerror = () => reject(new Error(`Failed to load texture image`));
|
|
397
|
+
img.src = src;
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/viewer.ts
|
|
402
|
+
var DEFAULTS = {
|
|
403
|
+
background: "#20232a",
|
|
404
|
+
grid: true,
|
|
405
|
+
controls: true,
|
|
406
|
+
autoRotate: false,
|
|
407
|
+
autoRotateSpeed: 1,
|
|
408
|
+
fov: 45,
|
|
409
|
+
ambientIntensity: 0.8,
|
|
410
|
+
directionalIntensity: 0.55,
|
|
411
|
+
doubleSided: true,
|
|
412
|
+
alphaTest: 0.02,
|
|
413
|
+
antialias: true
|
|
414
|
+
};
|
|
415
|
+
var BBModelViewer = class {
|
|
416
|
+
constructor(container, options = {}) {
|
|
417
|
+
this.scene = new THREE4__namespace.Scene();
|
|
418
|
+
this.modelRoot = null;
|
|
419
|
+
this.materials = null;
|
|
420
|
+
this.gridHelper = null;
|
|
421
|
+
this.frameId = 0;
|
|
422
|
+
this.disposed = false;
|
|
423
|
+
this.loadToken = 0;
|
|
424
|
+
this.animate = () => {
|
|
425
|
+
if (this.disposed) return;
|
|
426
|
+
this.frameId = requestAnimationFrame(this.animate);
|
|
427
|
+
this.controls?.update();
|
|
428
|
+
this.renderer.render(this.scene, this.camera);
|
|
429
|
+
};
|
|
430
|
+
this.container = container;
|
|
431
|
+
this.options = { ...DEFAULTS, ...options };
|
|
432
|
+
const { width, height } = this.measure();
|
|
433
|
+
const transparent = this.options.background === null || this.options.background === "transparent";
|
|
434
|
+
this.renderer = new THREE4__namespace.WebGLRenderer({
|
|
435
|
+
antialias: this.options.antialias,
|
|
436
|
+
alpha: transparent
|
|
437
|
+
});
|
|
438
|
+
this.renderer.setPixelRatio(options.pixelRatio ?? Math.min(window.devicePixelRatio, 2));
|
|
439
|
+
this.renderer.setSize(width, height, false);
|
|
440
|
+
this.renderer.outputColorSpace = THREE4__namespace.SRGBColorSpace;
|
|
441
|
+
this.container.appendChild(this.renderer.domElement);
|
|
442
|
+
this.renderer.domElement.style.display = "block";
|
|
443
|
+
this.renderer.domElement.style.width = "100%";
|
|
444
|
+
this.renderer.domElement.style.height = "100%";
|
|
445
|
+
if (!transparent) {
|
|
446
|
+
this.scene.background = new THREE4__namespace.Color(this.options.background);
|
|
447
|
+
}
|
|
448
|
+
this.camera = new THREE4__namespace.PerspectiveCamera(this.options.fov, width / height, 0.1, 1e4);
|
|
449
|
+
this.camera.position.set(40, 30, 40);
|
|
450
|
+
this.addLights();
|
|
451
|
+
if (this.options.controls) {
|
|
452
|
+
this.controls = new OrbitControls_js.OrbitControls(this.camera, this.renderer.domElement);
|
|
453
|
+
this.controls.enableDamping = true;
|
|
454
|
+
this.controls.dampingFactor = 0.08;
|
|
455
|
+
this.controls.autoRotate = this.options.autoRotate;
|
|
456
|
+
this.controls.autoRotateSpeed = this.options.autoRotateSpeed;
|
|
457
|
+
}
|
|
458
|
+
this.observeResize();
|
|
459
|
+
this.animate();
|
|
460
|
+
}
|
|
461
|
+
/** Fetches a `.bbmodel` file by URL and renders it. */
|
|
462
|
+
async load(url, init) {
|
|
463
|
+
try {
|
|
464
|
+
const response = await fetch(url, init);
|
|
465
|
+
if (!response.ok) {
|
|
466
|
+
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
|
|
467
|
+
}
|
|
468
|
+
const data = await response.json();
|
|
469
|
+
return await this.loadModel(data);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
this.options.onError?.(error);
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
/** Reads a `File`/`Blob` (e.g. from an `<input type="file">`) and renders it. */
|
|
476
|
+
async loadFile(file) {
|
|
477
|
+
try {
|
|
478
|
+
const text = await file.text();
|
|
479
|
+
const data = JSON.parse(text);
|
|
480
|
+
return await this.loadModel(data);
|
|
481
|
+
} catch (error) {
|
|
482
|
+
this.options.onError?.(error);
|
|
483
|
+
throw error;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/** Renders an already-parsed bbmodel object. */
|
|
487
|
+
async loadModel(data) {
|
|
488
|
+
const token = ++this.loadToken;
|
|
489
|
+
try {
|
|
490
|
+
const materials = new MaterialLibrary(data, {
|
|
491
|
+
alphaTest: this.options.alphaTest,
|
|
492
|
+
side: this.options.doubleSided ? THREE4__namespace.DoubleSide : THREE4__namespace.FrontSide
|
|
493
|
+
});
|
|
494
|
+
await materials.load();
|
|
495
|
+
if (token !== this.loadToken || this.disposed) {
|
|
496
|
+
materials.dispose();
|
|
497
|
+
return this.modelRoot ?? new THREE4__namespace.Group();
|
|
498
|
+
}
|
|
499
|
+
this.clear();
|
|
500
|
+
this.materials = materials;
|
|
501
|
+
this.modelRoot = buildModelObject(data, materials);
|
|
502
|
+
this.scene.add(this.modelRoot);
|
|
503
|
+
this.frameModel();
|
|
504
|
+
this.options.onLoad?.(data, this.modelRoot);
|
|
505
|
+
return this.modelRoot;
|
|
506
|
+
} catch (error) {
|
|
507
|
+
this.options.onError?.(error);
|
|
508
|
+
throw error;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
/** Removes the current model and frees its GPU resources. */
|
|
512
|
+
clear() {
|
|
513
|
+
if (this.modelRoot) {
|
|
514
|
+
this.scene.remove(this.modelRoot);
|
|
515
|
+
this.modelRoot.traverse((obj) => {
|
|
516
|
+
if (obj instanceof THREE4__namespace.Mesh) obj.geometry.dispose();
|
|
517
|
+
});
|
|
518
|
+
this.modelRoot = null;
|
|
519
|
+
}
|
|
520
|
+
if (this.gridHelper) {
|
|
521
|
+
this.scene.remove(this.gridHelper);
|
|
522
|
+
this.gridHelper.geometry.dispose();
|
|
523
|
+
this.gridHelper.material.dispose();
|
|
524
|
+
this.gridHelper = null;
|
|
525
|
+
}
|
|
526
|
+
this.materials?.dispose();
|
|
527
|
+
this.materials = null;
|
|
528
|
+
}
|
|
529
|
+
/** Recomputes size from the container; call after layout changes if needed. */
|
|
530
|
+
resize() {
|
|
531
|
+
const { width, height } = this.measure();
|
|
532
|
+
this.renderer.setSize(width, height, false);
|
|
533
|
+
this.camera.aspect = width / height;
|
|
534
|
+
this.camera.updateProjectionMatrix();
|
|
535
|
+
}
|
|
536
|
+
/** Frames the camera on the current model's bounding box. */
|
|
537
|
+
frameModel() {
|
|
538
|
+
if (!this.modelRoot) return;
|
|
539
|
+
const box = new THREE4__namespace.Box3().setFromObject(this.modelRoot);
|
|
540
|
+
if (box.isEmpty()) return;
|
|
541
|
+
const size = box.getSize(new THREE4__namespace.Vector3());
|
|
542
|
+
const center = box.getCenter(new THREE4__namespace.Vector3());
|
|
543
|
+
const maxDim = Math.max(size.x, size.y, size.z) || 16;
|
|
544
|
+
if (this.options.grid) this.addGrid(box, maxDim);
|
|
545
|
+
const fov = THREE4__namespace.MathUtils.degToRad(this.camera.fov);
|
|
546
|
+
const distance = maxDim / 2 / Math.tan(fov / 2) * 1.6;
|
|
547
|
+
const dir = new THREE4__namespace.Vector3(1, 0.75, 1).normalize();
|
|
548
|
+
this.camera.position.copy(center).addScaledVector(dir, distance);
|
|
549
|
+
this.camera.near = Math.max(distance / 100, 0.01);
|
|
550
|
+
this.camera.far = distance * 100;
|
|
551
|
+
this.camera.updateProjectionMatrix();
|
|
552
|
+
if (this.controls) {
|
|
553
|
+
this.controls.target.copy(center);
|
|
554
|
+
this.controls.update();
|
|
555
|
+
} else {
|
|
556
|
+
this.camera.lookAt(center);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
/** Tears down the renderer, listeners and GPU resources. */
|
|
560
|
+
dispose() {
|
|
561
|
+
this.disposed = true;
|
|
562
|
+
cancelAnimationFrame(this.frameId);
|
|
563
|
+
this.resizeObserver?.disconnect();
|
|
564
|
+
this.clear();
|
|
565
|
+
this.controls?.dispose();
|
|
566
|
+
this.renderer.dispose();
|
|
567
|
+
this.renderer.domElement.remove();
|
|
568
|
+
}
|
|
569
|
+
// --- internals ---
|
|
570
|
+
addLights() {
|
|
571
|
+
const ambient = new THREE4__namespace.AmbientLight(16777215, this.options.ambientIntensity);
|
|
572
|
+
const key = new THREE4__namespace.DirectionalLight(16777215, this.options.directionalIntensity);
|
|
573
|
+
key.position.set(0.6, 1, 0.8);
|
|
574
|
+
const fill = new THREE4__namespace.DirectionalLight(16777215, this.options.directionalIntensity * 0.45);
|
|
575
|
+
fill.position.set(-0.6, -0.2, -0.8);
|
|
576
|
+
this.scene.add(ambient, key, fill);
|
|
577
|
+
}
|
|
578
|
+
addGrid(box, maxDim) {
|
|
579
|
+
const cells = Math.max(2, Math.ceil(maxDim / 16));
|
|
580
|
+
const size = cells * 16 * 1.5;
|
|
581
|
+
const divisions = Math.round(size / 16);
|
|
582
|
+
const grid = new THREE4__namespace.GridHelper(size, divisions, 6710886, 3815994);
|
|
583
|
+
grid.position.set(box.getCenter(new THREE4__namespace.Vector3()).x, box.min.y, box.getCenter(new THREE4__namespace.Vector3()).z);
|
|
584
|
+
grid.material.transparent = true;
|
|
585
|
+
grid.material.opacity = 0.5;
|
|
586
|
+
this.scene.add(grid);
|
|
587
|
+
this.gridHelper = grid;
|
|
588
|
+
}
|
|
589
|
+
observeResize() {
|
|
590
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
591
|
+
this.resizeObserver = new ResizeObserver(() => this.resize());
|
|
592
|
+
this.resizeObserver.observe(this.container);
|
|
593
|
+
}
|
|
594
|
+
measure() {
|
|
595
|
+
const width = this.container.clientWidth || 300;
|
|
596
|
+
const height = this.container.clientHeight || 150;
|
|
597
|
+
return { width, height };
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
exports.BBModelViewer = BBModelViewer;
|
|
602
|
+
exports.MaterialLibrary = MaterialLibrary;
|
|
603
|
+
exports.buildElementMesh = buildElementMesh;
|
|
604
|
+
exports.buildModelObject = buildModelObject;
|
|
605
|
+
//# sourceMappingURL=index.cjs.map
|
|
606
|
+
//# sourceMappingURL=index.cjs.map
|