@carbonenginejs/runtime-utils 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -7
- package/THIRD-PARTY-NOTICES.md +30 -4
- package/docs/README.md +10 -7
- package/docs/architecture.md +7 -6
- package/docs/concepts/foundation-consolidation.md +20 -21
- package/docs/const-kb.md +18 -11
- package/docs/core-types/DECORATOR-TODOS.md +1 -1
- package/docs/reference/api.md +43 -1
- package/docs/reference/classes/README.md +20 -0
- package/package.json +8 -1
- package/src/constants/index.js +1 -0
- package/src/constants/trinity.js +13 -0
- package/src/errors/CjsError.js +286 -0
- package/src/errors/index.js +5 -0
- package/src/index.js +4 -0
- package/src/is.js +71 -9
- package/src/mesh.js +37 -7
- package/src/model/CjsModel.js +61 -16
- package/src/object.js +39 -0
- package/src/path.js +32 -0
- package/src/schema/CjsSchema.js +416 -297
- package/src/vec3.js +16 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { isPlainObject } from "../is.js";
|
|
2
|
+
|
|
3
|
+
const
|
|
4
|
+
ERROR_CODE_PATTERN = /^CJS_[A-Z][A-Z0-9]*(?:_[A-Z][A-Z0-9]*)*$/u,
|
|
5
|
+
DEFAULT_CANCELLATION_MESSAGE = "The operation was cancelled.";
|
|
6
|
+
|
|
7
|
+
export const CJS_OPERATION_CANCELLED = "CJS_OPERATION_CANCELLED";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Represents one structured operational failure with a stable CarbonEngineJS code.
|
|
11
|
+
*
|
|
12
|
+
* Programmer-contract violations should continue to use native error classes
|
|
13
|
+
* such as `TypeError`, `RangeError`, and `SyntaxError`.
|
|
14
|
+
*/
|
|
15
|
+
export class CjsError extends Error
|
|
16
|
+
{
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Creates an operational error.
|
|
20
|
+
*
|
|
21
|
+
* Details are detached and deeply frozen JSON-safe data. Cause identity is
|
|
22
|
+
* preserved through the native `Error` cause property when supplied.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} code Stable uppercase `CJS_*` machine-readable code.
|
|
25
|
+
* @param {string} message Human-readable failure description.
|
|
26
|
+
* @param {{cause?: *, details?: object|null}} [options]
|
|
27
|
+
*/
|
|
28
|
+
constructor(code, message, options = {})
|
|
29
|
+
{
|
|
30
|
+
const
|
|
31
|
+
normalizedCode = CjsError.#NormalizeCode(code),
|
|
32
|
+
normalizedMessage = CjsError.#NormalizeMessage(message),
|
|
33
|
+
normalizedOptions = CjsError.#NormalizeOptions(options),
|
|
34
|
+
errorOptions = Object.hasOwn(normalizedOptions, "cause")
|
|
35
|
+
? { cause: normalizedOptions.cause }
|
|
36
|
+
: undefined;
|
|
37
|
+
|
|
38
|
+
super(normalizedMessage, errorOptions);
|
|
39
|
+
|
|
40
|
+
this.name = new.target.name;
|
|
41
|
+
|
|
42
|
+
Object.defineProperties(this, {
|
|
43
|
+
code: {
|
|
44
|
+
value: normalizedCode,
|
|
45
|
+
enumerable: true,
|
|
46
|
+
configurable: false,
|
|
47
|
+
writable: false
|
|
48
|
+
},
|
|
49
|
+
details: {
|
|
50
|
+
value: CjsError.#NormalizeDetails(normalizedOptions.details),
|
|
51
|
+
enumerable: true,
|
|
52
|
+
configurable: false,
|
|
53
|
+
writable: false
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Checks a structured or legacy error for an exact stable code.
|
|
60
|
+
*
|
|
61
|
+
* Invalid candidate codes and inaccessible error properties return false
|
|
62
|
+
* so this helper remains safe inside failure handling.
|
|
63
|
+
*/
|
|
64
|
+
static hasCode(error, code)
|
|
65
|
+
{
|
|
66
|
+
if (!CjsError.#IsCode(code) || error === null || error === undefined)
|
|
67
|
+
{
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try
|
|
72
|
+
{
|
|
73
|
+
return error.code === code;
|
|
74
|
+
}
|
|
75
|
+
catch
|
|
76
|
+
{
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Reports whether a value is one valid stable error code. */
|
|
82
|
+
static #IsCode(value)
|
|
83
|
+
{
|
|
84
|
+
return typeof value === "string" && ERROR_CODE_PATTERN.test(value);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Validates and returns one stable error code. */
|
|
88
|
+
static #NormalizeCode(value)
|
|
89
|
+
{
|
|
90
|
+
if (!CjsError.#IsCode(value))
|
|
91
|
+
{
|
|
92
|
+
throw new TypeError("code must be an uppercase CJS_* identifier.");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Validates and returns one non-empty error message. */
|
|
99
|
+
static #NormalizeMessage(value)
|
|
100
|
+
{
|
|
101
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
102
|
+
{
|
|
103
|
+
throw new TypeError("message must be a non-empty string.");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Validates and returns one plain constructor-options record. */
|
|
110
|
+
static #NormalizeOptions(value)
|
|
111
|
+
{
|
|
112
|
+
if (!isPlainObject(value))
|
|
113
|
+
{
|
|
114
|
+
throw new TypeError("options must be a plain object.");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Converts optional details into detached deeply frozen data. */
|
|
121
|
+
static #NormalizeDetails(value)
|
|
122
|
+
{
|
|
123
|
+
if (value === undefined || value === null)
|
|
124
|
+
{
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!isPlainObject(value))
|
|
129
|
+
{
|
|
130
|
+
throw new TypeError("details must be a JSON-safe plain object or null.");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return CjsError.#CloneDetailsValue(value, "details", new Set());
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Clones one JSON-safe details value while detecting active cycles. */
|
|
137
|
+
static #CloneDetailsValue(value, path, active)
|
|
138
|
+
{
|
|
139
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
140
|
+
{
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (typeof value === "number")
|
|
145
|
+
{
|
|
146
|
+
if (!Number.isFinite(value))
|
|
147
|
+
{
|
|
148
|
+
throw new TypeError(`${path} must contain only finite numbers.`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!Array.isArray(value) && !isPlainObject(value))
|
|
155
|
+
{
|
|
156
|
+
throw new TypeError(`${path} must contain only JSON-safe values.`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (active.has(value))
|
|
160
|
+
{
|
|
161
|
+
throw new TypeError(`${path} must not contain a circular reference.`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
active.add(value);
|
|
165
|
+
|
|
166
|
+
const clone = Array.isArray(value)
|
|
167
|
+
? CjsError.#CloneDetailsArray(value, path, active)
|
|
168
|
+
: CjsError.#CloneDetailsRecord(value, path, active);
|
|
169
|
+
|
|
170
|
+
active.delete(value);
|
|
171
|
+
return Object.freeze(clone);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Clones one dense JSON-safe details array. */
|
|
175
|
+
static #CloneDetailsArray(value, path, active)
|
|
176
|
+
{
|
|
177
|
+
const clone = [];
|
|
178
|
+
|
|
179
|
+
for (const key of Reflect.ownKeys(value))
|
|
180
|
+
{
|
|
181
|
+
if (key === "length")
|
|
182
|
+
{
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (typeof key !== "string"
|
|
187
|
+
|| !/^(?:0|[1-9]\d*)$/u.test(key)
|
|
188
|
+
|| Number(key) >= value.length)
|
|
189
|
+
{
|
|
190
|
+
throw new TypeError(`${path} arrays must contain only indexed values.`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (let i = 0; i < value.length; i++)
|
|
195
|
+
{
|
|
196
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(i));
|
|
197
|
+
|
|
198
|
+
if (!descriptor?.enumerable || !Object.hasOwn(descriptor, "value"))
|
|
199
|
+
{
|
|
200
|
+
throw new TypeError(`${path} arrays must contain dense data properties.`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
clone.push(CjsError.#CloneDetailsValue(
|
|
204
|
+
descriptor.value,
|
|
205
|
+
`${path}[${i}]`,
|
|
206
|
+
active
|
|
207
|
+
));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return clone;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Clones one plain JSON-safe details record. */
|
|
214
|
+
static #CloneDetailsRecord(value, path, active)
|
|
215
|
+
{
|
|
216
|
+
const clone = {};
|
|
217
|
+
|
|
218
|
+
for (const key of Reflect.ownKeys(value))
|
|
219
|
+
{
|
|
220
|
+
if (typeof key !== "string")
|
|
221
|
+
{
|
|
222
|
+
throw new TypeError(`${path} must not contain symbol keys.`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
226
|
+
|
|
227
|
+
if (!descriptor?.enumerable || !Object.hasOwn(descriptor, "value"))
|
|
228
|
+
{
|
|
229
|
+
throw new TypeError(`${path} must contain only enumerable data properties.`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
Object.defineProperty(clone, key, {
|
|
233
|
+
value: CjsError.#CloneDetailsValue(
|
|
234
|
+
descriptor.value,
|
|
235
|
+
`${path}[${JSON.stringify(key)}]`,
|
|
236
|
+
active
|
|
237
|
+
),
|
|
238
|
+
enumerable: true,
|
|
239
|
+
configurable: false,
|
|
240
|
+
writable: false
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return clone;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Represents one cancelled operation using Web-compatible abort identity. */
|
|
250
|
+
export class CjsCancellationError extends CjsError
|
|
251
|
+
{
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Creates a cancellation error with stable code `CJS_OPERATION_CANCELLED`.
|
|
255
|
+
*
|
|
256
|
+
* @param {string} [message]
|
|
257
|
+
* @param {{cause?: *, details?: object|null}} [options]
|
|
258
|
+
*/
|
|
259
|
+
constructor(message = DEFAULT_CANCELLATION_MESSAGE, options = {})
|
|
260
|
+
{
|
|
261
|
+
super(CJS_OPERATION_CANCELLED, message, options);
|
|
262
|
+
this.name = "AbortError";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Checks for this cancellation type, its stable code, or a platform
|
|
267
|
+
* `AbortError` name.
|
|
268
|
+
*/
|
|
269
|
+
static is(error)
|
|
270
|
+
{
|
|
271
|
+
if (CjsError.hasCode(error, CJS_OPERATION_CANCELLED))
|
|
272
|
+
{
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
try
|
|
277
|
+
{
|
|
278
|
+
return error?.name === "AbortError";
|
|
279
|
+
}
|
|
280
|
+
catch
|
|
281
|
+
{
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
}
|
package/src/index.js
CHANGED
|
@@ -11,6 +11,8 @@ export * as json from "./json.js";
|
|
|
11
11
|
export * from "./json.js";
|
|
12
12
|
export * as lookup from "./lookup.js";
|
|
13
13
|
export * from "./lookup.js";
|
|
14
|
+
export * as object from "./object.js";
|
|
15
|
+
export * from "./object.js";
|
|
14
16
|
export * as path from "./path.js";
|
|
15
17
|
export * from "./path.js";
|
|
16
18
|
export * as text from "./text.js";
|
|
@@ -21,6 +23,8 @@ export {
|
|
|
21
23
|
assertNonEmptyString,
|
|
22
24
|
assertSupportedVersion
|
|
23
25
|
} from "./validation.js";
|
|
26
|
+
export * as errors from "./errors/index.js";
|
|
27
|
+
export * from "./errors/index.js";
|
|
24
28
|
|
|
25
29
|
export * as constants from "./constants/index.js";
|
|
26
30
|
export * from "./constants/index.js";
|
package/src/is.js
CHANGED
|
@@ -143,15 +143,77 @@ export function isError(a)
|
|
|
143
143
|
* @param {*} a
|
|
144
144
|
* @returns {Boolean}
|
|
145
145
|
*/
|
|
146
|
-
export function isNumber(a)
|
|
147
|
-
{
|
|
148
|
-
return isTag(a, "[object Number]");
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Checks if a value
|
|
153
|
-
* @param {*}
|
|
154
|
-
* @returns {Boolean}
|
|
146
|
+
export function isNumber(a)
|
|
147
|
+
{
|
|
148
|
+
return isTag(a, "[object Number]");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Checks if a value fits a signed 8-bit integer.
|
|
153
|
+
* @param {*} value
|
|
154
|
+
* @returns {Boolean}
|
|
155
|
+
*/
|
|
156
|
+
export function isInt8(value)
|
|
157
|
+
{
|
|
158
|
+
return Number.isInteger(value) && value >= -0x80 && value <= 0x7f;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Checks if a value fits an unsigned 8-bit integer.
|
|
163
|
+
* @param {*} value
|
|
164
|
+
* @returns {Boolean}
|
|
165
|
+
*/
|
|
166
|
+
export function isUint8(value)
|
|
167
|
+
{
|
|
168
|
+
return Number.isInteger(value) && value >= 0 && value <= 0xff;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Checks if a value fits a signed 16-bit integer.
|
|
173
|
+
* @param {*} value
|
|
174
|
+
* @returns {Boolean}
|
|
175
|
+
*/
|
|
176
|
+
export function isInt16(value)
|
|
177
|
+
{
|
|
178
|
+
return Number.isInteger(value) && value >= -0x8000 && value <= 0x7fff;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Checks if a value fits an unsigned 16-bit integer.
|
|
183
|
+
* @param {*} value
|
|
184
|
+
* @returns {Boolean}
|
|
185
|
+
*/
|
|
186
|
+
export function isUint16(value)
|
|
187
|
+
{
|
|
188
|
+
return Number.isInteger(value) && value >= 0 && value <= 0xffff;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Checks if a value fits a signed 32-bit integer.
|
|
193
|
+
* @param {*} value
|
|
194
|
+
* @returns {Boolean}
|
|
195
|
+
*/
|
|
196
|
+
export function isInt32(value)
|
|
197
|
+
{
|
|
198
|
+
return Number.isInteger(value)
|
|
199
|
+
&& value >= -0x80000000
|
|
200
|
+
&& value <= 0x7fffffff;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Checks if a value fits an unsigned 32-bit integer.
|
|
205
|
+
* @param {*} value
|
|
206
|
+
* @returns {Boolean}
|
|
207
|
+
*/
|
|
208
|
+
export function isUint32(value)
|
|
209
|
+
{
|
|
210
|
+
return Number.isInteger(value) && value >= 0 && value <= 0xffffffff;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Checks if a value is a function
|
|
215
|
+
* @param {*} a
|
|
216
|
+
* @returns {Boolean}
|
|
155
217
|
*/
|
|
156
218
|
export function isFunction(a)
|
|
157
219
|
{
|
package/src/mesh.js
CHANGED
|
@@ -45,13 +45,42 @@ function validateIndices(indices, vertexCount)
|
|
|
45
45
|
*/
|
|
46
46
|
export function triangleNormal(a, b, c)
|
|
47
47
|
{
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
return triangleNormalTo([ 0, 0, 0 ], a, b, c);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Calculate a unit face normal into caller-owned storage.
|
|
53
|
+
*
|
|
54
|
+
* @param {ArrayLike<number>} out Receiving xyz vector.
|
|
55
|
+
* @param {ArrayLike<number>} a First xyz vertex.
|
|
56
|
+
* @param {ArrayLike<number>} b Second xyz vertex.
|
|
57
|
+
* @param {ArrayLike<number>} c Third xyz vertex.
|
|
58
|
+
* @returns {ArrayLike<number>} The receiving vector.
|
|
59
|
+
*/
|
|
60
|
+
export function triangleNormalTo(out, a, b, c)
|
|
61
|
+
{
|
|
62
|
+
const
|
|
63
|
+
abX = b[0] - a[0],
|
|
64
|
+
abY = b[1] - a[1],
|
|
65
|
+
abZ = b[2] - a[2],
|
|
66
|
+
acX = c[0] - a[0],
|
|
67
|
+
acY = c[1] - a[1],
|
|
68
|
+
acZ = c[2] - a[2];
|
|
69
|
+
|
|
70
|
+
out[0] = abY * acZ - abZ * acY;
|
|
71
|
+
out[1] = abZ * acX - abX * acZ;
|
|
72
|
+
out[2] = abX * acY - abY * acX;
|
|
73
|
+
|
|
74
|
+
const length = Math.hypot(out[0], out[1], out[2]);
|
|
75
|
+
|
|
76
|
+
if (length > 0)
|
|
77
|
+
{
|
|
78
|
+
out[0] /= length;
|
|
79
|
+
out[1] /= length;
|
|
80
|
+
out[2] /= length;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return out;
|
|
55
84
|
}
|
|
56
85
|
|
|
57
86
|
/**
|
|
@@ -383,6 +412,7 @@ export function generateBiNormals(normals, tangents, options = {})
|
|
|
383
412
|
|
|
384
413
|
export const mesh = Object.freeze({
|
|
385
414
|
triangleNormal,
|
|
415
|
+
triangleNormalTo,
|
|
386
416
|
triangleArea2,
|
|
387
417
|
isDegenerateTriangle,
|
|
388
418
|
computeBoundsFromPositions,
|
package/src/model/CjsModel.js
CHANGED
|
@@ -13,6 +13,15 @@ const MAX_UPDATE_PASSES = 32;
|
|
|
13
13
|
*/
|
|
14
14
|
export class CjsModel extends CjsEventEmitter
|
|
15
15
|
{
|
|
16
|
+
/**
|
|
17
|
+
* Identifies this class as a schema-backed model.
|
|
18
|
+
*
|
|
19
|
+
* Declared statically so CjsSchema can recognise a model class from a field
|
|
20
|
+
* declaration alone, without importing CjsModel - which it cannot do, since
|
|
21
|
+
* this module already imports CjsSchema. Mirrors `CjsResource.isResource`.
|
|
22
|
+
*/
|
|
23
|
+
static isModel = true;
|
|
24
|
+
|
|
16
25
|
/**
|
|
17
26
|
* Creates a schema-backed model with initialized runtime state.
|
|
18
27
|
*/
|
|
@@ -208,16 +217,18 @@ export class CjsModel extends CjsEventEmitter
|
|
|
208
217
|
|
|
209
218
|
if (descend)
|
|
210
219
|
{
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const
|
|
220
|
+
// Only the fields declared to hold child models, precomputed per
|
|
221
|
+
// class - not every field, type-tested per value per visit.
|
|
222
|
+
const children = CjsSchema.getSchema(model.constructor).children;
|
|
223
|
+
const start = reverse ? children.length - 1 : 0;
|
|
224
|
+
const end = reverse ? -1 : children.length;
|
|
214
225
|
const step = reverse ? -1 : 1;
|
|
215
226
|
|
|
216
227
|
for (let i = start; i !== end; i += step)
|
|
217
228
|
{
|
|
218
|
-
const
|
|
219
|
-
if (options.ownedOnly === true &&
|
|
220
|
-
const value = model[
|
|
229
|
+
const child = children[i];
|
|
230
|
+
if (options.ownedOnly === true && !child.owned) continue;
|
|
231
|
+
const value = model[child.name];
|
|
221
232
|
|
|
222
233
|
if (Array.isArray(value))
|
|
223
234
|
{
|
|
@@ -242,19 +253,47 @@ export class CjsModel extends CjsEventEmitter
|
|
|
242
253
|
/**
|
|
243
254
|
* Collects unique resources reported by this model graph into an array.
|
|
244
255
|
*
|
|
256
|
+
* Every model in the graph is visited: reporting resources does not hide a
|
|
257
|
+
* model's descendants, because an under-reported dependency set would let
|
|
258
|
+
* readiness checks pass while a child's resources were still loading.
|
|
259
|
+
*
|
|
260
|
+
* Resources held in schema fields are collected automatically - they are
|
|
261
|
+
* already declared, as `@type.objectRef("TriGeometryRes")` and friends, so
|
|
262
|
+
* restating them in a hook would be the hand-written relay chain this
|
|
263
|
+
* traversal exists to replace.
|
|
264
|
+
*
|
|
265
|
+
* `OnGetResources()` is the escape hatch for resources a model holds
|
|
266
|
+
* outside its schema, such as private fields. It takes no arguments and
|
|
267
|
+
* always returns an iterable of resources - never a bare resource and never
|
|
268
|
+
* nothing. Most models do not implement it.
|
|
269
|
+
*
|
|
245
270
|
* @param {Array<*>} [out=[]] Output array, whose contents are replaced.
|
|
246
271
|
* @returns {Array<*>} The supplied output array.
|
|
247
272
|
*/
|
|
248
273
|
GetResources(out = [])
|
|
249
274
|
{
|
|
250
275
|
const resources = new Set();
|
|
251
|
-
AddResources(resources, out);
|
|
252
276
|
|
|
253
277
|
this.Traverse(model =>
|
|
254
278
|
{
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
279
|
+
for (const field of CjsSchema.getSchema(model.constructor).resources)
|
|
280
|
+
{
|
|
281
|
+
const value = model[field.name];
|
|
282
|
+
if (Array.isArray(value))
|
|
283
|
+
{
|
|
284
|
+
for (const item of value) AddResource(resources, item);
|
|
285
|
+
}
|
|
286
|
+
else
|
|
287
|
+
{
|
|
288
|
+
AddResource(resources, value);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (typeof model.OnGetResources === "function")
|
|
293
|
+
{
|
|
294
|
+
AddResources(resources, model.OnGetResources());
|
|
295
|
+
}
|
|
296
|
+
return true;
|
|
258
297
|
});
|
|
259
298
|
|
|
260
299
|
out.length = 0;
|
|
@@ -776,17 +815,23 @@ function initializeOwnedGraph(root, options = {})
|
|
|
776
815
|
return root;
|
|
777
816
|
}
|
|
778
817
|
|
|
818
|
+
function AddResource(target, value)
|
|
819
|
+
{
|
|
820
|
+
if (value?.isResource === true) target.add(value);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
|
|
779
824
|
function AddResources(target, values)
|
|
780
825
|
{
|
|
781
|
-
if (values ===
|
|
782
|
-
if (values?.isResource === true)
|
|
826
|
+
if (typeof values === "string" || typeof values?.[Symbol.iterator] !== "function")
|
|
783
827
|
{
|
|
784
|
-
|
|
785
|
-
return;
|
|
828
|
+
throw new TypeError("CjsModel.OnGetResources must return an iterable of resources.");
|
|
786
829
|
}
|
|
787
|
-
|
|
830
|
+
|
|
831
|
+
// Empty slots are the model's own unset fields, not a contract violation.
|
|
832
|
+
for (const value of values)
|
|
788
833
|
{
|
|
789
|
-
|
|
834
|
+
if (value !== null && value !== undefined) target.add(value);
|
|
790
835
|
}
|
|
791
836
|
}
|
|
792
837
|
|
package/src/object.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invokes handlers for keys explicitly owned by a source object.
|
|
3
|
+
*
|
|
4
|
+
* @param {object|Function} source Source values.
|
|
5
|
+
* @param {object} handlers Property-keyed handler functions.
|
|
6
|
+
* @param {*} [context=null] Optional `this` value for handlers.
|
|
7
|
+
* @returns {number} Number of invoked handlers.
|
|
8
|
+
*/
|
|
9
|
+
export function hasOwnThen(source, handlers, context = null)
|
|
10
|
+
{
|
|
11
|
+
if (!source || (typeof source !== "object" && typeof source !== "function"))
|
|
12
|
+
{
|
|
13
|
+
throw new TypeError("hasOwnThen source must be an object or function.");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!handlers || typeof handlers !== "object" || Array.isArray(handlers))
|
|
17
|
+
{
|
|
18
|
+
throw new TypeError("hasOwnThen handlers must be an object.");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let invoked = 0;
|
|
22
|
+
|
|
23
|
+
for (const property of Reflect.ownKeys(handlers))
|
|
24
|
+
{
|
|
25
|
+
if (!Object.hasOwn(source, property)) continue;
|
|
26
|
+
|
|
27
|
+
const handler = handlers[property];
|
|
28
|
+
|
|
29
|
+
if (typeof handler !== "function")
|
|
30
|
+
{
|
|
31
|
+
throw new TypeError(`hasOwnThen handler must be a function: ${String(property)}.`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
handler.call(context, source[property], source, property);
|
|
35
|
+
invoked++;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return invoked;
|
|
39
|
+
}
|
package/src/path.js
CHANGED
|
@@ -19,3 +19,35 @@ export function normalizePath(value, options = {})
|
|
|
19
19
|
|
|
20
20
|
return options.lowerCase ? result.toLowerCase() : result;
|
|
21
21
|
}
|
|
22
|
+
|
|
23
|
+
/** Normalizes a case-insensitive URI-style resource path. */
|
|
24
|
+
export function normalizeResourcePath(value)
|
|
25
|
+
{
|
|
26
|
+
return normalizePath(value, { lowerCase: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Returns the normalized extension of a URI-style resource path. */
|
|
30
|
+
export function getResourceExtension(value)
|
|
31
|
+
{
|
|
32
|
+
const path = normalizeResourcePath(value);
|
|
33
|
+
const queryIndex = path.search(/[?#]/u);
|
|
34
|
+
const cleanPath = queryIndex === -1 ? path : path.slice(0, queryIndex);
|
|
35
|
+
const slashIndex = cleanPath.lastIndexOf("/");
|
|
36
|
+
const dotIndex = cleanPath.lastIndexOf(".");
|
|
37
|
+
|
|
38
|
+
if (dotIndex === -1 || dotIndex < slashIndex)
|
|
39
|
+
{
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return cleanPath.slice(dotIndex + 1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Normalizes a resource extension without its optional leading dot. */
|
|
47
|
+
export function normalizeResourceExtension(value)
|
|
48
|
+
{
|
|
49
|
+
return String(value ?? "")
|
|
50
|
+
.trim()
|
|
51
|
+
.replace(/^\./u, "")
|
|
52
|
+
.toLowerCase();
|
|
53
|
+
}
|