@shapediver/viewer.shared.services 2.10.1-rc.0 → 2.12.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/converter/Converter.d.ts +15 -7
- package/dist/converter/Converter.d.ts.map +1 -1
- package/dist/converter/Converter.js +64 -55
- package/dist/converter/Converter.js.map +1 -1
- package/dist/dom-event-engine/DomEventEngine.js +9 -5
- package/dist/dom-event-engine/DomEventEngine.js.map +1 -1
- package/dist/dom-event-engine/IDomEventListener.js +2 -1
- package/dist/event-engine/EventEngine.js +16 -12
- package/dist/event-engine/EventEngine.js.map +1 -1
- package/dist/event-engine/EventTypes.d.ts +7 -2
- package/dist/event-engine/EventTypes.d.ts.map +1 -1
- package/dist/event-engine/EventTypes.js +24 -15
- package/dist/event-engine/EventTypes.js.map +1 -1
- package/dist/event-engine/interfaces/ICallback.js +2 -1
- package/dist/event-engine/interfaces/IEvent.js +2 -1
- package/dist/event-engine/interfaces/IEvent.js.map +1 -1
- package/dist/event-engine/interfaces/IListener.js +2 -1
- package/dist/http-client/HttpClient.d.ts +78 -2
- package/dist/http-client/HttpClient.d.ts.map +1 -1
- package/dist/http-client/HttpClient.js +177 -20
- package/dist/http-client/HttpClient.js.map +1 -1
- package/dist/http-client/HttpResponse.d.ts +1 -0
- package/dist/http-client/HttpResponse.d.ts.map +1 -1
- package/dist/http-client/HttpResponse.js +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +81 -32
- package/dist/index.js.map +1 -1
- package/dist/input-validator/InputValidator.js +16 -9
- package/dist/input-validator/InputValidator.js.map +1 -1
- package/dist/logger/ErrorTypeGuards.js +55 -35
- package/dist/logger/ErrorTypeGuards.js.map +1 -1
- package/dist/logger/Logger.js +7 -3
- package/dist/logger/Logger.js.map +1 -1
- package/dist/logger/ShapeDiverBackendErrors.js +13 -7
- package/dist/logger/ShapeDiverBackendErrors.js.map +1 -1
- package/dist/logger/ShapeDiverError.js +9 -4
- package/dist/logger/ShapeDiverError.js.map +1 -1
- package/dist/logger/ShapeDiverViewerErrors.js +40 -25
- package/dist/logger/ShapeDiverViewerErrors.js.map +1 -1
- package/dist/performance-evaluator/PerformanceEvaluator.d.ts +13 -0
- package/dist/performance-evaluator/PerformanceEvaluator.d.ts.map +1 -1
- package/dist/performance-evaluator/PerformanceEvaluator.js +39 -3
- package/dist/performance-evaluator/PerformanceEvaluator.js.map +1 -1
- package/dist/settings-engine/SettingsEngine.js +20 -16
- package/dist/settings-engine/SettingsEngine.js.map +1 -1
- package/dist/state-engine/StateEngine.d.ts +1 -0
- package/dist/state-engine/StateEngine.d.ts.map +1 -1
- package/dist/state-engine/StateEngine.js +8 -4
- package/dist/state-engine/StateEngine.js.map +1 -1
- package/dist/state-engine/StatePromise.js +5 -1
- package/dist/state-engine/StatePromise.js.map +1 -1
- package/dist/system-info/SystemInfo.js +10 -3
- package/dist/system-info/SystemInfo.js.map +1 -1
- package/dist/type-check/TypeChecker.js +5 -1
- package/dist/type-check/TypeChecker.js.map +1 -1
- package/dist/uuid-generator/UuidGenerator.js +10 -6
- package/dist/uuid-generator/UuidGenerator.js.map +1 -1
- package/package.json +4 -4
- package/src/converter/Converter.ts +69 -64
- package/src/event-engine/EventTypes.ts +8 -2
- package/src/http-client/HttpClient.ts +204 -26
- package/src/http-client/HttpResponse.ts +3 -1
- package/src/index.ts +2 -2
- package/src/performance-evaluator/PerformanceEvaluator.ts +33 -3
- package/src/settings-engine/SettingsEngine.ts +1 -1
- package/src/state-engine/StateEngine.ts +2 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { vec3
|
|
2
|
-
import { TinyColor } from '@ctrl/tinycolor'
|
|
1
|
+
import { vec3 } from 'gl-matrix';
|
|
2
|
+
import { ColorInput, TinyColor } from '@ctrl/tinycolor';
|
|
3
3
|
import { HttpClient } from '../http-client/HttpClient';
|
|
4
4
|
import { HttpResponse } from '../http-client/HttpResponse';
|
|
5
5
|
|
|
@@ -29,22 +29,22 @@ export class Converter {
|
|
|
29
29
|
reader.onerror = reject;
|
|
30
30
|
reader.readAsDataURL(blob);
|
|
31
31
|
});
|
|
32
|
-
data = data.replace('data:image/svg+xml;base64,', '')
|
|
32
|
+
data = data.replace('data:image/svg+xml;base64,', '');
|
|
33
33
|
data = atob(data);
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
const svgC = document.createElement('DIV');
|
|
36
36
|
svgC.id = 'svgc';
|
|
37
37
|
svgC.innerHTML = <string>data;
|
|
38
38
|
|
|
39
39
|
// now we can access the svg element as a DOM object
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const svgE = svgC.getElementsByTagName('svg');
|
|
41
|
+
const childImageURIs: string[] = [];
|
|
42
42
|
let styleURIs: string[] = [];
|
|
43
43
|
|
|
44
44
|
// collect image urls
|
|
45
45
|
for (let i = 0; i < svgE.length; ++i) {
|
|
46
46
|
for (let j = 0; j < 2; ++j) {
|
|
47
|
-
|
|
47
|
+
const childImages = <HTMLCollectionOf<SVGImageElement>>svgE[i].getElementsByTagName(['image', 'img'][j]);
|
|
48
48
|
for (let k = 0; k < childImages.length; ++k) {
|
|
49
49
|
if (childImages[k].href.baseVal.substring(0, 5) != 'data:') {
|
|
50
50
|
childImageURIs.push(childImages[k].href.baseVal);
|
|
@@ -54,9 +54,9 @@ export class Converter {
|
|
|
54
54
|
// collect potential font definitions
|
|
55
55
|
// we assume styles are imported using the following syntax:
|
|
56
56
|
// @import url(CSS_URL);
|
|
57
|
-
|
|
57
|
+
const styleElements = <HTMLCollectionOf<HTMLStyleElement>>svgE[i].getElementsByTagName('style');
|
|
58
58
|
for (let j = 0; j < styleElements.length; ++j) {
|
|
59
|
-
|
|
59
|
+
const regex = /@import\x20url\(\s*(.*?)\s*\);/g;
|
|
60
60
|
let m;
|
|
61
61
|
while ((m = regex.exec(styleElements[j].innerHTML)) !== null) {
|
|
62
62
|
styleURIs.push(m[1]);
|
|
@@ -71,17 +71,18 @@ export class Converter {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// creating a promise for each image which needs to be converted to a data URI
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
const replacementPromises = [];
|
|
75
|
+
const createImagePromise = async (uri: string) => {
|
|
76
76
|
if (uri.length > 0) {
|
|
77
|
-
const response = await this._httpClient.
|
|
78
|
-
|
|
79
|
-
let
|
|
77
|
+
const response = await this._httpClient.get(uri, undefined, true) as HttpResponse<ArrayBuffer>;
|
|
78
|
+
const uInt8Array = new Uint8Array(response.data as ArrayBuffer);
|
|
79
|
+
let i = uInt8Array.length;
|
|
80
|
+
const biStr = []; //new Array(i);
|
|
80
81
|
while (i--)
|
|
81
82
|
biStr[i] = String.fromCharCode(uInt8Array[i]);
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
const base64Data = window.btoa(biStr.join(''));
|
|
85
|
+
const imgDataUrl = 'data:' + response.headers['content-type'] + ';base64,' + base64Data;
|
|
85
86
|
|
|
86
87
|
// replace url in SVG string
|
|
87
88
|
// CAUTION theoretically this could cause unwanted replacements
|
|
@@ -93,34 +94,35 @@ export class Converter {
|
|
|
93
94
|
replacementPromises.push(createImagePromise(childImageURIs[i]));
|
|
94
95
|
|
|
95
96
|
// now we create promises for the google fonts to be imported
|
|
96
|
-
|
|
97
|
+
const createStylePromise = async (styleUrl: string) => {
|
|
97
98
|
const response = await this._httpClient.get(
|
|
98
99
|
styleUrl,
|
|
99
100
|
{ responseType: 'text' }
|
|
100
|
-
)
|
|
101
|
-
let cssString = response.data;
|
|
101
|
+
) as HttpResponse<ArrayBuffer>;
|
|
102
|
+
let cssString = response.data as unknown as string;
|
|
102
103
|
// we assume that fonts are imported using the following syntax:
|
|
103
104
|
// url(FONT_URI);
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
const fontURLs = [];
|
|
106
|
+
const regex = /url\(\s*(.*?)\s*\)/g;
|
|
106
107
|
let m;
|
|
107
108
|
while ((m = regex.exec(cssString)) !== null) {
|
|
108
109
|
fontURLs.push(m[1]);
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
const fontPromises = [];
|
|
113
|
+
const createFontPromise = async (fUrl: string) => {
|
|
113
114
|
const response = await this._httpClient.get(
|
|
114
115
|
fUrl,
|
|
115
116
|
{ responseType: 'arraybuffer' }
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
let
|
|
117
|
+
) as HttpResponse<ArrayBuffer>;
|
|
118
|
+
const uInt8Array = new Uint8Array(response.data as ArrayBuffer);
|
|
119
|
+
let i = uInt8Array.length;
|
|
120
|
+
const biStr = []; //new Array(i);
|
|
119
121
|
while (i--)
|
|
120
122
|
biStr[i] = String.fromCharCode(uInt8Array[i]);
|
|
121
123
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
const base64Data = window.btoa(biStr.join(''));
|
|
125
|
+
const fontDataUrl = 'data:' + response.headers['content-type'] + ';base64,' + base64Data;
|
|
124
126
|
if (fUrl.length > 0)
|
|
125
127
|
cssString = cssString.replace(fUrl, fontDataUrl);
|
|
126
128
|
};
|
|
@@ -137,21 +139,22 @@ export class Converter {
|
|
|
137
139
|
|
|
138
140
|
await Promise.all(replacementPromises);
|
|
139
141
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
+
const du = 'data:image/svg+xml,' + encodeURIComponent(data);
|
|
143
|
+
const img = new Image(); // same as document.createElement('img')
|
|
142
144
|
img.crossOrigin = 'Anonymous';
|
|
143
145
|
const promise = new Promise<void>((resolve, reject) => {
|
|
144
146
|
img.onload = () => resolve();
|
|
145
147
|
img.onerror = reject;
|
|
146
|
-
})
|
|
148
|
+
});
|
|
147
149
|
img.src = du;
|
|
148
150
|
await promise;
|
|
149
151
|
return img;
|
|
150
152
|
}
|
|
151
153
|
|
|
152
|
-
public async responseToImage(response: HttpResponse<ArrayBuffer
|
|
153
|
-
|
|
154
|
-
|
|
154
|
+
public async responseToImage(response: HttpResponse<ArrayBuffer | HTMLImageElement>, blob: Blob): Promise<HTMLImageElement> {
|
|
155
|
+
// if we already receive and image, this conversion already happened
|
|
156
|
+
if(response.data instanceof HTMLImageElement) return response.data;
|
|
157
|
+
|
|
155
158
|
if (response.headers['content-type'] === 'image/svg+xml') {
|
|
156
159
|
const img = await this.processSVG(blob);
|
|
157
160
|
return img;
|
|
@@ -160,8 +163,8 @@ export class Converter {
|
|
|
160
163
|
const promise = new Promise<void>((resolve, reject) => {
|
|
161
164
|
img.onload = () => resolve();
|
|
162
165
|
img.onerror = reject;
|
|
163
|
-
})
|
|
164
|
-
img.crossOrigin =
|
|
166
|
+
});
|
|
167
|
+
img.crossOrigin = 'anonymous';
|
|
165
168
|
img.src = URL.createObjectURL(blob);
|
|
166
169
|
await promise;
|
|
167
170
|
URL.revokeObjectURL(img.src);
|
|
@@ -169,17 +172,17 @@ export class Converter {
|
|
|
169
172
|
}
|
|
170
173
|
}
|
|
171
174
|
|
|
172
|
-
public toAlpha(color:
|
|
175
|
+
public toAlpha(color: unknown): number {
|
|
173
176
|
const c = this.toHexColor(color);
|
|
174
177
|
if (c.length <= 8) return 1;
|
|
175
178
|
return parseInt(c.slice(c.length - 2, c.length), 16) / 255;
|
|
176
179
|
}
|
|
177
180
|
|
|
178
|
-
public toColorArray(color:
|
|
179
|
-
if(typeof color !== 'string' || !color.startsWith(
|
|
181
|
+
public toColorArray(color: unknown): number[] {
|
|
182
|
+
if(typeof color !== 'string' || !color.startsWith('#'))
|
|
180
183
|
color = this.toHexColor(color);
|
|
181
|
-
const tColor = new TinyColor(color);
|
|
182
|
-
const rgb = tColor.toRgb()
|
|
184
|
+
const tColor = new TinyColor(color as ColorInput | undefined);
|
|
185
|
+
const rgb = tColor.toRgb();
|
|
183
186
|
return [rgb.r / 255.0, rgb.g / 255.0, rgb.b / 255.0];
|
|
184
187
|
}
|
|
185
188
|
|
|
@@ -187,7 +190,7 @@ export class Converter {
|
|
|
187
190
|
* @param color
|
|
188
191
|
* @param defColor
|
|
189
192
|
*/
|
|
190
|
-
public toHex8Color(color:
|
|
193
|
+
public toHex8Color(color: unknown, defColorString: string = '#000'): string {
|
|
191
194
|
const c = this.toHexColor(color, defColorString);
|
|
192
195
|
const tColor = new TinyColor(c);
|
|
193
196
|
const cH8 = tColor.toHex8String();
|
|
@@ -201,13 +204,13 @@ export class Converter {
|
|
|
201
204
|
* @param color
|
|
202
205
|
* @param defColor
|
|
203
206
|
*/
|
|
204
|
-
public toHexColor(color:
|
|
207
|
+
public toHexColor(color: unknown, defColorString: string = '#000'): string {
|
|
205
208
|
if (!color || color === 'default') return defColorString;
|
|
206
209
|
|
|
207
|
-
if (color.constructor === Float32Array)
|
|
208
|
-
color = Array.from(color);
|
|
210
|
+
if ((color as Float32Array).constructor === Float32Array)
|
|
211
|
+
color = Array.from(color as Float32Array);
|
|
209
212
|
|
|
210
|
-
const tColor = new TinyColor(color);
|
|
213
|
+
const tColor = new TinyColor(color as TinyColor | undefined);
|
|
211
214
|
|
|
212
215
|
if (color instanceof TinyColor)
|
|
213
216
|
return this.tinyColorToString(tColor);
|
|
@@ -215,17 +218,17 @@ export class Converter {
|
|
|
215
218
|
// check if we got a number
|
|
216
219
|
if (typeof color === 'number') {
|
|
217
220
|
let cs = color.toString(16);
|
|
218
|
-
|
|
221
|
+
const cl = cs.length;
|
|
219
222
|
if (cl < 3) cs = cs.padStart(3, '0');
|
|
220
223
|
else if (cl < 6) cs = cs.padStart(6, '0');
|
|
221
224
|
else if (cl < 8) cs = cs.padEnd(8, '0');
|
|
222
|
-
|
|
225
|
+
const tc = new TinyColor(cs);
|
|
223
226
|
return tc.isValid ? this.tinyColorToString(tc) : defColorString;
|
|
224
227
|
}
|
|
225
228
|
|
|
226
229
|
// check if the input is a THREE.Color
|
|
227
|
-
if (color.isColor && typeof color.getHexString == 'function') {
|
|
228
|
-
|
|
230
|
+
if ((color as THREE.Color).isColor && typeof (color as THREE.Color).getHexString == 'function') {
|
|
231
|
+
const tc = new TinyColor((color as THREE.Color).getHexString());
|
|
229
232
|
return tc.isValid ? this.tinyColorToString(tc) : defColorString;
|
|
230
233
|
}
|
|
231
234
|
|
|
@@ -241,13 +244,13 @@ export class Converter {
|
|
|
241
244
|
if (!isRGBArray)
|
|
242
245
|
return defColorString;
|
|
243
246
|
|
|
244
|
-
|
|
247
|
+
const tc = new TinyColor({
|
|
245
248
|
r: Math.max(0, Math.min(color[0], 255)),
|
|
246
249
|
g: Math.max(0, Math.min(color[1], 255)),
|
|
247
250
|
b: Math.max(0, Math.min(color[2], 255))
|
|
248
251
|
});
|
|
249
252
|
if (color.length == 4) {
|
|
250
|
-
|
|
253
|
+
const a = parseFloat(color[3]);
|
|
251
254
|
if (!isNaN(a)) {
|
|
252
255
|
tc.setAlpha(Math.max(0, Math.min(a, 255)) / 255);
|
|
253
256
|
}
|
|
@@ -258,50 +261,52 @@ export class Converter {
|
|
|
258
261
|
// if we got something other than a string, check if
|
|
259
262
|
// tinycolor can work with it
|
|
260
263
|
if (typeof color !== 'string') {
|
|
261
|
-
|
|
264
|
+
const tc = new TinyColor(color as string);
|
|
262
265
|
return tc.isValid ? this.tinyColorToString(tc) : defColorString;
|
|
263
266
|
}
|
|
264
267
|
|
|
265
268
|
// tinycolor doesn't like 0x
|
|
266
|
-
|
|
269
|
+
const tmpColor = color.replace('0x', '#');
|
|
267
270
|
|
|
268
271
|
// if we got no alpha value, add full opacity
|
|
269
272
|
if (tmpColor.match(/^#[a-f0-9]{6}$/i) !== null) {
|
|
270
|
-
|
|
273
|
+
const tc = new TinyColor(tmpColor + 'ff');
|
|
271
274
|
return tc.isValid ? this.tinyColorToString(tc) : defColorString;
|
|
272
275
|
}
|
|
273
276
|
|
|
274
277
|
// standard case
|
|
275
278
|
if (tmpColor.match(/^#[a-f0-9]{8}$/i) !== null) {
|
|
276
|
-
|
|
279
|
+
const tc = new TinyColor(tmpColor);
|
|
277
280
|
return tc.isValid ? this.tinyColorToString(tc) : defColorString;
|
|
278
281
|
}
|
|
279
282
|
|
|
280
283
|
// correct number which have the alpha value defined as a single hex digit
|
|
281
284
|
if (tmpColor.match(/^#[a-f0-9]{7}$/i) !== null) {
|
|
282
|
-
|
|
285
|
+
const tc = new TinyColor(tmpColor.slice(0, 7) + '0' + tmpColor.slice(-1));
|
|
283
286
|
return tc.isValid ? this.tinyColorToString(tc) : defColorString;
|
|
284
287
|
}
|
|
285
288
|
|
|
286
289
|
// check if tinycolor understands the string
|
|
287
|
-
|
|
290
|
+
const tc = new TinyColor(tmpColor);
|
|
288
291
|
return tc.isValid ? this.tinyColorToString(tc) : defColorString;
|
|
289
292
|
}
|
|
290
293
|
|
|
291
|
-
public toThreeJsColorInput(color:
|
|
294
|
+
public toThreeJsColorInput(color: unknown): string {
|
|
292
295
|
const c = this.toHexColor(color);
|
|
293
296
|
return c.slice(0, c.length - 2);
|
|
294
297
|
}
|
|
295
298
|
|
|
296
|
-
public toVec3(point:
|
|
299
|
+
public toVec3(point: vec3 | { x: number, y: number, z: number} | { X: number, Y: number, Z: number}): vec3 {
|
|
297
300
|
if (Array.isArray(point) && point.length >= 3 && typeof point[0] === 'number' && typeof point[1] === 'number' && typeof point[2] === 'number')
|
|
298
301
|
return vec3.fromValues(point[0], point[1], point[2]);
|
|
299
302
|
|
|
300
|
-
|
|
301
|
-
|
|
303
|
+
const pointCast1 = point as { x: number, y: number, z: number};
|
|
304
|
+
if (((pointCast1.x || pointCast1.x === 0) && typeof pointCast1.x === 'number') && ((pointCast1.y || pointCast1.y === 0) && typeof pointCast1.y === 'number') && ((pointCast1.z || pointCast1.z === 0) && typeof pointCast1.z === 'number'))
|
|
305
|
+
return vec3.fromValues(pointCast1.x, pointCast1.y, pointCast1.z);
|
|
302
306
|
|
|
303
|
-
|
|
304
|
-
|
|
307
|
+
const pointCast2 = point as { X: number, Y: number, Z: number};
|
|
308
|
+
if (((pointCast2.X || pointCast2.X === 0) && typeof pointCast2.X === 'number') && ((pointCast2.Y || pointCast2.Y === 0) && typeof pointCast2.Y === 'number') && ((pointCast2.Z || pointCast2.Z === 0) && typeof pointCast2.Z === 'number'))
|
|
309
|
+
return vec3.fromValues(pointCast2.X, pointCast2.Y, pointCast2.Z);
|
|
305
310
|
|
|
306
311
|
return vec3.create();
|
|
307
312
|
}
|
|
@@ -9,7 +9,8 @@ export enum EVENTTYPE_RENDERING {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export enum EVENTTYPE_SCENE {
|
|
12
|
-
SCENE_BOUNDING_BOX_CHANGE = "scene.boundingBoxChange"
|
|
12
|
+
SCENE_BOUNDING_BOX_CHANGE = "scene.boundingBoxChange",
|
|
13
|
+
SCENE_BOUNDING_BOX_EMPTY = "scene.boundingBoxEmpty"
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
// VIEWPORT
|
|
@@ -39,6 +40,10 @@ export enum EVENTTYPE_SESSION {
|
|
|
39
40
|
SESSION_INITIAL_OUTPUTS_LOADED = 'session.initialOutputsLoaded'
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
export enum EVENTTYPE_OUTPUT {
|
|
44
|
+
OUTPUT_UPDATED = "output.updated"
|
|
45
|
+
}
|
|
46
|
+
|
|
42
47
|
export enum EVENTTYPE_TASK {
|
|
43
48
|
TASK_START = "task.start",
|
|
44
49
|
TASK_PROCESS = "task.process",
|
|
@@ -58,6 +63,7 @@ export enum EVENTTYPE_INTERACTION {
|
|
|
58
63
|
|
|
59
64
|
export const EVENTTYPE = {
|
|
60
65
|
CAMERA: EVENTTYPE_CAMERA,
|
|
66
|
+
OUTPUT: EVENTTYPE_OUTPUT,
|
|
61
67
|
RENDERING: EVENTTYPE_RENDERING,
|
|
62
68
|
SCENE: EVENTTYPE_SCENE,
|
|
63
69
|
SESSION: EVENTTYPE_SESSION,
|
|
@@ -66,4 +72,4 @@ export const EVENTTYPE = {
|
|
|
66
72
|
TASK: EVENTTYPE_TASK
|
|
67
73
|
};
|
|
68
74
|
|
|
69
|
-
export type MainEventTypes = typeof EVENTTYPE_CAMERA | typeof EVENTTYPE_RENDERING | typeof EVENTTYPE_SCENE | typeof EVENTTYPE_SESSION | typeof EVENTTYPE_VIEWPORT | typeof EVENTTYPE_INTERACTION | typeof EVENTTYPE_TASK;
|
|
75
|
+
export type MainEventTypes = typeof EVENTTYPE_CAMERA | typeof EVENTTYPE_OUTPUT | typeof EVENTTYPE_RENDERING | typeof EVENTTYPE_SCENE | typeof EVENTTYPE_SESSION | typeof EVENTTYPE_VIEWPORT | typeof EVENTTYPE_INTERACTION | typeof EVENTTYPE_TASK;
|
|
@@ -1,13 +1,24 @@
|
|
|
1
|
-
import axios, { AxiosRequestConfig } from 'axios'
|
|
2
|
-
import { ShapeDiverError as ShapeDiverBackendError, isGBResponseError, isGBRequestError, isGBError } from '@shapediver/sdk.geometry-api-sdk-v2'
|
|
1
|
+
import axios, { AxiosRequestConfig } from 'axios';
|
|
2
|
+
import { ShapeDiverError as ShapeDiverBackendError, isGBResponseError, isGBRequestError, isGBError } from '@shapediver/sdk.geometry-api-sdk-v2';
|
|
3
3
|
import { ShapeDiverGeometryBackendError, ShapeDiverGeometryBackendRequestError, ShapeDiverGeometryBackendResponseError } from '../logger/ShapeDiverBackendErrors';
|
|
4
4
|
import { HttpResponse } from './HttpResponse';
|
|
5
|
+
import { Converter } from '../converter/Converter';
|
|
5
6
|
|
|
6
7
|
export class HttpClient {
|
|
7
|
-
// #region Properties (
|
|
8
|
-
|
|
8
|
+
// #region Properties (7)
|
|
9
9
|
private static _instance: HttpClient;
|
|
10
10
|
|
|
11
|
+
private _dataCache: Map<
|
|
12
|
+
string,
|
|
13
|
+
{
|
|
14
|
+
value: Promise<HttpResponse<unknown>>,
|
|
15
|
+
timestamp: number,
|
|
16
|
+
resolved: boolean,
|
|
17
|
+
size?: number
|
|
18
|
+
}> = new Map();
|
|
19
|
+
private _enableCaching: boolean = true;
|
|
20
|
+
private _excludedQueryParameters: string[] = ['Expires', 'Signature', 'Key-Pair-Id'];
|
|
21
|
+
private _maxCacheSize: number = 1024 * 1024 * 32;
|
|
11
22
|
private _sessionLoading: {
|
|
12
23
|
[key: string]: {
|
|
13
24
|
getAsset: (url: string) => Promise<[ArrayBuffer, string, string]>,
|
|
@@ -15,10 +26,11 @@ export class HttpClient {
|
|
|
15
26
|
}
|
|
16
27
|
} = {};
|
|
17
28
|
|
|
18
|
-
// #endregion Properties (
|
|
29
|
+
// #endregion Properties (7)
|
|
19
30
|
|
|
20
31
|
// #region Constructors (1)
|
|
21
32
|
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
22
34
|
private constructor() { }
|
|
23
35
|
|
|
24
36
|
// #endregion Constructors (1)
|
|
@@ -31,8 +43,44 @@ export class HttpClient {
|
|
|
31
43
|
|
|
32
44
|
// #endregion Public Static Accessors (1)
|
|
33
45
|
|
|
46
|
+
// #region Public Accessors (6)
|
|
47
|
+
|
|
48
|
+
public get enableCaching(): boolean {
|
|
49
|
+
return this._enableCaching;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public set enableCaching(value: boolean) {
|
|
53
|
+
this._enableCaching = value;
|
|
54
|
+
if (this._enableCaching === false)
|
|
55
|
+
this._dataCache.clear();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public get excludedQueryParameters(): string[] {
|
|
59
|
+
return this._excludedQueryParameters;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public set excludedQueryParameters(value: string[]) {
|
|
63
|
+
this._excludedQueryParameters = value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public get maxCacheSize(): number {
|
|
67
|
+
return this._maxCacheSize;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public set maxCacheSize(value: number) {
|
|
71
|
+
this._maxCacheSize = value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// #endregion Public Accessors (6)
|
|
75
|
+
|
|
34
76
|
// #region Public Methods (5)
|
|
35
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Add the data loading options from a session.
|
|
80
|
+
*
|
|
81
|
+
* @param sessionId
|
|
82
|
+
* @param callbacks
|
|
83
|
+
*/
|
|
36
84
|
public addDataLoading(sessionId: string, callbacks: {
|
|
37
85
|
getAsset: (url: string) => Promise<[ArrayBuffer, string, string]>,
|
|
38
86
|
downloadTexture: (sessionId: string, url: string) => Promise<[ArrayBuffer, string]>,
|
|
@@ -53,13 +101,30 @@ export class HttpClient {
|
|
|
53
101
|
if (isGBResponseError(e)) {
|
|
54
102
|
throw new ShapeDiverGeometryBackendResponseError(e.message, e.status, e.error, e.desc);
|
|
55
103
|
} else if (isGBRequestError(e)) {
|
|
56
|
-
throw new ShapeDiverGeometryBackendRequestError(e.message, e.desc)
|
|
57
|
-
} else if(isGBError(e)) {
|
|
58
|
-
throw new ShapeDiverGeometryBackendError(e.message)
|
|
104
|
+
throw new ShapeDiverGeometryBackendRequestError(e.message, e.desc);
|
|
105
|
+
} else if (isGBError(e)) {
|
|
106
|
+
throw new ShapeDiverGeometryBackendError(e.message);
|
|
59
107
|
}
|
|
60
108
|
}
|
|
61
109
|
|
|
62
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Get the requested resource either as a download or from the cache.
|
|
112
|
+
* If available, the registered session loading is used for download.
|
|
113
|
+
* Textures are downloaded via a specific endpoint and can be converted in this step as well.
|
|
114
|
+
* Depending on the provided caching options, the requested resource might already be cached.
|
|
115
|
+
*
|
|
116
|
+
* @param href
|
|
117
|
+
* @param config
|
|
118
|
+
* @param textureLoading
|
|
119
|
+
* @param textureConversion
|
|
120
|
+
* @returns
|
|
121
|
+
*/
|
|
122
|
+
public async get(href: string, config: AxiosRequestConfig = { responseType: 'arraybuffer' }, textureLoading: boolean = false): Promise<HttpResponse<unknown>> {
|
|
123
|
+
const dataKey = this.hrefToDataKey(href);
|
|
124
|
+
|
|
125
|
+
// return element if it exists in cache
|
|
126
|
+
if (this._dataCache.has(dataKey)) return this.getFromCache(dataKey);
|
|
127
|
+
|
|
63
128
|
// try to get sessionId from href
|
|
64
129
|
let sessionId = this.getSessionId(href);
|
|
65
130
|
|
|
@@ -75,35 +140,40 @@ export class HttpClient {
|
|
|
75
140
|
if (sessionId)
|
|
76
141
|
sessionLoading = this._sessionLoading[sessionId];
|
|
77
142
|
|
|
143
|
+
let loadingPromise;
|
|
144
|
+
|
|
78
145
|
// separation texture vs everything else
|
|
79
146
|
if (textureLoading) {
|
|
80
147
|
// if we have a sessionId and the sessionLoading functions and the image is not a blob or data, we load it via the sdk
|
|
81
148
|
if (sessionLoading !== undefined && sessionId !== undefined && !href.startsWith('blob:') && !href.startsWith('data:')) {
|
|
82
149
|
// take first session to load a texture that is not session related
|
|
83
|
-
|
|
84
|
-
sessionLoading!.downloadTexture(sessionId!, href).then((result) => {
|
|
150
|
+
loadingPromise = new Promise<HttpResponse<ArrayBuffer>>((resolve, reject) => {
|
|
151
|
+
sessionLoading!.downloadTexture(sessionId!, href).then(async (result) => {
|
|
85
152
|
resolve({
|
|
86
153
|
data: result[0],
|
|
87
154
|
headers: {
|
|
88
155
|
'content-type': result[1]
|
|
89
156
|
}
|
|
90
|
-
})
|
|
91
|
-
}).catch(e => reject(e))
|
|
92
|
-
}).catch(e => { throw this.convertError(e) });
|
|
157
|
+
});
|
|
158
|
+
}).catch(e => reject(e));
|
|
159
|
+
}).catch(e => { throw this.convertError(e); });
|
|
93
160
|
} else {
|
|
94
161
|
// we can load blobs and data urls directly
|
|
95
162
|
// or load it directly if we don't have a session
|
|
96
|
-
|
|
97
|
-
.
|
|
163
|
+
loadingPromise = axios(href, Object.assign({ method: 'get' }, config))
|
|
164
|
+
.then(async (result) => {
|
|
165
|
+
return result;
|
|
166
|
+
})
|
|
167
|
+
.catch(e => { throw this.convertError(e); });
|
|
98
168
|
}
|
|
99
169
|
} else {
|
|
100
170
|
if (!sessionLoading) {
|
|
101
171
|
// if there is no session to load from, we use the fallback option
|
|
102
|
-
|
|
103
|
-
.catch(e => { throw this.convertError(e) });
|
|
172
|
+
loadingPromise = axios(href, Object.assign({ method: 'get' }, config))
|
|
173
|
+
.catch(e => { throw this.convertError(e); });
|
|
104
174
|
} else {
|
|
105
175
|
// all data links where we could somehow find a session to load it with
|
|
106
|
-
|
|
176
|
+
loadingPromise = new Promise<HttpResponse<ArrayBuffer>>((resolve, reject) => {
|
|
107
177
|
sessionLoading!.getAsset(href)
|
|
108
178
|
.then((result) => {
|
|
109
179
|
resolve({
|
|
@@ -111,31 +181,115 @@ export class HttpClient {
|
|
|
111
181
|
headers: {
|
|
112
182
|
'content-type': result[1]
|
|
113
183
|
}
|
|
114
|
-
})
|
|
184
|
+
});
|
|
115
185
|
})
|
|
116
186
|
.catch(() => {
|
|
117
187
|
// if this fails, we just load it directly
|
|
118
188
|
const axiosPromise = axios(href, Object.assign({ method: 'get' }, config));
|
|
119
|
-
axiosPromise.catch(e => reject(e))
|
|
189
|
+
axiosPromise.catch(e => reject(e));
|
|
120
190
|
resolve(axiosPromise);
|
|
121
191
|
});
|
|
122
|
-
}).catch(e => { throw this.convertError(e) });
|
|
192
|
+
}).catch(e => { throw this.convertError(e); });
|
|
123
193
|
}
|
|
124
194
|
}
|
|
195
|
+
|
|
196
|
+
if (this.enableCaching)
|
|
197
|
+
this.addToCache(dataKey, loadingPromise);
|
|
198
|
+
|
|
199
|
+
return loadingPromise;
|
|
125
200
|
}
|
|
126
201
|
|
|
127
|
-
|
|
128
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Get the requested texture either as a download or from the cache.
|
|
204
|
+
*
|
|
205
|
+
* @param href
|
|
206
|
+
* @returns
|
|
207
|
+
*/
|
|
208
|
+
public async loadTexture(href: string): Promise<HttpResponse<{ image: HTMLImageElement, blob: Blob }>> {
|
|
209
|
+
const response = await (this.get(href, undefined, true) as Promise<HttpResponse<ArrayBuffer>>);
|
|
210
|
+
const arrayBufferView = new Uint8Array( response.data );
|
|
211
|
+
const blob = new Blob([ arrayBufferView ], { type: response.headers['content-type'] } );
|
|
212
|
+
const image = await Converter.instance.responseToImage(response, blob);
|
|
213
|
+
return {
|
|
214
|
+
data: {
|
|
215
|
+
image,
|
|
216
|
+
blob
|
|
217
|
+
},
|
|
218
|
+
size: response.data.byteLength,
|
|
219
|
+
headers: response.headers
|
|
220
|
+
};
|
|
129
221
|
}
|
|
130
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Add the data loading options from a session.
|
|
225
|
+
*
|
|
226
|
+
* @param sessionId
|
|
227
|
+
*/
|
|
131
228
|
public removeDataLoading(sessionId: string) {
|
|
132
229
|
delete this._sessionLoading[sessionId];
|
|
133
230
|
}
|
|
134
231
|
|
|
135
232
|
// #endregion Public Methods (5)
|
|
136
233
|
|
|
137
|
-
// #region Private Methods (
|
|
234
|
+
// #region Private Methods (5)
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Add
|
|
238
|
+
*
|
|
239
|
+
* @param key
|
|
240
|
+
* @param value
|
|
241
|
+
*/
|
|
242
|
+
private addToCache(key: string, value: Promise<HttpResponse<unknown>>) {
|
|
243
|
+
// Remove items from the cache until the cache size is smaller than the maximum cache size.
|
|
244
|
+
// Only resolved promises are evaluated, as unresolved promises don't add any size.
|
|
245
|
+
while (this.calculateCacheSize() >= this._maxCacheSize) {
|
|
246
|
+
// Remove the oldest entry if the cache is full
|
|
247
|
+
const oldestKey = this._dataCache.keys().next().value;
|
|
248
|
+
this._dataCache.delete(oldestKey);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const timestamp = Date.now();
|
|
252
|
+
this._dataCache.set(key, { value, timestamp, resolved: false });
|
|
253
|
+
|
|
254
|
+
// once the promise resolves, set resolved and size properties
|
|
255
|
+
value.then((promiseResult) => {
|
|
256
|
+
const size = promiseResult.size ? promiseResult.size : (promiseResult.data as ArrayBuffer).byteLength;
|
|
257
|
+
this._dataCache.set(key, { value, timestamp, resolved: true, size });
|
|
258
|
+
}).catch(e => { throw this.convertError(e); });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Calculate the current cache size from all resolved promises.
|
|
263
|
+
*
|
|
264
|
+
* @returns
|
|
265
|
+
*/
|
|
266
|
+
private calculateCacheSize() {
|
|
267
|
+
let size = 0;
|
|
268
|
+
this._dataCache.forEach(value => {
|
|
269
|
+
if (value.resolved === true)
|
|
270
|
+
size += value.size!;
|
|
271
|
+
});
|
|
272
|
+
return size;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get the value of an object from the cache.
|
|
277
|
+
*
|
|
278
|
+
* @param key
|
|
279
|
+
* @returns
|
|
280
|
+
*/
|
|
281
|
+
private getFromCache(key: string): Promise<HttpResponse<unknown>> {
|
|
282
|
+
const cachedObject = this._dataCache.get(key)!;
|
|
283
|
+
this._dataCache.set(key, { value: cachedObject.value, timestamp: Date.now(), resolved: cachedObject.resolved, size: cachedObject.size });
|
|
284
|
+
return cachedObject.value;
|
|
285
|
+
}
|
|
138
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Get the session id of the provided href.
|
|
289
|
+
*
|
|
290
|
+
* @param href
|
|
291
|
+
* @returns
|
|
292
|
+
*/
|
|
139
293
|
private getSessionId(href: string): string | undefined {
|
|
140
294
|
// searching for "/session/SESSION_ID/{'output' | 'export' | 'texture'}/ASSET_DATA"
|
|
141
295
|
const parts = href.split('/');
|
|
@@ -151,5 +305,29 @@ export class HttpClient {
|
|
|
151
305
|
return;
|
|
152
306
|
}
|
|
153
307
|
|
|
154
|
-
|
|
308
|
+
/**
|
|
309
|
+
* Convert the provided href to a data cache key.
|
|
310
|
+
* In this conversion the excludedQueryParameters are removed from the href.
|
|
311
|
+
*
|
|
312
|
+
* @param href
|
|
313
|
+
* @returns
|
|
314
|
+
*/
|
|
315
|
+
private hrefToDataKey(href: string) {
|
|
316
|
+
const url = new URL(href);
|
|
317
|
+
|
|
318
|
+
// Create a URLSearchParams object from the existing query parameters
|
|
319
|
+
const params = new URLSearchParams(url.search);
|
|
320
|
+
|
|
321
|
+
for (let i = 0; i < this._excludedQueryParameters.length; i++)
|
|
322
|
+
// Remove specific query parameters
|
|
323
|
+
params.delete(this._excludedQueryParameters[i]);
|
|
324
|
+
|
|
325
|
+
// Reconstruct the URL with the modified query parameters
|
|
326
|
+
url.search = params.toString();
|
|
327
|
+
|
|
328
|
+
const hrefAsKey = url.toString();
|
|
329
|
+
return window.btoa(hrefAsKey);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// #endregion Private Methods (5)
|
|
155
333
|
}
|