3dviewer-sdk 1.0.1 → 1.0.3
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/contracts/events.d.ts +97 -0
- package/dist/contracts/events.js +1 -0
- package/dist/contracts/messages.d.ts +153 -0
- package/dist/contracts/messages.js +49 -0
- package/dist/core/emitter.d.ts +7 -0
- package/dist/core/emitter.js +31 -0
- package/dist/index.d.mts +70 -4
- package/dist/index.d.ts +70 -4
- package/dist/index.js +29 -11
- package/dist/index.mjs +29 -11
- package/dist/modules/camera.module.d.ts +13 -0
- package/dist/modules/camera.module.js +18 -0
- package/dist/modules/files.module.d.ts +84 -0
- package/dist/modules/files.module.js +315 -0
- package/dist/modules/interaction.module.d.ts +28 -0
- package/dist/modules/interaction.module.js +63 -0
- package/dist/modules/language.module.d.ts +8 -0
- package/dist/modules/language.module.js +15 -0
- package/dist/modules/markup.module.d.ts +26 -0
- package/dist/modules/markup.module.js +96 -0
- package/dist/modules/model-tree.module.d.ts +12 -0
- package/dist/modules/model-tree.module.js +42 -0
- package/dist/modules/node.module.d.ts +11 -0
- package/dist/modules/node.module.js +6 -0
- package/dist/modules/toolbar.module.d.ts +83 -0
- package/dist/modules/toolbar.module.js +219 -0
- package/dist/viewer.d.ts +51 -0
- package/dist/viewer.js +277 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -194,12 +194,12 @@ var FilesModule = class {
|
|
|
194
194
|
});
|
|
195
195
|
}
|
|
196
196
|
// Trigger conversion flow and resolve final viewer metadata.
|
|
197
|
-
async convert(file) {
|
|
197
|
+
async convert(file, options = {}) {
|
|
198
198
|
const target = this.resolveFile(file);
|
|
199
199
|
return this.withOperation({ stage: "converting", message: "Converting file..." }, async () => {
|
|
200
200
|
this.viewer._emit("files:conversion:start", { fileName: target.name });
|
|
201
201
|
try {
|
|
202
|
-
const prepared = await this.convertInternal(target);
|
|
202
|
+
const prepared = await this.convertInternal(target, options);
|
|
203
203
|
this.viewer._emit("files:conversion:success", prepared);
|
|
204
204
|
return prepared;
|
|
205
205
|
} catch (e) {
|
|
@@ -209,11 +209,11 @@ var FilesModule = class {
|
|
|
209
209
|
});
|
|
210
210
|
}
|
|
211
211
|
// Convenience API: upload first, then convert in one call.
|
|
212
|
-
async prepare(file) {
|
|
212
|
+
async prepare(file, options = {}) {
|
|
213
213
|
const target = this.resolveFile(file);
|
|
214
214
|
return this.withOperation({ stage: "uploading", message: "Preparing file..." }, async () => {
|
|
215
215
|
await this.uploadInternal(target);
|
|
216
|
-
const prepared = await this.convertInternal(target);
|
|
216
|
+
const prepared = await this.convertInternal(target, options);
|
|
217
217
|
return prepared;
|
|
218
218
|
});
|
|
219
219
|
}
|
|
@@ -230,11 +230,11 @@ var FilesModule = class {
|
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
232
|
// Full pipeline: upload + convert + open iframe.
|
|
233
|
-
async render(file) {
|
|
233
|
+
async render(file, options = {}) {
|
|
234
234
|
const target = this.resolveFile(file);
|
|
235
235
|
return this.withOperation({ stage: "uploading", message: "Uploading + converting + opening..." }, async () => {
|
|
236
236
|
await this.upload(target);
|
|
237
|
-
const prepared = await this.convert(target);
|
|
237
|
+
const prepared = await this.convert(target, options);
|
|
238
238
|
this.updateState({ stage: "rendering", message: "Opening viewer..." });
|
|
239
239
|
this.open(prepared);
|
|
240
240
|
this.viewer._emit("files:load:success", prepared);
|
|
@@ -327,10 +327,11 @@ var FilesModule = class {
|
|
|
327
327
|
}
|
|
328
328
|
}
|
|
329
329
|
// Build StreamFile payload compatible with conversion service.
|
|
330
|
-
buildCachePayload(file, baseFileId) {
|
|
330
|
+
buildCachePayload(file, baseFileId, options = {}) {
|
|
331
331
|
const createdDate = (/* @__PURE__ */ new Date()).toISOString();
|
|
332
332
|
return {
|
|
333
333
|
filename: file.name,
|
|
334
|
+
...options.downloadUrl ? { downloadUrl: options.downloadUrl } : {},
|
|
334
335
|
baseFileId,
|
|
335
336
|
baseMajorRev: 0,
|
|
336
337
|
baseMinorRev: 0,
|
|
@@ -374,10 +375,10 @@ var FilesModule = class {
|
|
|
374
375
|
};
|
|
375
376
|
}
|
|
376
377
|
// Submit conversion/caching request and return service response.
|
|
377
|
-
async cacheFile(file, baseFileId) {
|
|
378
|
+
async cacheFile(file, baseFileId, options = {}) {
|
|
378
379
|
const hostConversion = await this.resolveHostConversion();
|
|
379
380
|
const url = `${hostConversion}/api/StreamFile?overwrite=true&ignore_line_weight=1`;
|
|
380
|
-
const payload = this.buildCachePayload(file, baseFileId);
|
|
381
|
+
const payload = this.buildCachePayload(file, baseFileId, options);
|
|
381
382
|
const response = await fetch(url, {
|
|
382
383
|
method: "POST",
|
|
383
384
|
headers: {
|
|
@@ -394,12 +395,12 @@ var FilesModule = class {
|
|
|
394
395
|
return await response.json();
|
|
395
396
|
}
|
|
396
397
|
// Convert file and generate final iframe URL with query string.
|
|
397
|
-
async convertInternal(file) {
|
|
398
|
+
async convertInternal(file, options = {}) {
|
|
398
399
|
var _a, _b, _c, _d;
|
|
399
400
|
this.updateState({ stage: "converting", message: "Converting file..." });
|
|
400
401
|
const uploadSession = this.getUploadSessionForFile(file) || this.createUploadSession(file);
|
|
401
402
|
const seedBaseFileId = uploadSession.baseFileId;
|
|
402
|
-
const cacheResult = await this.cacheFile(file, seedBaseFileId);
|
|
403
|
+
const cacheResult = await this.cacheFile(file, seedBaseFileId, options);
|
|
403
404
|
const baseFileId = (_a = cacheResult.baseFileId) != null ? _a : seedBaseFileId;
|
|
404
405
|
const baseMajorRev = (_b = cacheResult.baseMajorRev) != null ? _b : 0;
|
|
405
406
|
const baseMinorRev = (_c = cacheResult.baseMinorRev) != null ? _c : 0;
|
|
@@ -801,6 +802,22 @@ var MarkupModule = class {
|
|
|
801
802
|
}
|
|
802
803
|
};
|
|
803
804
|
|
|
805
|
+
// src/modules/language.module.ts
|
|
806
|
+
var LanguageModule = class {
|
|
807
|
+
constructor(viewer) {
|
|
808
|
+
this.viewer = viewer;
|
|
809
|
+
}
|
|
810
|
+
change(language) {
|
|
811
|
+
this.viewer.postToViewer("viewer-language-change" /* LANGUAGE_CHANGE */, {
|
|
812
|
+
language,
|
|
813
|
+
timestamp: Date.now()
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
set(language) {
|
|
817
|
+
this.change(language);
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
|
|
804
821
|
// src/viewer.ts
|
|
805
822
|
var Viewer3D = class {
|
|
806
823
|
constructor(options) {
|
|
@@ -963,6 +980,7 @@ var Viewer3D = class {
|
|
|
963
980
|
this.toolbar = new ToolbarModule(this);
|
|
964
981
|
this.modelTree = new ModelTreeModule(this);
|
|
965
982
|
this.markup = new MarkupModule(this);
|
|
983
|
+
this.language = new LanguageModule(this);
|
|
966
984
|
}
|
|
967
985
|
// ===== options helpers =====
|
|
968
986
|
getOptions() {
|
package/dist/index.mjs
CHANGED
|
@@ -168,12 +168,12 @@ var FilesModule = class {
|
|
|
168
168
|
});
|
|
169
169
|
}
|
|
170
170
|
// Trigger conversion flow and resolve final viewer metadata.
|
|
171
|
-
async convert(file) {
|
|
171
|
+
async convert(file, options = {}) {
|
|
172
172
|
const target = this.resolveFile(file);
|
|
173
173
|
return this.withOperation({ stage: "converting", message: "Converting file..." }, async () => {
|
|
174
174
|
this.viewer._emit("files:conversion:start", { fileName: target.name });
|
|
175
175
|
try {
|
|
176
|
-
const prepared = await this.convertInternal(target);
|
|
176
|
+
const prepared = await this.convertInternal(target, options);
|
|
177
177
|
this.viewer._emit("files:conversion:success", prepared);
|
|
178
178
|
return prepared;
|
|
179
179
|
} catch (e) {
|
|
@@ -183,11 +183,11 @@ var FilesModule = class {
|
|
|
183
183
|
});
|
|
184
184
|
}
|
|
185
185
|
// Convenience API: upload first, then convert in one call.
|
|
186
|
-
async prepare(file) {
|
|
186
|
+
async prepare(file, options = {}) {
|
|
187
187
|
const target = this.resolveFile(file);
|
|
188
188
|
return this.withOperation({ stage: "uploading", message: "Preparing file..." }, async () => {
|
|
189
189
|
await this.uploadInternal(target);
|
|
190
|
-
const prepared = await this.convertInternal(target);
|
|
190
|
+
const prepared = await this.convertInternal(target, options);
|
|
191
191
|
return prepared;
|
|
192
192
|
});
|
|
193
193
|
}
|
|
@@ -204,11 +204,11 @@ var FilesModule = class {
|
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
// Full pipeline: upload + convert + open iframe.
|
|
207
|
-
async render(file) {
|
|
207
|
+
async render(file, options = {}) {
|
|
208
208
|
const target = this.resolveFile(file);
|
|
209
209
|
return this.withOperation({ stage: "uploading", message: "Uploading + converting + opening..." }, async () => {
|
|
210
210
|
await this.upload(target);
|
|
211
|
-
const prepared = await this.convert(target);
|
|
211
|
+
const prepared = await this.convert(target, options);
|
|
212
212
|
this.updateState({ stage: "rendering", message: "Opening viewer..." });
|
|
213
213
|
this.open(prepared);
|
|
214
214
|
this.viewer._emit("files:load:success", prepared);
|
|
@@ -301,10 +301,11 @@ var FilesModule = class {
|
|
|
301
301
|
}
|
|
302
302
|
}
|
|
303
303
|
// Build StreamFile payload compatible with conversion service.
|
|
304
|
-
buildCachePayload(file, baseFileId) {
|
|
304
|
+
buildCachePayload(file, baseFileId, options = {}) {
|
|
305
305
|
const createdDate = (/* @__PURE__ */ new Date()).toISOString();
|
|
306
306
|
return {
|
|
307
307
|
filename: file.name,
|
|
308
|
+
...options.downloadUrl ? { downloadUrl: options.downloadUrl } : {},
|
|
308
309
|
baseFileId,
|
|
309
310
|
baseMajorRev: 0,
|
|
310
311
|
baseMinorRev: 0,
|
|
@@ -348,10 +349,10 @@ var FilesModule = class {
|
|
|
348
349
|
};
|
|
349
350
|
}
|
|
350
351
|
// Submit conversion/caching request and return service response.
|
|
351
|
-
async cacheFile(file, baseFileId) {
|
|
352
|
+
async cacheFile(file, baseFileId, options = {}) {
|
|
352
353
|
const hostConversion = await this.resolveHostConversion();
|
|
353
354
|
const url = `${hostConversion}/api/StreamFile?overwrite=true&ignore_line_weight=1`;
|
|
354
|
-
const payload = this.buildCachePayload(file, baseFileId);
|
|
355
|
+
const payload = this.buildCachePayload(file, baseFileId, options);
|
|
355
356
|
const response = await fetch(url, {
|
|
356
357
|
method: "POST",
|
|
357
358
|
headers: {
|
|
@@ -368,12 +369,12 @@ var FilesModule = class {
|
|
|
368
369
|
return await response.json();
|
|
369
370
|
}
|
|
370
371
|
// Convert file and generate final iframe URL with query string.
|
|
371
|
-
async convertInternal(file) {
|
|
372
|
+
async convertInternal(file, options = {}) {
|
|
372
373
|
var _a, _b, _c, _d;
|
|
373
374
|
this.updateState({ stage: "converting", message: "Converting file..." });
|
|
374
375
|
const uploadSession = this.getUploadSessionForFile(file) || this.createUploadSession(file);
|
|
375
376
|
const seedBaseFileId = uploadSession.baseFileId;
|
|
376
|
-
const cacheResult = await this.cacheFile(file, seedBaseFileId);
|
|
377
|
+
const cacheResult = await this.cacheFile(file, seedBaseFileId, options);
|
|
377
378
|
const baseFileId = (_a = cacheResult.baseFileId) != null ? _a : seedBaseFileId;
|
|
378
379
|
const baseMajorRev = (_b = cacheResult.baseMajorRev) != null ? _b : 0;
|
|
379
380
|
const baseMinorRev = (_c = cacheResult.baseMinorRev) != null ? _c : 0;
|
|
@@ -775,6 +776,22 @@ var MarkupModule = class {
|
|
|
775
776
|
}
|
|
776
777
|
};
|
|
777
778
|
|
|
779
|
+
// src/modules/language.module.ts
|
|
780
|
+
var LanguageModule = class {
|
|
781
|
+
constructor(viewer) {
|
|
782
|
+
this.viewer = viewer;
|
|
783
|
+
}
|
|
784
|
+
change(language) {
|
|
785
|
+
this.viewer.postToViewer("viewer-language-change" /* LANGUAGE_CHANGE */, {
|
|
786
|
+
language,
|
|
787
|
+
timestamp: Date.now()
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
set(language) {
|
|
791
|
+
this.change(language);
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
|
|
778
795
|
// src/viewer.ts
|
|
779
796
|
var Viewer3D = class {
|
|
780
797
|
constructor(options) {
|
|
@@ -937,6 +954,7 @@ var Viewer3D = class {
|
|
|
937
954
|
this.toolbar = new ToolbarModule(this);
|
|
938
955
|
this.modelTree = new ModelTreeModule(this);
|
|
939
956
|
this.markup = new MarkupModule(this);
|
|
957
|
+
this.language = new LanguageModule(this);
|
|
940
958
|
}
|
|
941
959
|
// ===== options helpers =====
|
|
942
960
|
getOptions() {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Viewer3D } from "../viewer";
|
|
2
|
+
export declare class CameraModule {
|
|
3
|
+
private viewer;
|
|
4
|
+
on: {
|
|
5
|
+
home: (cb: (payload: {
|
|
6
|
+
timestamp: number;
|
|
7
|
+
}) => void) => () => void;
|
|
8
|
+
};
|
|
9
|
+
constructor(viewer: Viewer3D);
|
|
10
|
+
zoomIn(percent: number): void;
|
|
11
|
+
zoomOut(percent: number): void;
|
|
12
|
+
home(): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ViewerMessageType } from "../contracts/messages";
|
|
2
|
+
export class CameraModule {
|
|
3
|
+
constructor(viewer) {
|
|
4
|
+
this.viewer = viewer;
|
|
5
|
+
this.on = {
|
|
6
|
+
home: (cb) => this.viewer._on("camera:home", cb),
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
zoomIn(percent) {
|
|
10
|
+
this.viewer.postToViewer(ViewerMessageType.ZOOM, { action: "in", percent });
|
|
11
|
+
}
|
|
12
|
+
zoomOut(percent) {
|
|
13
|
+
this.viewer.postToViewer(ViewerMessageType.ZOOM, { action: "out", percent });
|
|
14
|
+
}
|
|
15
|
+
home() {
|
|
16
|
+
this.viewer.postToViewer(ViewerMessageType.HOME, {});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { LoadStatePayload, PreparedViewerData } from "../contracts/events";
|
|
2
|
+
import { Viewer3D } from "../viewer";
|
|
3
|
+
export declare type FilesConfig = {
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
viewerPath?: string;
|
|
6
|
+
uploadPath?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare type ConvertOptions = {
|
|
9
|
+
downloadUrl?: string;
|
|
10
|
+
};
|
|
11
|
+
export declare class FilesModule {
|
|
12
|
+
private viewer;
|
|
13
|
+
on: {
|
|
14
|
+
state: (cb: (payload: LoadStatePayload) => void) => () => void;
|
|
15
|
+
uploadStart: (cb: (payload: {
|
|
16
|
+
fileName: string;
|
|
17
|
+
}) => void) => () => void;
|
|
18
|
+
uploadSuccess: (cb: (payload: {
|
|
19
|
+
fileName: string;
|
|
20
|
+
baseFileId: string;
|
|
21
|
+
}) => void) => () => void;
|
|
22
|
+
uploadError: (cb: (payload: {
|
|
23
|
+
fileName: string;
|
|
24
|
+
error: string;
|
|
25
|
+
}) => void) => () => void;
|
|
26
|
+
conversionStart: (cb: (payload: {
|
|
27
|
+
fileName: string;
|
|
28
|
+
}) => void) => () => void;
|
|
29
|
+
conversionSuccess: (cb: (payload: PreparedViewerData) => void) => () => void;
|
|
30
|
+
conversionError: (cb: (payload: {
|
|
31
|
+
fileName: string;
|
|
32
|
+
error: string;
|
|
33
|
+
}) => void) => () => void;
|
|
34
|
+
renderStart: (cb: (payload: {
|
|
35
|
+
url: string;
|
|
36
|
+
}) => void) => () => void;
|
|
37
|
+
renderSuccess: (cb: (payload: {
|
|
38
|
+
url: string;
|
|
39
|
+
}) => void) => () => void;
|
|
40
|
+
renderError: (cb: (payload: {
|
|
41
|
+
url?: string;
|
|
42
|
+
error: string;
|
|
43
|
+
}) => void) => () => void;
|
|
44
|
+
loadSuccess: (cb: (payload: PreparedViewerData) => void) => () => void;
|
|
45
|
+
loadError: (cb: (payload: {
|
|
46
|
+
error: string;
|
|
47
|
+
}) => void) => () => void;
|
|
48
|
+
};
|
|
49
|
+
private config;
|
|
50
|
+
private operationStartTime;
|
|
51
|
+
private state;
|
|
52
|
+
private lastUploadSession;
|
|
53
|
+
constructor(viewer: Viewer3D);
|
|
54
|
+
setConfig(next: FilesConfig): void;
|
|
55
|
+
getState(): LoadStatePayload;
|
|
56
|
+
upload(file?: File): Promise<{
|
|
57
|
+
fileName: string;
|
|
58
|
+
baseFileId: string;
|
|
59
|
+
}>;
|
|
60
|
+
convert(file?: File, options?: ConvertOptions): Promise<PreparedViewerData>;
|
|
61
|
+
prepare(file?: File, options?: ConvertOptions): Promise<PreparedViewerData>;
|
|
62
|
+
open(input: PreparedViewerData | {
|
|
63
|
+
url: string;
|
|
64
|
+
}): void;
|
|
65
|
+
render(file?: File, options?: ConvertOptions): Promise<PreparedViewerData>;
|
|
66
|
+
private resolveFile;
|
|
67
|
+
private normalizeBaseUrl;
|
|
68
|
+
private resolveBaseUrl;
|
|
69
|
+
private resolveViewerPath;
|
|
70
|
+
private resolveViewerOrigin;
|
|
71
|
+
private resolveHostConversion;
|
|
72
|
+
private getUploadPath;
|
|
73
|
+
private fileSignature;
|
|
74
|
+
private createBaseFileId;
|
|
75
|
+
private createUploadSession;
|
|
76
|
+
private getUploadSessionForFile;
|
|
77
|
+
private uploadInternal;
|
|
78
|
+
private buildCachePayload;
|
|
79
|
+
private cacheFile;
|
|
80
|
+
private convertInternal;
|
|
81
|
+
private updateState;
|
|
82
|
+
private withOperation;
|
|
83
|
+
private toErrorMessage;
|
|
84
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
const DEFAULT_API_BASE_URL = "https://dev.3dviewer.anybim.vn";
|
|
2
|
+
const DEFAULT_VIEWER_ORIGIN = "http://localhost:3000";
|
|
3
|
+
export class FilesModule {
|
|
4
|
+
constructor(viewer) {
|
|
5
|
+
this.viewer = viewer;
|
|
6
|
+
this.config = {};
|
|
7
|
+
this.operationStartTime = 0;
|
|
8
|
+
this.state = {
|
|
9
|
+
isLoading: false,
|
|
10
|
+
stage: "idle",
|
|
11
|
+
};
|
|
12
|
+
this.lastUploadSession = null;
|
|
13
|
+
// Bind external event helpers to internal typed emitter events.
|
|
14
|
+
this.on = {
|
|
15
|
+
state: (cb) => this.viewer._on("files:state", cb),
|
|
16
|
+
uploadStart: (cb) => this.viewer._on("files:upload:start", cb),
|
|
17
|
+
uploadSuccess: (cb) => this.viewer._on("files:upload:success", cb),
|
|
18
|
+
uploadError: (cb) => this.viewer._on("files:upload:error", cb),
|
|
19
|
+
conversionStart: (cb) => this.viewer._on("files:conversion:start", cb),
|
|
20
|
+
conversionSuccess: (cb) => this.viewer._on("files:conversion:success", cb),
|
|
21
|
+
conversionError: (cb) => this.viewer._on("files:conversion:error", cb),
|
|
22
|
+
renderStart: (cb) => this.viewer._on("files:render:start", cb),
|
|
23
|
+
renderSuccess: (cb) => this.viewer._on("files:render:success", cb),
|
|
24
|
+
renderError: (cb) => this.viewer._on("files:render:error", cb),
|
|
25
|
+
loadSuccess: (cb) => this.viewer._on("files:load:success", cb),
|
|
26
|
+
loadError: (cb) => this.viewer._on("files:load:error", cb),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// Merge file-pipeline runtime config.
|
|
30
|
+
setConfig(next) {
|
|
31
|
+
this.config = { ...this.config, ...next };
|
|
32
|
+
}
|
|
33
|
+
// Return a snapshot of current loading state.
|
|
34
|
+
getState() {
|
|
35
|
+
return { ...this.state };
|
|
36
|
+
}
|
|
37
|
+
// ---------- public pipeline ----------
|
|
38
|
+
// Upload file to conversion server and keep generated baseFileId in session.
|
|
39
|
+
async upload(file) {
|
|
40
|
+
const target = this.resolveFile(file);
|
|
41
|
+
return this.withOperation({ stage: "uploading", message: "Uploading file..." }, async () => {
|
|
42
|
+
var _a;
|
|
43
|
+
this.viewer._emit("files:upload:start", { fileName: target.name });
|
|
44
|
+
await this.uploadInternal(target);
|
|
45
|
+
const baseFileId = ((_a = this.getUploadSessionForFile(target)) === null || _a === void 0 ? void 0 : _a.baseFileId) || "";
|
|
46
|
+
this.viewer._emit("files:upload:success", { fileName: target.name, baseFileId });
|
|
47
|
+
return { fileName: target.name, baseFileId };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
// Trigger conversion flow and resolve final viewer metadata.
|
|
51
|
+
async convert(file, options = {}) {
|
|
52
|
+
const target = this.resolveFile(file);
|
|
53
|
+
return this.withOperation({ stage: "converting", message: "Converting file..." }, async () => {
|
|
54
|
+
this.viewer._emit("files:conversion:start", { fileName: target.name });
|
|
55
|
+
try {
|
|
56
|
+
const prepared = await this.convertInternal(target, options);
|
|
57
|
+
this.viewer._emit("files:conversion:success", prepared);
|
|
58
|
+
return prepared;
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
this.viewer._emit("files:conversion:error", { fileName: target.name, error: this.toErrorMessage(e) });
|
|
62
|
+
throw e;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
// Convenience API: upload first, then convert in one call.
|
|
67
|
+
async prepare(file, options = {}) {
|
|
68
|
+
const target = this.resolveFile(file);
|
|
69
|
+
return this.withOperation({ stage: "uploading", message: "Preparing file..." }, async () => {
|
|
70
|
+
await this.uploadInternal(target);
|
|
71
|
+
const prepared = await this.convertInternal(target, options);
|
|
72
|
+
return prepared;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// Open iframe with an already prepared viewer URL.
|
|
76
|
+
open(input) {
|
|
77
|
+
const url = input.url;
|
|
78
|
+
this.viewer._emit("files:render:start", { url });
|
|
79
|
+
try {
|
|
80
|
+
this.viewer.open(url);
|
|
81
|
+
this.viewer._emit("files:render:success", { url });
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
this.viewer._emit("files:render:error", { url, error: this.toErrorMessage(e) });
|
|
85
|
+
throw e;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Full pipeline: upload + convert + open iframe.
|
|
89
|
+
async render(file, options = {}) {
|
|
90
|
+
const target = this.resolveFile(file);
|
|
91
|
+
return this.withOperation({ stage: "uploading", message: "Uploading + converting + opening..." }, async () => {
|
|
92
|
+
await this.upload(target);
|
|
93
|
+
const prepared = await this.convert(target, options);
|
|
94
|
+
// open viewer
|
|
95
|
+
this.updateState({ stage: "rendering", message: "Opening viewer..." });
|
|
96
|
+
this.open(prepared);
|
|
97
|
+
this.viewer._emit("files:load:success", prepared);
|
|
98
|
+
return prepared;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// Resolve file argument, fallback to options.file, and persist it back.
|
|
102
|
+
resolveFile(file) {
|
|
103
|
+
const optFile = this.viewer.getOptions().file;
|
|
104
|
+
const target = file || optFile;
|
|
105
|
+
if (!target)
|
|
106
|
+
throw new Error("No file provided. Pass a File or set options.file");
|
|
107
|
+
// store back to viewer options (optional)
|
|
108
|
+
this.viewer.patchOptions({ file: target });
|
|
109
|
+
return target;
|
|
110
|
+
}
|
|
111
|
+
// Trim input URL and remove trailing slash.
|
|
112
|
+
normalizeBaseUrl(input) {
|
|
113
|
+
return input.trim().replace(/\/+$/, "");
|
|
114
|
+
}
|
|
115
|
+
// Resolve API base URL with default fallback.
|
|
116
|
+
resolveBaseUrl() {
|
|
117
|
+
const raw = this.config.baseUrl || this.viewer.getOptions().baseUrl || DEFAULT_API_BASE_URL;
|
|
118
|
+
return this.normalizeBaseUrl(raw);
|
|
119
|
+
}
|
|
120
|
+
// Resolve viewer route path (e.g. /mainviewer).
|
|
121
|
+
resolveViewerPath() {
|
|
122
|
+
const p = (this.config.viewerPath || this.viewer.getOptions().viewerPath || "/mainviewer").trim();
|
|
123
|
+
if (!p)
|
|
124
|
+
return "/mainviewer";
|
|
125
|
+
return p.startsWith("/") ? p : `/${p}`;
|
|
126
|
+
}
|
|
127
|
+
// Viewer host used to open iframe after conversion completes.
|
|
128
|
+
resolveViewerOrigin() {
|
|
129
|
+
return this.normalizeBaseUrl(DEFAULT_VIEWER_ORIGIN);
|
|
130
|
+
}
|
|
131
|
+
// Build conversion service root from API base URL.
|
|
132
|
+
resolveHostConversion() {
|
|
133
|
+
const baseUrl = this.resolveBaseUrl();
|
|
134
|
+
return baseUrl.endsWith("/service/conversion") ? baseUrl : `${baseUrl}/service/conversion`;
|
|
135
|
+
}
|
|
136
|
+
// Resolve upload path sent to conversion APIs.
|
|
137
|
+
getUploadPath() {
|
|
138
|
+
return this.config.uploadPath || this.viewer.getOptions().uploadPath || ".";
|
|
139
|
+
}
|
|
140
|
+
// Build a stable in-memory signature to identify same file uploads.
|
|
141
|
+
fileSignature(file) {
|
|
142
|
+
return `${file.name}::${file.size}::${file.lastModified}`;
|
|
143
|
+
}
|
|
144
|
+
// Create a UUID-like baseFileId when caller does not provide one.
|
|
145
|
+
createBaseFileId() {
|
|
146
|
+
if (typeof crypto !== "undefined" && "randomUUID" in crypto)
|
|
147
|
+
return crypto.randomUUID();
|
|
148
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
149
|
+
const r = Math.floor(Math.random() * 16);
|
|
150
|
+
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
|
151
|
+
return v.toString(16);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// Create upload session metadata reused between upload and convert.
|
|
155
|
+
createUploadSession(file) {
|
|
156
|
+
return {
|
|
157
|
+
signature: this.fileSignature(file),
|
|
158
|
+
baseFileId: this.createBaseFileId(),
|
|
159
|
+
fileName: file.name,
|
|
160
|
+
uploadPath: this.getUploadPath(),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// Return previous upload session for same file and upload path.
|
|
164
|
+
getUploadSessionForFile(file) {
|
|
165
|
+
if (!this.lastUploadSession)
|
|
166
|
+
return null;
|
|
167
|
+
const sameFile = this.lastUploadSession.signature === this.fileSignature(file);
|
|
168
|
+
const samePath = this.lastUploadSession.uploadPath === this.getUploadPath();
|
|
169
|
+
return sameFile && samePath ? this.lastUploadSession : null;
|
|
170
|
+
}
|
|
171
|
+
// Call upload endpoint and persist upload session on success.
|
|
172
|
+
async uploadInternal(file) {
|
|
173
|
+
this.updateState({ stage: "uploading", message: "Uploading file..." });
|
|
174
|
+
try {
|
|
175
|
+
const existing = this.getUploadSessionForFile(file);
|
|
176
|
+
const session = existing || this.createUploadSession(file);
|
|
177
|
+
const hostConversion = this.resolveHostConversion();
|
|
178
|
+
const path = this.getUploadPath();
|
|
179
|
+
// upload endpoint
|
|
180
|
+
const url = `${hostConversion}/api/File/upload?path=${encodeURIComponent(path)}`;
|
|
181
|
+
const formData = new FormData();
|
|
182
|
+
formData.append("file", file, file.name);
|
|
183
|
+
const res = await fetch(url, { method: "POST", body: formData, headers: { Accept: "text/plain" } });
|
|
184
|
+
if (!res.ok)
|
|
185
|
+
throw new Error(`Upload failed (${res.status} ${res.statusText})`);
|
|
186
|
+
this.lastUploadSession = session;
|
|
187
|
+
}
|
|
188
|
+
catch (e) {
|
|
189
|
+
const msg = this.toErrorMessage(e);
|
|
190
|
+
this.viewer._emit("files:upload:error", { fileName: file.name, error: msg });
|
|
191
|
+
throw e;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Build StreamFile payload compatible with conversion service.
|
|
195
|
+
buildCachePayload(file, baseFileId, options = {}) {
|
|
196
|
+
const createdDate = new Date().toISOString();
|
|
197
|
+
return {
|
|
198
|
+
filename: file.name,
|
|
199
|
+
...(options.downloadUrl ? { downloadUrl: options.downloadUrl } : {}),
|
|
200
|
+
baseFileId,
|
|
201
|
+
baseMajorRev: 0,
|
|
202
|
+
baseMinorRev: 0,
|
|
203
|
+
isChecked: false,
|
|
204
|
+
status: { size: file.size },
|
|
205
|
+
child: [],
|
|
206
|
+
isDirectory: false,
|
|
207
|
+
createdDate,
|
|
208
|
+
cacheStatus: 0,
|
|
209
|
+
modelFileId: "",
|
|
210
|
+
id: "",
|
|
211
|
+
originalFilePath: this.getUploadPath(),
|
|
212
|
+
streamLocation: null,
|
|
213
|
+
converter: "Hoops",
|
|
214
|
+
originalSize: 0,
|
|
215
|
+
cacheSize: 0,
|
|
216
|
+
importTime: 0,
|
|
217
|
+
importAssemblyTreeTime: 0,
|
|
218
|
+
creator: {
|
|
219
|
+
id: "00000000-0000-0000-0000-000000000000",
|
|
220
|
+
name: "Anonymous",
|
|
221
|
+
},
|
|
222
|
+
originalFile: file.name,
|
|
223
|
+
multiStream: false,
|
|
224
|
+
isRootModel: 0,
|
|
225
|
+
extraConvertOutput: "",
|
|
226
|
+
cacheFilename: null,
|
|
227
|
+
errorMassage: null,
|
|
228
|
+
convertOptions: {
|
|
229
|
+
convert3DModel: 1,
|
|
230
|
+
convert2DSheet: 1,
|
|
231
|
+
extractProperties: 1,
|
|
232
|
+
childModels: 0,
|
|
233
|
+
},
|
|
234
|
+
drawingConvertStatus: {
|
|
235
|
+
convert3DModel: 5,
|
|
236
|
+
convert2DSheet: 5,
|
|
237
|
+
extractProperties: 5,
|
|
238
|
+
},
|
|
239
|
+
attemptedConvertTimes: 0,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// Submit conversion/caching request and return service response.
|
|
243
|
+
async cacheFile(file, baseFileId, options = {}) {
|
|
244
|
+
const hostConversion = await this.resolveHostConversion();
|
|
245
|
+
const url = `${hostConversion}/api/StreamFile?overwrite=true&ignore_line_weight=1`;
|
|
246
|
+
const payload = this.buildCachePayload(file, baseFileId, options);
|
|
247
|
+
const response = await fetch(url, {
|
|
248
|
+
method: "POST",
|
|
249
|
+
headers: {
|
|
250
|
+
"Content-Type": "application/json",
|
|
251
|
+
Accept: "application/json",
|
|
252
|
+
},
|
|
253
|
+
body: JSON.stringify(payload),
|
|
254
|
+
});
|
|
255
|
+
if (!response.ok) {
|
|
256
|
+
throw new Error(`Cache/convert failed (${response.status} ${response.statusText})`);
|
|
257
|
+
}
|
|
258
|
+
return (await response.json());
|
|
259
|
+
}
|
|
260
|
+
// Convert file and generate final iframe URL with query string.
|
|
261
|
+
async convertInternal(file, options = {}) {
|
|
262
|
+
var _a, _b, _c, _d;
|
|
263
|
+
this.updateState({ stage: "converting", message: "Converting file..." });
|
|
264
|
+
const uploadSession = this.getUploadSessionForFile(file) || this.createUploadSession(file);
|
|
265
|
+
const seedBaseFileId = uploadSession.baseFileId;
|
|
266
|
+
// 1) request cache/convert
|
|
267
|
+
const cacheResult = await this.cacheFile(file, seedBaseFileId, options);
|
|
268
|
+
const baseFileId = (_a = cacheResult.baseFileId) !== null && _a !== void 0 ? _a : seedBaseFileId;
|
|
269
|
+
const baseMajorRev = (_b = cacheResult.baseMajorRev) !== null && _b !== void 0 ? _b : 0;
|
|
270
|
+
const baseMinorRev = (_c = cacheResult.baseMinorRev) !== null && _c !== void 0 ? _c : 0;
|
|
271
|
+
const fileName = cacheResult.filename || file.name;
|
|
272
|
+
const cacheListItem = { baseFileId, baseMajorRev, baseMinorRev, fileName };
|
|
273
|
+
// Single-shot mode: one conversion request only, no polling retry.
|
|
274
|
+
if (cacheResult.cacheStatus !== 2) {
|
|
275
|
+
throw new Error(`Conversion not ready after first request (cacheStatus=${(_d = cacheResult.cacheStatus) !== null && _d !== void 0 ? _d : "unknown"})`);
|
|
276
|
+
}
|
|
277
|
+
// 3) build viewer url
|
|
278
|
+
const query = new URLSearchParams({ fileList: JSON.stringify([cacheListItem]) }).toString();
|
|
279
|
+
const viewerBase = this.resolveViewerOrigin();
|
|
280
|
+
const viewerPath = this.resolveViewerPath();
|
|
281
|
+
const url = `${viewerBase}${viewerPath}?${query}`;
|
|
282
|
+
return { baseFileId, baseMajorRev, baseMinorRev, fileName, query, url };
|
|
283
|
+
}
|
|
284
|
+
// Update internal loading state and emit state event.
|
|
285
|
+
updateState(next) {
|
|
286
|
+
const elapsedMs = this.operationStartTime > 0 ? Date.now() - this.operationStartTime : 0;
|
|
287
|
+
this.state = { ...this.state, ...next, elapsedMs };
|
|
288
|
+
this.viewer._emit("files:state", this.state);
|
|
289
|
+
}
|
|
290
|
+
// Shared wrapper to handle loading state lifecycle and top-level errors.
|
|
291
|
+
async withOperation(initial, run) {
|
|
292
|
+
// shared operation wrapper for loading state and errors
|
|
293
|
+
this.operationStartTime = Date.now();
|
|
294
|
+
this.updateState({
|
|
295
|
+
isLoading: true,
|
|
296
|
+
stage: initial.stage,
|
|
297
|
+
message: initial.message,
|
|
298
|
+
});
|
|
299
|
+
try {
|
|
300
|
+
const result = await run();
|
|
301
|
+
this.updateState({ isLoading: false, stage: "completed", message: "Completed" });
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
catch (e) {
|
|
305
|
+
const msg = this.toErrorMessage(e);
|
|
306
|
+
this.updateState({ isLoading: false, stage: "error", message: msg });
|
|
307
|
+
this.viewer._emit("files:load:error", { error: msg });
|
|
308
|
+
throw e;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// Normalize unknown error shape into displayable message.
|
|
312
|
+
toErrorMessage(e) {
|
|
313
|
+
return e instanceof Error ? e.message : String(e);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Viewer3D } from "../viewer";
|
|
2
|
+
export declare class InteractionModule {
|
|
3
|
+
private viewer;
|
|
4
|
+
on: {
|
|
5
|
+
panChange: (cb: (payload: {
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
}) => void) => () => void;
|
|
8
|
+
};
|
|
9
|
+
constructor(viewer: Viewer3D);
|
|
10
|
+
enablePan(): void;
|
|
11
|
+
disablePan(): void;
|
|
12
|
+
select(): void;
|
|
13
|
+
areaSelect(): void;
|
|
14
|
+
orbit(): void;
|
|
15
|
+
rotateZ(): void;
|
|
16
|
+
walkThrough(): void;
|
|
17
|
+
zoomWindow(): void;
|
|
18
|
+
zoomFit(): void;
|
|
19
|
+
drawModeShaded(): void;
|
|
20
|
+
drawModeWireframe(): void;
|
|
21
|
+
drawModeHiddenLine(): void;
|
|
22
|
+
drawModeShadedWire(): void;
|
|
23
|
+
drawModeXRay(): void;
|
|
24
|
+
drawModeGhosting(): void;
|
|
25
|
+
explode(magnitude: number): void;
|
|
26
|
+
explodeOff(): void;
|
|
27
|
+
private setDrawMode;
|
|
28
|
+
}
|