@carbonenginejs/runtime-utils 0.1.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carbonenginejs/runtime-utils",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Browser-safe shared utilities, math, constants, Carbon types, schemas, documents, and runtime model primitives.",
5
5
  "type": "module",
6
6
  "main": "src/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 is a function
153
- * @param {*} a
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
  {
@@ -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
- const fields = getModelFields(model);
212
- const start = reverse ? fields.length - 1 : 0;
213
- const end = reverse ? -1 : fields.length;
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 field = fields[i];
219
- if (options.ownedOnly === true && field.io?.ownership !== "owned") continue;
220
- const value = model[field.name];
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
- if (typeof model.OnGetResources !== "function") return true;
256
- AddResources(resources, model.OnGetResources(resources));
257
- return false;
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 === null || values === undefined) return;
782
- if (values?.isResource === true)
826
+ if (typeof values === "string" || typeof values?.[Symbol.iterator] !== "function")
783
827
  {
784
- target.add(values);
785
- return;
828
+ throw new TypeError("CjsModel.OnGetResources must return an iterable of resources.");
786
829
  }
787
- if (typeof values !== "string" && typeof values[Symbol.iterator] === "function")
830
+
831
+ // Empty slots are the model's own unset fields, not a contract violation.
832
+ for (const value of values)
788
833
  {
789
- for (const value of values) AddResources(target, value);
834
+ if (value !== null && value !== undefined) target.add(value);
790
835
  }
791
836
  }
792
837
 
@@ -1,8 +1,15 @@
1
- const CLASS_SCHEMA = new WeakMap();
2
- const CONSTRUCTOR_BY_NAME = new Map();
3
- const ENUM_SCHEMA_BY_NAME = new Map();
4
- const ENUM_SCHEMA_BY_OBJECT = new WeakMap();
5
- const STAGE3_FIELD_METADATA = Symbol("carbonenginejs.schema.stage3Fields");
1
+ const CLASS_SCHEMA = new WeakMap();
2
+
3
+ // Exported schemas, memoized per class. SCHEMA_GENERATION is bumped by every
4
+ // metadata definition (see getOrCreateClassSchema), which is what makes a stale
5
+ // memo detectable without tracking which subclasses a base class change reaches.
6
+ const SCHEMA_EXPORTS = new WeakMap();
7
+ let SCHEMA_GENERATION = 0;
8
+
9
+ const CONSTRUCTOR_BY_NAME = new Map();
10
+ const ENUM_SCHEMA_BY_NAME = new Map();
11
+ const ENUM_SCHEMA_BY_OBJECT = new WeakMap();
12
+ const STAGE3_FIELD_METADATA = Symbol("carbonenginejs.schema.stage3Fields");
6
13
 
7
14
  export const CJS_ENUM_NAME = Symbol.for("carbonenginejs.enum.name");
8
15
 
@@ -56,32 +63,32 @@ export class CjsSchema
56
63
  return this;
57
64
  }
58
65
 
59
- static defineMethod(Constructor, methodName, namespace, value)
60
- {
61
- defineMethodMetadata(Constructor, methodName, namespace, value);
62
- return this;
63
- }
64
-
65
- static getField(Constructor, fieldName)
66
- {
67
- return getEffectiveFields(Constructor).find(field => field.name === fieldName) || null;
68
- }
69
-
70
- /**
71
- * Excludes named inherited fields from the decorated class's schema surface.
72
- */
73
- static hideInherited(fieldNames)
74
- {
75
- return hiddenInheritedFieldsDecorator(normalizeHiddenInheritedFields(fieldNames));
76
- }
77
-
78
- /**
79
- * Checks whether a field is hidden from a class by its inheritance chain.
80
- */
81
- static isFieldHidden(Constructor, fieldName)
82
- {
83
- return getHiddenInheritedFieldNames(Constructor).has(fieldName);
84
- }
66
+ static defineMethod(Constructor, methodName, namespace, value)
67
+ {
68
+ defineMethodMetadata(Constructor, methodName, namespace, value);
69
+ return this;
70
+ }
71
+
72
+ static getField(Constructor, fieldName)
73
+ {
74
+ return getEffectiveFields(Constructor).find(field => field.name === fieldName) || null;
75
+ }
76
+
77
+ /**
78
+ * Excludes named inherited fields from the decorated class's schema surface.
79
+ */
80
+ static hideInherited(fieldNames)
81
+ {
82
+ return hiddenInheritedFieldsDecorator(normalizeHiddenInheritedFields(fieldNames));
83
+ }
84
+
85
+ /**
86
+ * Checks whether a field is hidden from a class by its inheritance chain.
87
+ */
88
+ static isFieldHidden(Constructor, fieldName)
89
+ {
90
+ return getHiddenInheritedFieldNames(Constructor).has(fieldName);
91
+ }
85
92
 
