3dviewer-sdk 1.0.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/dist/index.js ADDED
@@ -0,0 +1,484 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ HcViewer: () => HcViewer
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/core/emitter.ts
28
+ var Emitter = class {
29
+ constructor() {
30
+ this.listeners = {};
31
+ }
32
+ on(event, cb) {
33
+ var _a;
34
+ const arr = (_a = this.listeners)[event] || (_a[event] = []);
35
+ arr.push(cb);
36
+ return () => this.off(event, cb);
37
+ }
38
+ off(event, cb) {
39
+ const arr = this.listeners[event];
40
+ if (!arr) return;
41
+ const idx = arr.indexOf(cb);
42
+ if (idx >= 0) arr.splice(idx, 1);
43
+ if (arr.length === 0) delete this.listeners[event];
44
+ }
45
+ emit(event, payload) {
46
+ var _a;
47
+ (_a = this.listeners[event]) == null ? void 0 : _a.forEach((cb) => cb(payload));
48
+ }
49
+ clear() {
50
+ this.listeners = {};
51
+ }
52
+ };
53
+
54
+ // src/modules/camera.module.ts
55
+ var CameraModule = class {
56
+ constructor(viewer) {
57
+ this.viewer = viewer;
58
+ this.on = {
59
+ home: (cb) => this.viewer._on("camera:home", cb)
60
+ };
61
+ }
62
+ zoomIn(percent) {
63
+ this.viewer.postToViewer("viewer-zoom" /* ZOOM */, { action: "in", percent });
64
+ }
65
+ zoomOut(percent) {
66
+ this.viewer.postToViewer("viewer-zoom" /* ZOOM */, { action: "out", percent });
67
+ }
68
+ home() {
69
+ this.viewer.postToViewer("viewer-home" /* HOME */, {});
70
+ }
71
+ };
72
+
73
+ // src/modules/interaction.module.ts
74
+ var InteractionModule = class {
75
+ constructor(viewer) {
76
+ this.viewer = viewer;
77
+ this.on = {
78
+ panChange: (cb) => this.viewer._on("interaction:pan-change", cb)
79
+ };
80
+ }
81
+ enablePan() {
82
+ this.viewer.postToViewer("viewer-pan-toggle" /* PAN_TOGGLE */, { enabled: true });
83
+ }
84
+ disablePan() {
85
+ this.viewer.postToViewer("viewer-pan-toggle" /* PAN_TOGGLE */, { enabled: false });
86
+ }
87
+ };
88
+
89
+ // src/modules/node.module.ts
90
+ var NodeModule = class {
91
+ constructor(viewer) {
92
+ this.viewer = viewer;
93
+ this.on = { select: (cb) => this.viewer._on("node:select", cb) };
94
+ }
95
+ };
96
+
97
+ // src/modules/files.module.ts
98
+ var DEFAULT_API_BASE_URL = "https://dev.3dviewer.anybim.vn";
99
+ var DEFAULT_VIEWER_ORIGIN = "http://localhost:3000";
100
+ var FilesModule = class {
101
+ constructor(viewer) {
102
+ this.viewer = viewer;
103
+ this.config = {};
104
+ this.operationStartTime = 0;
105
+ this.state = {
106
+ isLoading: false,
107
+ stage: "idle"
108
+ };
109
+ this.lastUploadSession = null;
110
+ this.on = {
111
+ state: (cb) => this.viewer._on("files:state", cb),
112
+ uploadStart: (cb) => this.viewer._on("files:upload:start", cb),
113
+ uploadSuccess: (cb) => this.viewer._on("files:upload:success", cb),
114
+ uploadError: (cb) => this.viewer._on("files:upload:error", cb),
115
+ conversionStart: (cb) => this.viewer._on("files:conversion:start", cb),
116
+ conversionSuccess: (cb) => this.viewer._on("files:conversion:success", cb),
117
+ conversionError: (cb) => this.viewer._on("files:conversion:error", cb),
118
+ renderStart: (cb) => this.viewer._on("files:render:start", cb),
119
+ renderSuccess: (cb) => this.viewer._on("files:render:success", cb),
120
+ renderError: (cb) => this.viewer._on("files:render:error", cb),
121
+ loadSuccess: (cb) => this.viewer._on("files:load:success", cb),
122
+ loadError: (cb) => this.viewer._on("files:load:error", cb)
123
+ };
124
+ }
125
+ setConfig(next) {
126
+ this.config = { ...this.config, ...next };
127
+ }
128
+ getState() {
129
+ return { ...this.state };
130
+ }
131
+ // ---------- public pipeline ----------
132
+ async upload(file) {
133
+ const target = this.resolveFile(file);
134
+ return this.withOperation({ stage: "uploading", message: "Uploading file..." }, async () => {
135
+ var _a;
136
+ this.viewer._emit("files:upload:start", { fileName: target.name });
137
+ await this.uploadInternal(target);
138
+ const baseFileId = ((_a = this.getUploadSessionForFile(target)) == null ? void 0 : _a.baseFileId) || "";
139
+ this.viewer._emit("files:upload:success", { fileName: target.name, baseFileId });
140
+ return { fileName: target.name, baseFileId };
141
+ });
142
+ }
143
+ async convert(file) {
144
+ const target = this.resolveFile(file);
145
+ return this.withOperation({ stage: "converting", message: "Converting file..." }, async () => {
146
+ this.viewer._emit("files:conversion:start", { fileName: target.name });
147
+ try {
148
+ const prepared = await this.convertInternal(target);
149
+ this.viewer._emit("files:conversion:success", prepared);
150
+ return prepared;
151
+ } catch (e) {
152
+ this.viewer._emit("files:conversion:error", { fileName: target.name, error: this.toErrorMessage(e) });
153
+ throw e;
154
+ }
155
+ });
156
+ }
157
+ async prepare(file) {
158
+ const target = this.resolveFile(file);
159
+ return this.withOperation({ stage: "uploading", message: "Preparing file..." }, async () => {
160
+ await this.uploadInternal(target);
161
+ const prepared = await this.convertInternal(target);
162
+ return prepared;
163
+ });
164
+ }
165
+ open(input) {
166
+ const url = input.url;
167
+ this.viewer._emit("files:render:start", { url });
168
+ try {
169
+ this.viewer.open(url);
170
+ this.viewer._emit("files:render:success", { url });
171
+ } catch (e) {
172
+ this.viewer._emit("files:render:error", { url, error: this.toErrorMessage(e) });
173
+ throw e;
174
+ }
175
+ }
176
+ async render(file) {
177
+ const target = this.resolveFile(file);
178
+ return this.withOperation({ stage: "uploading", message: "Uploading + converting + opening..." }, async () => {
179
+ await this.upload(target);
180
+ const prepared = await this.convert(target);
181
+ this.updateState({ stage: "rendering", message: "Opening viewer..." });
182
+ this.open(prepared);
183
+ this.viewer._emit("files:load:success", prepared);
184
+ return prepared;
185
+ });
186
+ }
187
+ resolveFile(file) {
188
+ const optFile = this.viewer.getOptions().file;
189
+ const target = file || optFile;
190
+ if (!target) throw new Error("No file provided. Pass a File or set options.file");
191
+ this.viewer.patchOptions({ file: target });
192
+ return target;
193
+ }
194
+ normalizeBaseUrl(input) {
195
+ return input.trim().replace(/\/+$/, "");
196
+ }
197
+ resolveBaseUrl() {
198
+ const raw = this.config.baseUrl || this.viewer.getOptions().baseUrl || DEFAULT_API_BASE_URL;
199
+ return this.normalizeBaseUrl(raw);
200
+ }
201
+ resolveViewerPath() {
202
+ const p = (this.config.viewerPath || this.viewer.getOptions().viewerPath || "/mainviewer").trim();
203
+ if (!p) return "/mainviewer";
204
+ return p.startsWith("/") ? p : `/${p}`;
205
+ }
206
+ resolveViewerOrigin() {
207
+ return this.normalizeBaseUrl(DEFAULT_VIEWER_ORIGIN);
208
+ }
209
+ resolveHostConversion() {
210
+ const baseUrl = this.resolveBaseUrl();
211
+ return baseUrl.endsWith("/service/conversion") ? baseUrl : `${baseUrl}/service/conversion`;
212
+ }
213
+ getUploadPath() {
214
+ return this.config.uploadPath || this.viewer.getOptions().uploadPath || ".";
215
+ }
216
+ fileSignature(file) {
217
+ return `${file.name}::${file.size}::${file.lastModified}`;
218
+ }
219
+ createBaseFileId() {
220
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
221
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
222
+ const r = Math.floor(Math.random() * 16);
223
+ const v = c === "x" ? r : r & 3 | 8;
224
+ return v.toString(16);
225
+ });
226
+ }
227
+ createUploadSession(file) {
228
+ return {
229
+ signature: this.fileSignature(file),
230
+ baseFileId: this.createBaseFileId(),
231
+ fileName: file.name,
232
+ uploadPath: this.getUploadPath()
233
+ };
234
+ }
235
+ getUploadSessionForFile(file) {
236
+ if (!this.lastUploadSession) return null;
237
+ const sameFile = this.lastUploadSession.signature === this.fileSignature(file);
238
+ const samePath = this.lastUploadSession.uploadPath === this.getUploadPath();
239
+ return sameFile && samePath ? this.lastUploadSession : null;
240
+ }
241
+ async uploadInternal(file) {
242
+ this.updateState({ stage: "uploading", message: "Uploading file..." });
243
+ try {
244
+ const existing = this.getUploadSessionForFile(file);
245
+ const session = existing || this.createUploadSession(file);
246
+ const hostConversion = this.resolveHostConversion();
247
+ const path = this.getUploadPath();
248
+ const url = `${hostConversion}/api/File/upload?path=${encodeURIComponent(path)}`;
249
+ const formData = new FormData();
250
+ formData.append("file", file, file.name);
251
+ const res = await fetch(url, { method: "POST", body: formData, headers: { Accept: "text/plain" } });
252
+ if (!res.ok) throw new Error(`Upload failed (${res.status} ${res.statusText})`);
253
+ this.lastUploadSession = session;
254
+ } catch (e) {
255
+ const msg = this.toErrorMessage(e);
256
+ this.viewer._emit("files:upload:error", { fileName: file.name, error: msg });
257
+ throw e;
258
+ }
259
+ }
260
+ buildCachePayload(file, baseFileId) {
261
+ const createdDate = (/* @__PURE__ */ new Date()).toISOString();
262
+ return {
263
+ filename: file.name,
264
+ baseFileId,
265
+ baseMajorRev: 0,
266
+ baseMinorRev: 0,
267
+ isChecked: false,
268
+ status: { size: file.size },
269
+ child: [],
270
+ isDirectory: false,
271
+ createdDate,
272
+ cacheStatus: 0,
273
+ modelFileId: "",
274
+ id: "",
275
+ originalFilePath: this.getUploadPath(),
276
+ streamLocation: null,
277
+ converter: "Hoops",
278
+ originalSize: 0,
279
+ cacheSize: 0,
280
+ importTime: 0,
281
+ importAssemblyTreeTime: 0,
282
+ creator: {
283
+ id: "00000000-0000-0000-0000-000000000000",
284
+ name: "Anonymous"
285
+ },
286
+ originalFile: file.name,
287
+ multiStream: false,
288
+ isRootModel: 0,
289
+ extraConvertOutput: "",
290
+ cacheFilename: null,
291
+ errorMassage: null,
292
+ convertOptions: {
293
+ convert3DModel: 1,
294
+ convert2DSheet: 1,
295
+ extractProperties: 1,
296
+ childModels: 0
297
+ },
298
+ drawingConvertStatus: {
299
+ convert3DModel: 5,
300
+ convert2DSheet: 5,
301
+ extractProperties: 5
302
+ },
303
+ attemptedConvertTimes: 0
304
+ };
305
+ }
306
+ async cacheFile(file, baseFileId) {
307
+ const hostConversion = await this.resolveHostConversion();
308
+ const url = `${hostConversion}/api/StreamFile?overwrite=true&ignore_line_weight=1`;
309
+ const payload = this.buildCachePayload(file, baseFileId);
310
+ const response = await fetch(url, {
311
+ method: "POST",
312
+ headers: {
313
+ "Content-Type": "application/json",
314
+ Accept: "application/json"
315
+ },
316
+ body: JSON.stringify(payload)
317
+ });
318
+ if (!response.ok) {
319
+ throw new Error(
320
+ `Cache/convert failed (${response.status} ${response.statusText})`
321
+ );
322
+ }
323
+ return await response.json();
324
+ }
325
+ async convertInternal(file) {
326
+ var _a, _b, _c;
327
+ this.updateState({ stage: "converting", message: "Converting file..." });
328
+ const uploadSession = this.getUploadSessionForFile(file) || this.createUploadSession(file);
329
+ const seedBaseFileId = uploadSession.baseFileId;
330
+ const cacheResult = await this.cacheFile(file, seedBaseFileId);
331
+ const baseFileId = (_a = cacheResult.baseFileId) != null ? _a : seedBaseFileId;
332
+ const baseMajorRev = (_b = cacheResult.baseMajorRev) != null ? _b : 0;
333
+ const baseMinorRev = (_c = cacheResult.baseMinorRev) != null ? _c : 0;
334
+ const fileName = cacheResult.filename || file.name;
335
+ const cacheListItem = { baseFileId, baseMajorRev, baseMinorRev, fileName };
336
+ if (cacheResult.cacheStatus !== 2) {
337
+ throw new Error(`Conversion not ready after first request (cacheStatus=${cacheResult.cacheStatus ?? "unknown"})`);
338
+ }
339
+ const query = new URLSearchParams({ fileList: JSON.stringify([cacheListItem]) }).toString();
340
+ const viewerBase = this.resolveViewerOrigin();
341
+ const viewerPath = this.resolveViewerPath();
342
+ const url = `${viewerBase}${viewerPath}?${query}`;
343
+ return { baseFileId, baseMajorRev, baseMinorRev, fileName, query, url };
344
+ }
345
+ updateState(next) {
346
+ const elapsedMs = this.operationStartTime > 0 ? Date.now() - this.operationStartTime : 0;
347
+ this.state = { ...this.state, ...next, elapsedMs };
348
+ this.viewer._emit("files:state", this.state);
349
+ }
350
+ async withOperation(initial, run) {
351
+ this.operationStartTime = Date.now();
352
+ this.updateState({
353
+ isLoading: true,
354
+ stage: initial.stage,
355
+ message: initial.message
356
+ });
357
+ try {
358
+ const result = await run();
359
+ this.updateState({ isLoading: false, stage: "completed", message: "Completed" });
360
+ return result;
361
+ } catch (e) {
362
+ const msg = this.toErrorMessage(e);
363
+ this.updateState({ isLoading: false, stage: "error", message: msg });
364
+ this.viewer._emit("files:load:error", { error: msg });
365
+ throw e;
366
+ }
367
+ }
368
+ toErrorMessage(e) {
369
+ return e instanceof Error ? e.message : String(e);
370
+ }
371
+ };
372
+
373
+ // src/viewer.ts
374
+ var HcViewer = class {
375
+ constructor(options) {
376
+ this.options = options;
377
+ this.containerEl = null;
378
+ this.iframeEl = null;
379
+ this.initialized = false;
380
+ this.emitter = new Emitter();
381
+ this.handleMessage = (event) => {
382
+ var _a, _b, _c;
383
+ const data = event.data;
384
+ if (!data || typeof data !== "object") return;
385
+ switch (data.type) {
386
+ case "viewer-home-click" /* HOME_CLICK */:
387
+ this._emit("camera:home", { timestamp: Date.now() });
388
+ break;
389
+ case "viewer-node-select" /* NODE_SELECT */:
390
+ this._emit("node:select", { nodeId: String((_b = (_a = data.payload) == null ? void 0 : _a.nodeId) != null ? _b : ""), timestamp: Date.now() });
391
+ break;
392
+ case "viewer-pan-change" /* PAN_CHANGE */:
393
+ this._emit("interaction:pan-change", { enabled: Boolean((_c = data.payload) == null ? void 0 : _c.enabled) });
394
+ break;
395
+ default:
396
+ break;
397
+ }
398
+ };
399
+ this.camera = new CameraModule(this);
400
+ this.interaction = new InteractionModule(this);
401
+ this.node = new NodeModule(this);
402
+ this.files = new FilesModule(this);
403
+ }
404
+ // ===== options helpers =====
405
+ getOptions() {
406
+ return this.options;
407
+ }
408
+ patchOptions(next) {
409
+ this.options = { ...this.options, ...next };
410
+ }
411
+ getUrl() {
412
+ var _a;
413
+ return (_a = this.options.url) != null ? _a : null;
414
+ }
415
+ // ===== lifecycle =====
416
+ init() {
417
+ if (this.initialized) return;
418
+ this.containerEl = typeof this.options.container === "string" ? document.querySelector(this.options.container) : this.options.container;
419
+ if (!this.containerEl) throw new Error("Container element not found");
420
+ window.addEventListener("message", this.handleMessage);
421
+ this.initialized = true;
422
+ }
423
+ async render(file) {
424
+ this.ensureInit();
425
+ if (this.iframeEl) return;
426
+ if (!this.options.url) return this.files.render(file);
427
+ const iframe = document.createElement("iframe");
428
+ iframe.src = this.options.url;
429
+ iframe.style.border = "none";
430
+ iframe.style.width = this.options.width || "100%";
431
+ iframe.style.height = this.options.height || "100%";
432
+ iframe.width = this.options.width || "100%";
433
+ iframe.height = this.options.height || "100%";
434
+ if (this.options.sandbox) iframe.setAttribute("sandbox", this.options.sandbox);
435
+ this.containerEl.appendChild(iframe);
436
+ this.iframeEl = iframe;
437
+ }
438
+ open(url) {
439
+ this.ensureInit();
440
+ this.options.url = url;
441
+ if (!this.iframeEl) {
442
+ this.render();
443
+ return;
444
+ }
445
+ this.iframeEl.src = url;
446
+ }
447
+ destroy() {
448
+ window.removeEventListener("message", this.handleMessage);
449
+ if (this.iframeEl && this.containerEl) {
450
+ try {
451
+ this.containerEl.removeChild(this.iframeEl);
452
+ } catch {
453
+ }
454
+ }
455
+ this.iframeEl = null;
456
+ this.containerEl = null;
457
+ this.initialized = false;
458
+ }
459
+ ensureInit() {
460
+ if (!this.initialized) throw new Error("Call viewer.init() before using viewer");
461
+ }
462
+ // ===== typed internal events (modules dùng) =====
463
+ _on(event, cb) {
464
+ return this.emitter.on(event, cb);
465
+ }
466
+ _off(event, cb) {
467
+ this.emitter.off(event, cb);
468
+ }
469
+ _emit(event, payload) {
470
+ this.emitter.emit(event, payload);
471
+ }
472
+ // ===== postMessage bridge =====
473
+ postToViewer(type, payload) {
474
+ var _a;
475
+ if (!((_a = this.iframeEl) == null ? void 0 : _a.contentWindow)) return;
476
+ const message = { source: "HC_SDK", type, payload };
477
+ const targetOrigin = this.options.allowedOrigin || "*";
478
+ this.iframeEl.contentWindow.postMessage(message, targetOrigin);
479
+ }
480
+ };
481
+ // Annotate the CommonJS export names for ESM import in node:
482
+ 0 && (module.exports = {
483
+ HcViewer
484
+ });