86
93
  static getMethod(Constructor, methodName)
87
94
  {
@@ -128,6 +135,9 @@ export class CjsSchema
128
135
  }
129
136
 
130
137
  CONSTRUCTOR_BY_NAME.set(name.trim(), Constructor);
138
+ // Buckets resolve class references by name, so a late registration
139
+ // changes how already-built schemas should have been bucketed.
140
+ SCHEMA_GENERATION += 1;
131
141
  return this;
132
142
  }
133
143
 
@@ -151,49 +161,40 @@ export class CjsSchema
151
161
  return name ? ENUM_SCHEMA_BY_NAME.get(name) || null : null;
152
162
  }
153
163
 
154
- static getSchema(Constructor, options = {})
155
- {
156
- const schema = CLASS_SCHEMA.get(Constructor);
157
- const namespaces = normalizeNamespaces(options.namespaces);
158
- const fields = [];
159
- const methods = [];
160
-
161
- for (const field of getEffectiveFields(Constructor))
162
- {
163
- fields.push(enrichEnumField(exportField(field, namespaces), Constructor));
164
- }
165
-
166
- for (const method of schema?.methods || [])
167
- {
168
- methods.push(exportField(method, namespaces));
169
- }
170
-
171
- const result = {
172
- className: CjsSchema.getClassName(Constructor),
173
- fields: Object.freeze(fields)
174
- };
175
-
176
- const family = schema?.family || CjsSchema.getClassFamily(Constructor);
177
- if (family)
178
- {
179
- result.family = family;
180
- }
181
-
182
- if (schema?.sourceClass && schema.sourceClass !== result.className)
183
- {
184
- result.sourceClass = schema.sourceClass;
185
- }
186
-
187
- if (schema?.aliases?.length)
188
- {
189
- result.aliases = Object.freeze([...schema.aliases]);
190
- }
164
+ /**
165
+ * Return the exported schema for a class.
166
+ *
167
+ * The schema is the precomputed answer - collapsing the inheritance
168
+ * lineage and merging metadata - so building it per call would defeat its
169
+ * purpose. Callers traverse model graphs and ask once per node, so this is
170
+ * memoized per class and rebuilt only when class metadata is defined.
171
+ *
172
+ * Namespace-filtered exports are not memoized: they are a projection of the
173
+ * full schema requested by tooling, not the hot read path.
174
+ *
175
+ * The result is shared, not copied - treat it as read-only. It is not
176
+ * frozen: deep-cloning and freezing every field on the way out cost far
177
+ * more than the mistakes it guarded against.
178
+ *
179
+ * @param {Function} Constructor
180
+ * @param {object} [options={}]
181
+ * @param {string|Array<string>} [options.namespaces] Restricts exported metadata namespaces.
182
+ * @returns {object} Shared schema export.
183
+ */
184
+ static getSchema(Constructor, options = {})
185
+ {
186
+ const namespaces = normalizeNamespaces(options.namespaces);
187
+ if (namespaces) return buildSchema(Constructor, namespaces);
191
188
 
192
- if (methods.length) result.methods = Object.freeze(methods);
189
+ const memo = SCHEMA_EXPORTS.get(Constructor);
190
+ if (memo && memo.generation === SCHEMA_GENERATION) return memo.schema;
193
191
 
194
- return Object.freeze(result);
192
+ const schema = buildSchema(Constructor, null);
193
+ SCHEMA_EXPORTS.set(Constructor, { generation: SCHEMA_GENERATION, schema });
194
+ return schema;
195
195
  }
196
196
 
197
+
197
198
  static type = Object.freeze({
198
199
  array: itemType => fieldDecorator("type", { kind: "array", itemType }),
199
200
  boolean: fieldDecorator("type", { kind: "boolean" }),
@@ -338,17 +339,17 @@ function createComponentsNamespace()
338
339
  return Object.freeze(components);
339
340
  }
340
341
 
341
- function fieldDecorator(namespace, value)
342
- {
343
- return function schemaFieldDecorator(targetOrValue, contextOrFieldName)
344
- {
345
- if (contextOrFieldName && typeof contextOrFieldName === "object")
346
- {
347
- const context = contextOrFieldName;
348
- if (context.kind !== "field") throw new TypeError("CjsSchema decorators only support class fields.");
349
- recordStage3FieldMetadata(context, namespace, value);
350
-
351
- // Register field metadata on instance construction. addInitializer covers
342
+ function fieldDecorator(namespace, value)
343
+ {
344
+ return function schemaFieldDecorator(targetOrValue, contextOrFieldName)
345
+ {
346
+ if (contextOrFieldName && typeof contextOrFieldName === "object")
347
+ {
348
+ const context = contextOrFieldName;
349
+ if (context.kind !== "field") throw new TypeError("CjsSchema decorators only support class fields.");
350
+ recordStage3FieldMetadata(context, namespace, value);
351
+
352
+ // Register field metadata on instance construction. addInitializer covers
352
353
  // spec-compliant runtimes; the returned field initializer covers runtimes (e.g.
353
354
  // Deno/SWC) that do NOT fire field-decorator addInitializer. Both register the same
354
355
  // metadata (idempotent via mergeNamespace), so whichever the runtime honours, the
@@ -376,44 +377,44 @@ function fieldDecorator(namespace, value)
376
377
  };
377
378
  }
378
379
 
379
- function classDefinitionDecorator(definition)
380
- {
381
- return function schemaClassDefinitionDecorator(value, context)
382
- {
383
- if (context && typeof context === "object")
384
- {
385
- if (context.kind !== "class") throw new TypeError("CjsSchema type.define only supports classes.");
386
- registerStage3FieldMetadata(value, context.metadata);
387
- defineClassMetadata(value, normalizeClassDefinition(value, definition));
388
- return;
389
- }
380
+ function classDefinitionDecorator(definition)
381
+ {
382
+ return function schemaClassDefinitionDecorator(value, context)
383
+ {
384
+ if (context && typeof context === "object")
385
+ {
386
+ if (context.kind !== "class") throw new TypeError("CjsSchema type.define only supports classes.");
387
+ registerStage3FieldMetadata(value, context.metadata);
388
+ defineClassMetadata(value, normalizeClassDefinition(value, definition));
389
+ return;
390
+ }
390
391
 
391
392
  if (typeof value !== "function")
392
393
  {
393
394
  throw new TypeError("CjsSchema type.define requires a class constructor.");
394
395
  }
395
396
 
396
- defineClassMetadata(value, normalizeClassDefinition(value, definition));
397
- };
398
- }
399
-
400
- function hiddenInheritedFieldsDecorator(fieldNames)
401
- {
402
- return function schemaHiddenInheritedFieldsDecorator(value, context)
403
- {
404
- if (context && typeof context === "object")
405
- {
406
- if (context.kind !== "class") throw new TypeError("CjsSchema.hideInherited only supports classes.");
407
- registerStage3FieldMetadata(value, context.metadata);
408
- }
409
- else if (typeof value !== "function")
410
- {
411
- throw new TypeError("CjsSchema.hideInherited requires a class constructor.");
412
- }
413
-
414
- defineHiddenInheritedFields(value, fieldNames);
415
- };
416
- }
397
+ defineClassMetadata(value, normalizeClassDefinition(value, definition));
398
+ };
399
+ }
400
+
401
+ function hiddenInheritedFieldsDecorator(fieldNames)
402
+ {
403
+ return function schemaHiddenInheritedFieldsDecorator(value, context)
404
+ {
405
+ if (context && typeof context === "object")
406
+ {
407
+ if (context.kind !== "class") throw new TypeError("CjsSchema.hideInherited only supports classes.");
408
+ registerStage3FieldMetadata(value, context.metadata);
409
+ }
410
+ else if (typeof value !== "function")
411
+ {
412
+ throw new TypeError("CjsSchema.hideInherited requires a class constructor.");
413
+ }
414
+
415
+ defineHiddenInheritedFields(value, fieldNames);
416
+ };
417
+ }
417
418
 
418
419
  const CONTEXT_FIRST_PARAMETER = /^\(?\s*_?(context|updateContext)\b/;
419
420
 
@@ -523,10 +524,10 @@ function defineMethodMetadata(Constructor, methodName, namespace, value)
523
524
  defineMemberMetadata(Constructor, "methods", "methodsByName", methodName, namespace, value);
524
525
  }
525
526
 
526
- function defineMemberMetadata(Constructor, listKey, mapKey, name, namespace, value)
527
- {
528
- const schema = getOrCreateClassSchema(Constructor);
529
- let item = schema[mapKey].get(name);
527
+ function defineMemberMetadata(Constructor, listKey, mapKey, name, namespace, value)
528
+ {
529
+ const schema = getOrCreateClassSchema(Constructor);
530
+ let item = schema[mapKey].get(name);
530
531
 
531
532
  if (!item)
532
533
  {
@@ -534,188 +535,307 @@ function defineMemberMetadata(Constructor, listKey, mapKey, name, namespace, val
534
535
  schema[listKey].push(item);
535
536
  schema[mapKey].set(name, item);
536
537
  }
537
-
538
- item[namespace] = mergeNamespace(item[namespace], value);
539
- }
540
-
541
- function defineHiddenInheritedFields(Constructor, fieldNames)
542
- {
543
- if (typeof Constructor !== "function")
544
- {
545
- throw new TypeError("CjsSchema.hideInherited requires a class constructor.");
546
- }
547
-
548
- const Parent = Object.getPrototypeOf(Constructor);
549
- const inheritedFields = new Set(getEffectiveFields(Parent).map(field => field.name));
550
- const className = CLASS_SCHEMA.get(Constructor)?.className || Constructor.name || "<anonymous>";
551
-
552
- for (const fieldName of fieldNames)
553
- {
554
- if (!inheritedFields.has(fieldName))
555
- {
556
- throw new TypeError(
557
- `CjsSchema.hideInherited cannot hide "${fieldName}" on ${className}: ` +
558
- "the parent schema does not expose that field."
559
- );
560
- }
561
- }
562
-
563
- const schema = getOrCreateClassSchema(Constructor);
564
- for (const fieldName of fieldNames)
565
- {
566
- schema.hiddenInherited.add(fieldName);
567
- }
568
- }
569
-
570
- function getEffectiveFields(Constructor)
571
- {
572
- const ordered = [];
573
- const byName = new Map();
574
- const hidden = new Set();
575
-
576
- for (const current of getSchemaLineage(Constructor))
577
- {
578
- const schema = CLASS_SCHEMA.get(current);
579
- for (const field of schema?.fields || [])
580
- {
581
- const existing = byName.get(field.name);
582
- if (existing)
583
- {
584
- mergeMemberMetadata(existing, field);
585
- }
586
- else
587
- {
588
- const merged = mergeMemberMetadata({ name: field.name }, field);
589
- ordered.push(merged);
590
- byName.set(field.name, merged);
591
- }
592
- }
593
-
594
- for (const fieldName of schema?.hiddenInherited || [])
595
- {
596
- hidden.add(fieldName);
597
- }
598
- }
599
-
600
- return ordered.filter(field => !hidden.has(field.name));
601
- }
602
-
603
- function getHiddenInheritedFieldNames(Constructor)
604
- {
605
- const hidden = new Set();
606
- for (const current of getSchemaLineage(Constructor))
607
- {
608
- for (const fieldName of CLASS_SCHEMA.get(current)?.hiddenInherited || [])
609
- {
610
- hidden.add(fieldName);
611
- }
612
- }
613
- return hidden;
614
- }
615
-
616
- function getSchemaLineage(Constructor)
617
- {
618
- const lineage = [];
619
- let current = Constructor;
620
- while (typeof current === "function")
621
- {
622
- if (CLASS_SCHEMA.has(current)) lineage.push(current);
623
- current = Object.getPrototypeOf(current);
624
- }
625
- return lineage.reverse();
626
- }
627
-
628
- function mergeMemberMetadata(target, source)
629
- {
630
- for (const [namespace, value] of Object.entries(source))
631
- {
632
- if (namespace === "name") continue;
633
- target[namespace] = mergeNamespace(target[namespace], value);
634
- }
635
- return target;
636
- }
637
-
638
- function getOrCreateClassSchema(Constructor)
639
- {
640
- let schema = CLASS_SCHEMA.get(Constructor);
641
- if (!schema)
538
+
539
+ item[namespace] = mergeNamespace(item[namespace], value);
540
+ }
541
+
542
+ function defineHiddenInheritedFields(Constructor, fieldNames)
543
+ {
544
+ if (typeof Constructor !== "function")
545
+ {
546
+ throw new TypeError("CjsSchema.hideInherited requires a class constructor.");
547
+ }
548
+
549
+ const Parent = Object.getPrototypeOf(Constructor);
550
+ const inheritedFields = new Set(getEffectiveFields(Parent).map(field => field.name));
551
+ // Declared name only: Constructor.name does not survive minification, and a
552
+ // mangled name in an error reads as a real one and sends you chasing it.
553
+ const className = CLASS_SCHEMA.get(Constructor)?.className || "<undeclared>";
554
+
555
+ for (const fieldName of fieldNames)
556
+ {
557
+ if (!inheritedFields.has(fieldName))
558
+ {
559
+ throw new TypeError(
560
+ `CjsSchema.hideInherited cannot hide "${fieldName}" on ${className}: ` +
561
+ "the parent schema does not expose that field."
562
+ );
563
+ }
564
+ }
565
+
566
+ const schema = getOrCreateClassSchema(Constructor);
567
+ for (const fieldName of fieldNames)
568
+ {
569
+ schema.hiddenInherited.add(fieldName);
570
+ }
571
+ }
572
+
573
+ function getEffectiveFields(Constructor)
574
+ {
575
+ const ordered = [];
576
+ const byName = new Map();
577
+ const hidden = new Set();
578
+
579
+ for (const current of getSchemaLineage(Constructor))
580
+ {
581
+ const schema = CLASS_SCHEMA.get(current);
582
+ for (const field of schema?.fields || [])
583
+ {
584
+ const existing = byName.get(field.name);
585
+ if (existing)
586
+ {
587
+ mergeMemberMetadata(existing, field);
588
+ }
589
+ else
590
+ {
591
+ const merged = mergeMemberMetadata({ name: field.name }, field);
592
+ ordered.push(merged);
593
+ byName.set(field.name, merged);
594
+ }
595
+ }
596
+
597
+ for (const fieldName of schema?.hiddenInherited || [])
598
+ {
599
+ hidden.add(fieldName);
600
+ }
601
+ }
602
+
603
+ return ordered.filter(field => !hidden.has(field.name));
604
+ }
605
+
606
+ function getHiddenInheritedFieldNames(Constructor)
607
+ {
608
+ const hidden = new Set();
609
+ for (const current of getSchemaLineage(Constructor))
610
+ {
611
+ for (const fieldName of CLASS_SCHEMA.get(current)?.hiddenInherited || [])
612
+ {
613
+ hidden.add(fieldName);
614
+ }
615
+ }
616
+ return hidden;
617
+ }
618
+
619
+ function getSchemaLineage(Constructor)
620
+ {
621
+ const lineage = [];
622
+ let current = Constructor;
623
+ while (typeof current === "function")
624
+ {
625
+ if (CLASS_SCHEMA.has(current)) lineage.push(current);
626
+ current = Object.getPrototypeOf(current);
627
+ }
628
+ return lineage.reverse();
629
+ }
630
+
631
+ function mergeMemberMetadata(target, source)
632
+ {
633
+ for (const [namespace, value] of Object.entries(source))
634
+ {
635
+ if (namespace === "name") continue;
636
+ target[namespace] = mergeNamespace(target[namespace], value);
637
+ }
638
+ return target;
639
+ }
640
+
641
+ function buildSchema(Constructor, namespaces)
642
+ {
643
+ const schema = CLASS_SCHEMA.get(Constructor);
644
+ const fields = [];
645
+ const methods = [];
646
+
647
+ for (const field of getEffectiveFields(Constructor))
648
+ {
649
+ fields.push(enrichEnumField(exportField(field, namespaces), Constructor));
650
+ }
651
+
652
+ for (const method of schema?.methods || [])
653
+ {
654
+ methods.push(exportField(method, namespaces));
655
+ }
656
+
657
+ const result = {
658
+ className: CjsSchema.getClassName(Constructor),
659
+ fields
660
+ };
661
+
662
+ const family = schema?.family || CjsSchema.getClassFamily(Constructor);
663
+ if (family)
664
+ {
665
+ result.family = family;
666
+ }
667
+
668
+ if (schema?.sourceClass && schema.sourceClass !== result.className)
669
+ {
670
+ result.sourceClass = schema.sourceClass;
671
+ }
672
+
673
+ if (schema?.aliases?.length)
674
+ {
675
+ result.aliases = [ ...schema.aliases ];
676
+ }
677
+
678
+ if (methods.length) result.methods = methods;
679
+
680
+ addSchemaBuckets(result);
681
+ return result;
682
+ }
683
+
684
+
685
+ // Kinds that hold many values rather than one. `map` and `set` are included
686
+ // because they are iterable collections of the referenced class, same as a list.
687
+ const MANY_KINDS = new Set([ "list", "array", "set", "map" ]);
688
+
689
+ // Kinds that can hold a child model. Bucketing on the KIND rather than on a
690
+ // resolvable class reference keeps traversal conservative: `type.struct(Class)`
691
+ // drops the reference during normalization, and a raw defineField may omit the
692
+ // item type, so requiring a reference would silently stop visiting those.
693
+ // Scalar and math kinds are excluded, which is where the saving comes from.
694
+ const MODEL_KINDS = new Set([
695
+ "struct", "model", "rawStruct", "objectRef", "unknown",
696
+ "list", "array", "set", "map"
697
+ ]);
698
+
699
+
700
+ /**
701
+ * Precompute the answers consumers would otherwise recompute per traversal.
702
+ *
703
+ * Graph walks ask the same two questions of every node - which fields hold
704
+ * child models, which hold resources - and answering them by scanning the field
705
+ * list and type-testing each value costs more than the walk itself. The class
706
+ * cannot change without rebuilding its schema, so this is solved once.
707
+ */
708
+ function addSchemaBuckets(schema)
709
+ {
710
+ const byName = new Map();
711
+ const children = [];
712
+ const resources = [];
713
+
714
+ for (const field of schema.fields)
715
+ {
716
+ byName.set(field.name, field);
717
+
718
+ const type = field.type;
719
+ // An undeclared field could hold anything, so it stays traversable.
720
+ const kind = type?.kind;
721
+ if (kind && !MODEL_KINDS.has(kind)) continue;
722
+
723
+ const entry = { name: field.name, many: MANY_KINDS.has(kind) };
724
+
725
+ if (type && resolveFieldClass(type)?.isResource === true)
726
+ {
727
+ resources.push(entry);
728
+ continue;
729
+ }
730
+
731
+ entry.owned = field.io?.ownership === "owned";
732
+ children.push(entry);
733
+ }
734
+
735
+ schema.byName = byName;
736
+ schema.children = children;
737
+ schema.resources = resources;
738
+ }
739
+
740
+
741
+ // Only string references survive normalization - type.struct(SomeClass) drops
742
+ // the reference entirely - so a field can only be bucketed when it names a
743
+ // class. The one field in the tree that named nothing was a missing
744
+ // declaration, not a deliberate escape.
745
+ function resolveFieldClass(type)
746
+ {
747
+ const ref = type.className || type.itemType || type.valueType;
748
+ return typeof ref === "string" && ref ? CONSTRUCTOR_BY_NAME.get(ref) || null : null;
749
+ }
750
+
751
+
752
+ function getOrCreateClassSchema(Constructor)
753
+ {
754
+ // The only route by which class metadata is mutated, and therefore the only
755
+ // place exported schemas can go stale. A single global counter rather than
756
+ // per-class invalidation because a base class change invalidates every
757
+ // subclass, and lineage is not tracked in reverse.
758
+ SCHEMA_GENERATION += 1;
759
+
760
+ let schema = CLASS_SCHEMA.get(Constructor);
761
+ if (!schema)
642
762
  {
643
763
  schema = {
644
764
  className: null,
645
765
  family: null,
646
766
  sourceClass: null,
647
- aliases: null,
648
- fields: [],
649
- fieldsByName: new Map(),
650
- hiddenInherited: new Set(),
651
- methods: [],
652
- methodsByName: new Map()
653
- };
767
+ aliases: null,
768
+ fields: [],
769
+ fieldsByName: new Map(),
770
+ hiddenInherited: new Set(),
771
+ methods: [],
772
+ methodsByName: new Map()
773
+ };
654
774
  CLASS_SCHEMA.set(Constructor, schema);
655
775
  }
656
- return schema;
657
- }
658
-
659
- function normalizeHiddenInheritedFields(fieldNames)
660
- {
661
- if (!Array.isArray(fieldNames) || !fieldNames.length)
662
- {
663
- throw new TypeError("CjsSchema.hideInherited requires a non-empty array of field names.");
664
- }
665
-
666
- const normalized = fieldNames.map((fieldName, index) =>
667
- {
668
- if (typeof fieldName !== "string" || !fieldName.trim())
669
- {
670
- throw new TypeError(`CjsSchema.hideInherited fieldNames[${index}] must be a non-empty string.`);
671
- }
672
- return fieldName.trim();
673
- });
674
-
675
- return Object.freeze([...new Set(normalized)]);
676
- }
677
-
678
- function recordStage3FieldMetadata(context, namespace, value)
679
- {
680
- const metadata = context?.metadata;
681
- if (!metadata || typeof metadata !== "object") return;
682
-
683
- let fields;
684
- if (Object.prototype.hasOwnProperty.call(metadata, STAGE3_FIELD_METADATA))
685
- {
686
- fields = metadata[STAGE3_FIELD_METADATA];
687
- }
688
- else
689
- {
690
- fields = [];
691
- Object.defineProperty(metadata, STAGE3_FIELD_METADATA, {
692
- configurable: false,
693
- enumerable: false,
694
- value: fields,
695
- writable: false
696
- });
697
- }
698
-
699
- fields.push({
700
- name: context.name,
701
- namespace,
702
- value
703
- });
704
- }
705
-
706
- function registerStage3FieldMetadata(Constructor, metadata)
707
- {
708
- if (!metadata || typeof metadata !== "object") return;
709
- if (!Object.prototype.hasOwnProperty.call(metadata, STAGE3_FIELD_METADATA)) return;
710
-
711
- for (const field of metadata[STAGE3_FIELD_METADATA])
712
- {
713
- defineFieldMetadata(Constructor, field.name, field.namespace, field.value);
714
- }
715
- }
716
-
717
- function normalizeClassDefinition(Constructor, definition)
718
- {
776
+ return schema;
777
+ }
778
+
779
+ function normalizeHiddenInheritedFields(fieldNames)
780
+ {
781
+ if (!Array.isArray(fieldNames) || !fieldNames.length)
782
+ {
783
+ throw new TypeError("CjsSchema.hideInherited requires a non-empty array of field names.");
784
+ }
785
+
786
+ const normalized = fieldNames.map((fieldName, index) =>
787
+ {
788
+ if (typeof fieldName !== "string" || !fieldName.trim())
789
+ {
790
+ throw new TypeError(`CjsSchema.hideInherited fieldNames[${index}] must be a non-empty string.`);
791
+ }
792
+ return fieldName.trim();
793
+ });
794
+
795
+ return Object.freeze([...new Set(normalized)]);
796
+ }
797
+
798
+ function recordStage3FieldMetadata(context, namespace, value)
799
+ {
800
+ const metadata = context?.metadata;
801
+ if (!metadata || typeof metadata !== "object") return;
802
+
803
+ let fields;
804
+ if (Object.prototype.hasOwnProperty.call(metadata, STAGE3_FIELD_METADATA))
805
+ {
806
+ fields = metadata[STAGE3_FIELD_METADATA];
807
+ }
808
+ else
809
+ {
810
+ fields = [];
811
+ Object.defineProperty(metadata, STAGE3_FIELD_METADATA, {
812
+ configurable: false,
813
+ enumerable: false,
814
+ value: fields,
815
+ writable: false
816
+ });
817
+ }
818
+
819
+ fields.push({
820
+ name: context.name,
821
+ namespace,
822
+ value
823
+ });
824
+ }
825
+
826
+ function registerStage3FieldMetadata(Constructor, metadata)
827
+ {
828
+ if (!metadata || typeof metadata !== "object") return;
829
+ if (!Object.prototype.hasOwnProperty.call(metadata, STAGE3_FIELD_METADATA)) return;
830
+
831
+ for (const field of metadata[STAGE3_FIELD_METADATA])
832
+ {
833
+ defineFieldMetadata(Constructor, field.name, field.namespace, field.value);
834
+ }
835
+ }
836
+
837
+ function normalizeClassDefinition(Constructor, definition)
838
+ {
719
839
  if (typeof definition === "string")
720
840
  {
721
841
  definition = { className: definition };
@@ -937,9 +1057,8 @@ function componentIndex(char)
937
1057
 
938
1058
  function mergeNamespace(existing, value)
939
1059
  {
940
- if (!existing) return cloneSchemaValue(value);
941
- if (isPlainObject(existing) && isPlainObject(value)) return Object.freeze({ ...existing, ...value });
942
- return cloneSchemaValue(value);
1060
+ if (isPlainObject(existing) && isPlainObject(value)) return { ...existing, ...value };
1061
+ return value;
943
1062
  }
944
1063
 
945
1064
  // Resolves @schema.enum("X") through the owning class's PascalCase static so
@@ -984,10 +1103,10 @@ function exportField(field, namespaces)
984
1103
  {
985
1104
  if (key === "name") continue;
986
1105
  if (namespaces && !namespaces.has(key)) continue;
987
- result[key] = cloneSchemaValue(value);
1106
+ result[key] = value;
988
1107
  }
989
1108
 
990
- return Object.freeze(result);
1109
+ return result;
991
1110
  }
992
1111
 
993
1112
  function normalizeNamespaces(namespaces)
package/src/vec3.js CHANGED
@@ -559,6 +559,21 @@ vec3.isEmpty = function (a)
559
559
  return a[0] === 0 && a[1] === 0 && a[2] === 0;
560
560
  };
561
561
 
562
+ /**
563
+ * The largest of the three components.
564
+ *
565
+ * A reduction to one number, unlike `max`, which is the component-wise maximum
566
+ * of two vectors. Carbon spells it `MaxVectorComponent`, and uses it to reduce a
567
+ * colour to the single value that decides how bright it counts as.
568
+ *
569
+ * @param {vec3} a
570
+ * @returns {Number}
571
+ */
572
+ vec3.maxComponent = function (a)
573
+ {
574
+ return Math.max(a[0], a[1], a[2]);
575
+ };
576
+
562
577
  /**
563
578
  * Multiplies a vec3 by a scalar
564
579
  *
@@ -1097,6 +1112,7 @@ export const {
1097
1112
  length,
1098
1113
  lerp,
1099
1114
  max,
1115
+ maxComponent,
1100
1116
  min,
1101
1117
  mul,
1102
1118
  multiply,