circuitjson-toolkit 1.0.16 → 1.1.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.
Files changed (155) hide show
  1. package/AGENTS.md +15 -0
  2. package/NOTICE.md +31 -0
  3. package/README.md +267 -107
  4. package/docs/api.md +501 -96
  5. package/docs/capabilities.md +70 -0
  6. package/docs/migration/behaviors.md +45 -0
  7. package/docs/migration/parser.md +60 -0
  8. package/docs/migration/renderers.md +515 -0
  9. package/docs/migration/root.md +740 -0
  10. package/docs/migration.md +120 -0
  11. package/docs/model-format.md +175 -57
  12. package/docs/provenance.md +206 -0
  13. package/docs/release-notes-v1.1.0.md +154 -0
  14. package/docs/testing.md +117 -7
  15. package/package.json +31 -5
  16. package/spec/api-baseline-v1.0.17.json +1 -0
  17. package/spec/baseline-provenance-v1.0.17.json +7 -0
  18. package/spec/circuitjson-schema-snapshot.json +321 -0
  19. package/spec/circuitjson-schema-source.json +28 -0
  20. package/spec/feature-preservation.json +1 -0
  21. package/spec/library-scope.md +27 -20
  22. package/src/capabilities.mjs +1 -0
  23. package/src/core/ArchiveEntryPath.mjs +93 -0
  24. package/src/core/ArchiveLimits.mjs +31 -0
  25. package/src/core/ArchiveLimitsValidator.mjs +107 -0
  26. package/src/core/AsyncInputOwnership.mjs +56 -0
  27. package/src/core/AttachedValueLimits.mjs +67 -0
  28. package/src/core/CircuitJsonDiagnosticIndexer.mjs +184 -0
  29. package/src/core/CircuitJsonDocument.mjs +19 -61
  30. package/src/core/CircuitJsonElementTypes.mjs +10 -0
  31. package/src/core/CircuitJsonElementValidator.mjs +98 -847
  32. package/src/core/CircuitJsonIndexer.mjs +274 -194
  33. package/src/core/CircuitJsonManufacturingBuilder.mjs +167 -164
  34. package/src/core/CircuitJsonParser.mjs +75 -13
  35. package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +12 -6
  36. package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +108 -10
  37. package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +1 -1
  38. package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +44 -38
  39. package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +146 -28
  40. package/src/core/CircuitJsonPcbPrimitiveFields.mjs +70 -4
  41. package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +18 -2
  42. package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +26 -9
  43. package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +7 -6
  44. package/src/core/CircuitJsonSerializedInputAudit.mjs +87 -0
  45. package/src/core/CircuitJsonSourceMetadata.mjs +5 -1
  46. package/src/core/CircuitJsonSupportMatrixBuilder.mjs +3 -1
  47. package/src/core/CircuitJsonToolkitElementSchema.mjs +218 -0
  48. package/src/core/CircuitJsonUnitParsers.mjs +101 -0
  49. package/src/core/CircuitJsonUnits.mjs +13 -87
  50. package/src/core/CircuitJsonUpstreamSchema.mjs +9 -0
  51. package/src/core/CircuitJsonUpstreamValidator.mjs +418 -0
  52. package/src/core/CircuitJsonValidationUnits.mjs +6 -0
  53. package/src/core/ManufacturingService.mjs +323 -0
  54. package/src/core/Parser.mjs +343 -0
  55. package/src/core/ParserOptions.mjs +333 -0
  56. package/src/core/PcbBoundsSelectionModel.mjs +55 -19
  57. package/src/core/PcbDiagnosticFocusModel.mjs +42 -11
  58. package/src/core/PcbInteractionIndex.mjs +368 -0
  59. package/src/core/PcbInteractionPrimitiveModel.mjs +393 -62
  60. package/src/core/ProjectAsyncInputOwner.mjs +70 -0
  61. package/src/core/ProjectLoader.mjs +975 -0
  62. package/src/core/SimulationService.mjs +790 -0
  63. package/src/core/ToolkitCapabilities.mjs +130 -0
  64. package/src/core/ZipArchiveInspector.mjs +649 -0
  65. package/src/core/context/BinaryDataSnapshot.mjs +217 -0
  66. package/src/core/context/CircuitJsonContextIndexes.mjs +96 -0
  67. package/src/core/context/CircuitJsonDerivedCache.mjs +114 -0
  68. package/src/core/context/CircuitJsonDocumentContext.mjs +353 -0
  69. package/src/core/context/CircuitJsonLegacyModel.mjs +147 -0
  70. package/src/core/context/CircuitJsonLegacyNormalizer.mjs +847 -0
  71. package/src/core/context/CircuitJsonMetadataBoundary.mjs +76 -0
  72. package/src/core/context/CircuitJsonModelFreezeTraversal.mjs +179 -0
  73. package/src/core/context/CircuitJsonReadOnlyDocument.mjs +920 -0
  74. package/src/core/context/CircuitJsonSchematicTableNormalizer.mjs +314 -0
  75. package/src/core/context/CircuitJsonValidationAuthority.mjs +39 -0
  76. package/src/core/context/CircuitJsonValidationProof.mjs +217 -0
  77. package/src/core/context/PcbPrimitivePreparation.mjs +198 -0
  78. package/src/core/context/PcbSpatialIndex.mjs +701 -0
  79. package/src/core/context/ProtectedExtensionBinaryBoundary.mjs +128 -0
  80. package/src/core/context/StructuredDataSnapshot.mjs +683 -0
  81. package/src/core/contracts/DocumentResult.mjs +198 -0
  82. package/src/core/contracts/ProjectResult.mjs +96 -0
  83. package/src/core/contracts/RuntimeProxyBoundary.mjs +48 -0
  84. package/src/core/contracts/ToolkitAsset.mjs +493 -0
  85. package/src/core/contracts/ToolkitDiagnostic.mjs +38 -0
  86. package/src/core/contracts/ToolkitError.mjs +176 -0
  87. package/src/core/contracts/ToolkitProgress.mjs +89 -0
  88. package/src/core/interaction/CanonicalInteractionOptions.mjs +246 -0
  89. package/src/core/interaction/PcbInteractionBounds.mjs +167 -0
  90. package/src/core/query/CircuitTraversal.mjs +343 -0
  91. package/src/core/query/ComponentGrouping.mjs +275 -0
  92. package/src/core/query/QueryNetlistBuilder.mjs +306 -0
  93. package/src/core/query/QueryService.mjs +435 -0
  94. package/src/core/query/RegexPattern.mjs +75 -0
  95. package/src/core/rendering/CanonicalBomOrder.mjs +81 -0
  96. package/src/core/rendering/CanonicalBomRows.mjs +92 -0
  97. package/src/core/rendering/CanonicalRenderOptions.mjs +498 -0
  98. package/src/core/rendering/CanonicalSvgDocument.mjs +102 -0
  99. package/src/core/rendering/PcbRenderPlan.mjs +429 -0
  100. package/src/core/rendering/SchematicSheetSelector.mjs +335 -0
  101. package/src/core/scene3d/PcbScene3dBuilder.mjs +906 -0
  102. package/src/core/scene3d/PcbScene3dPreparator.mjs +47 -0
  103. package/src/core/scene3d/Scene3dAssetIndex.mjs +284 -0
  104. package/src/core/scene3d/Scene3dBoardModel.mjs +596 -0
  105. package/src/core/scene3d/Scene3dDocumentMetadata.mjs +167 -0
  106. package/src/core/scene3d/Scene3dFreeze.mjs +37 -0
  107. package/src/core/scene3d/Scene3dIdRegistry.mjs +34 -0
  108. package/src/core/scene3d/Scene3dInputPreflight.mjs +193 -0
  109. package/src/core/scene3d/Scene3dMaterials.mjs +58 -0
  110. package/src/core/scene3d/Scene3dModelReference.mjs +134 -0
  111. package/src/core/scene3d/Scene3dOptions.mjs +363 -0
  112. package/src/core/scene3d/SceneAssetResolver.mjs +441 -0
  113. package/src/core/simulation/SimulationParameterCloner.mjs +543 -0
  114. package/src/core/worker/ParserWorkerClient.mjs +997 -0
  115. package/src/core/worker/ToolkitWorkerProtocol.mjs +412 -0
  116. package/src/core/worker/WorkerRequestData.mjs +623 -0
  117. package/src/core/worker/WorkerResponseData.mjs +905 -0
  118. package/src/extensions.mjs +37 -0
  119. package/src/index.mjs +14 -9
  120. package/src/interaction.mjs +2 -0
  121. package/src/manufacturing.mjs +1 -0
  122. package/src/parser.mjs +12 -2
  123. package/src/project.mjs +5 -0
  124. package/src/query.mjs +1 -0
  125. package/src/renderers.mjs +3 -29
  126. package/src/scene3d.mjs +3 -0
  127. package/src/simulation.mjs +1 -0
  128. package/src/styles/renderers.css +24 -0
  129. package/src/testing/ToolkitContractFixtures.mjs +124 -0
  130. package/src/testing/ToolkitLoopbackWorker.mjs +174 -0
  131. package/src/testing/runToolkitContract.mjs +705 -0
  132. package/src/testing.mjs +3 -0
  133. package/src/ui/BomTableRenderer.mjs +304 -0
  134. package/src/ui/CircuitJsonPcbBoardSvgRenderer.mjs +80 -0
  135. package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +3 -5
  136. package/src/ui/CircuitJsonPcbSvgRenderer.mjs +63 -43
  137. package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +3 -5
  138. package/src/ui/CircuitJsonSchematicDebugRenderer.mjs +164 -0
  139. package/src/ui/CircuitJsonSchematicImageSvgRenderer.mjs +210 -0
  140. package/src/ui/CircuitJsonSchematicLineRenderer.mjs +86 -0
  141. package/src/ui/CircuitJsonSchematicSheetSymbolSvgRenderer.mjs +98 -0
  142. package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +117 -17
  143. package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +67 -20
  144. package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +45 -9
  145. package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +151 -148
  146. package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +4 -292
  147. package/src/ui/PcbSvgRenderer.mjs +41 -0
  148. package/src/ui/SafeSvgPaint.mjs +26 -0
  149. package/src/ui/SafeXmlText.mjs +60 -0
  150. package/src/ui/SchematicGeometryBounds.mjs +540 -0
  151. package/src/ui/SchematicSvgRenderer.mjs +110 -0
  152. package/src/ui/SchematicTableGeometry.mjs +319 -0
  153. package/src/ui/SchematicTextAnchor.mjs +55 -0
  154. package/src/ui/SchematicTextBounds.mjs +98 -0
  155. package/src/workers/parser.worker.mjs +59 -0
@@ -0,0 +1,441 @@
1
+ import { ToolkitAsset } from '../contracts/ToolkitAsset.mjs'
2
+ import { ToolkitError } from '../contracts/ToolkitError.mjs'
3
+ import { ToolkitProgress } from '../contracts/ToolkitProgress.mjs'
4
+ import { BinaryDataSnapshot } from '../context/BinaryDataSnapshot.mjs'
5
+ import { CircuitJsonReadOnlyDocument } from '../context/CircuitJsonReadOnlyDocument.mjs'
6
+ import { Scene3dOptions } from './Scene3dOptions.mjs'
7
+
8
+ /**
9
+ * Resolves scene assets only through an explicitly injected callback.
10
+ */
11
+ export class SceneAssetResolver {
12
+ /**
13
+ * Resolves a bounded asset list while preserving request order.
14
+ * @param {unknown} requests Asset request records.
15
+ * @param {unknown} [options] Resolution options.
16
+ * @returns {Promise<object[]>} Resolved canonical asset records.
17
+ */
18
+ static async resolveAll(requests, options = {}) {
19
+ const requestValues = Scene3dOptions.dataArray(
20
+ requests,
21
+ 'Scene assets',
22
+ Scene3dOptions.maxAssetCount
23
+ )
24
+ const normalized = Scene3dOptions.normalize(options)
25
+ Scene3dOptions.assertNotAborted(normalized.signal)
26
+ const capturedRequests = SceneAssetResolver.#preflightResident(
27
+ requestValues,
28
+ normalized
29
+ )
30
+ const assets = capturedRequests.map((request) =>
31
+ SceneAssetResolver.#request(request)
32
+ )
33
+ if (!assets.length) return []
34
+
35
+ let previous = SceneAssetResolver.#progress(
36
+ normalized,
37
+ {
38
+ stage: 'decode',
39
+ detail: 'scene-assets',
40
+ completed: 0,
41
+ total: assets.length
42
+ },
43
+ null
44
+ )
45
+ const results = new Array(assets.length)
46
+ let totalBytes = 0
47
+
48
+ for (
49
+ let start = 0;
50
+ start < assets.length;
51
+ start += normalized.resolveConcurrency
52
+ ) {
53
+ Scene3dOptions.assertNotAborted(normalized.signal)
54
+ const end = Math.min(
55
+ start + normalized.resolveConcurrency,
56
+ assets.length
57
+ )
58
+ const batch = []
59
+ for (let index = start; index < end; index += 1) {
60
+ batch.push(
61
+ SceneAssetResolver.#resolveOne(assets[index], normalized)
62
+ )
63
+ }
64
+ const resolvedBatch = await Promise.all(batch)
65
+ for (let offset = 0; offset < resolvedBatch.length; offset += 1) {
66
+ const index = start + offset
67
+ const resolved = resolvedBatch[offset]
68
+ const range =
69
+ typeof resolved.data === 'string'
70
+ ? null
71
+ : BinaryDataSnapshot.describe(resolved.data)
72
+ const byteLength = SceneAssetResolver.#byteLength(
73
+ resolved.data,
74
+ range
75
+ )
76
+ if (byteLength > normalized.maxAssetBytes) {
77
+ throw SceneAssetResolver.#limitError(
78
+ resolved.request,
79
+ byteLength,
80
+ normalized.maxAssetBytes,
81
+ 'asset'
82
+ )
83
+ }
84
+ if (totalBytes + byteLength > normalized.maxTotalAssetBytes) {
85
+ throw SceneAssetResolver.#limitError(
86
+ resolved.request,
87
+ totalBytes + byteLength,
88
+ normalized.maxTotalAssetBytes,
89
+ 'total'
90
+ )
91
+ }
92
+ const asset = SceneAssetResolver.#assetWithData(
93
+ resolved.request,
94
+ resolved.data,
95
+ range
96
+ )
97
+ totalBytes += byteLength
98
+ results[index] = asset
99
+ previous = SceneAssetResolver.#progress(
100
+ normalized,
101
+ {
102
+ stage: 'decode',
103
+ detail: 'scene-assets',
104
+ completed: index + 1,
105
+ total: assets.length
106
+ },
107
+ previous
108
+ )
109
+ Scene3dOptions.assertNotAborted(normalized.signal)
110
+ }
111
+ }
112
+
113
+ Scene3dOptions.assertNotAborted(normalized.signal)
114
+ SceneAssetResolver.#progress(
115
+ normalized,
116
+ {
117
+ stage: 'complete',
118
+ detail: 'scene-assets',
119
+ completed: assets.length,
120
+ total: assets.length
121
+ },
122
+ previous
123
+ )
124
+ Scene3dOptions.assertNotAborted(normalized.signal)
125
+ return results
126
+ }
127
+
128
+ /**
129
+ * Normalizes one public asset request without invoking accessors.
130
+ * @param {unknown} request Asset request candidate.
131
+ * @returns {object} Canonical asset record.
132
+ */
133
+ static #request(request) {
134
+ const fields = CircuitJsonReadOnlyDocument.copyAssetFields(request)
135
+ const asset = ToolkitAsset.create(fields)
136
+ if (!asset.id) {
137
+ throw new TypeError('Scene asset request id must not be empty.')
138
+ }
139
+ return asset
140
+ }
141
+
142
+ /**
143
+ * Bounds all resident request data before normalization can copy or encode it.
144
+ * @param {object[]} requests Descriptor-safe request records.
145
+ * @param {ReturnType<Scene3dOptions['normalize']>} options Normalized options.
146
+ * @returns {object[]} Atomically captured request records.
147
+ */
148
+ static #preflightResident(requests, options) {
149
+ const metadataBudget =
150
+ CircuitJsonReadOnlyDocument.createMetadataBudget()
151
+ let totalBytes = 0
152
+ const captured = new Array(requests.length)
153
+ for (let index = 0; index < requests.length; index += 1) {
154
+ const request = CircuitJsonReadOnlyDocument.captureAsset(
155
+ requests[index],
156
+ metadataBudget,
157
+ false,
158
+ (byteLength, identity) => {
159
+ if (byteLength > options.maxAssetBytes) {
160
+ throw SceneAssetResolver.#preflightLimitError(
161
+ identity,
162
+ byteLength,
163
+ options.maxAssetBytes,
164
+ 'asset'
165
+ )
166
+ }
167
+ if (totalBytes + byteLength > options.maxTotalAssetBytes) {
168
+ throw SceneAssetResolver.#preflightLimitError(
169
+ identity,
170
+ totalBytes + byteLength,
171
+ options.maxTotalAssetBytes,
172
+ 'total'
173
+ )
174
+ }
175
+ }
176
+ )
177
+ captured[index] = request
178
+ const byteLength =
179
+ CircuitJsonReadOnlyDocument.assetPayloadByteLength(
180
+ request,
181
+ metadataBudget
182
+ )
183
+ if (byteLength > options.maxAssetBytes) {
184
+ throw SceneAssetResolver.#candidateLimitError(
185
+ request,
186
+ byteLength,
187
+ options.maxAssetBytes,
188
+ 'asset'
189
+ )
190
+ }
191
+ totalBytes += byteLength
192
+ if (totalBytes > options.maxTotalAssetBytes) {
193
+ throw SceneAssetResolver.#candidateLimitError(
194
+ request,
195
+ totalBytes,
196
+ options.maxTotalAssetBytes,
197
+ 'total'
198
+ )
199
+ }
200
+ }
201
+ return captured
202
+ }
203
+
204
+ /**
205
+ * Creates a byte-limit error before payload or source metadata is copied.
206
+ * @param {{ id: unknown, name: unknown }} identity Captured primitive identity.
207
+ * @param {number} actual Actual resident bytes.
208
+ * @param {number} maximum Maximum bytes.
209
+ * @param {'asset' | 'total'} scope Limit scope.
210
+ * @returns {ToolkitError} Typed limit error.
211
+ */
212
+ static #preflightLimitError(identity, actual, maximum, scope) {
213
+ return SceneAssetResolver.#limitError(
214
+ {
215
+ id: SceneAssetResolver.#primitiveText(identity.id),
216
+ name: SceneAssetResolver.#primitiveText(identity.name)
217
+ },
218
+ actual,
219
+ maximum,
220
+ scope
221
+ )
222
+ }
223
+
224
+ /**
225
+ * Converts an already-validated primitive identity without object coercion.
226
+ * @param {unknown} value Primitive identity field.
227
+ * @returns {string} Text identity.
228
+ */
229
+ static #primitiveText(value) {
230
+ return value === null || value === undefined ? '' : String(value)
231
+ }
232
+
233
+ /**
234
+ * Creates a limit error from metadata-only request normalization.
235
+ * @param {object} request Asset request candidate.
236
+ * @param {number} actual Actual bytes.
237
+ * @param {number} maximum Maximum bytes.
238
+ * @param {'asset' | 'total'} scope Limit scope.
239
+ * @returns {ToolkitError} Typed limit error.
240
+ */
241
+ static #candidateLimitError(request, actual, maximum, scope) {
242
+ const metadata = ToolkitAsset.create(
243
+ CircuitJsonReadOnlyDocument.copyAssetMetadataFields(request)
244
+ )
245
+ return SceneAssetResolver.#limitError(metadata, actual, maximum, scope)
246
+ }
247
+
248
+ /**
249
+ * Resolves or copies one asset payload.
250
+ * @param {object} request Canonical asset request.
251
+ * @param {ReturnType<Scene3dOptions['normalize']>} options Normalized options.
252
+ * @returns {Promise<{ request: object, data: string | ArrayBuffer | ArrayBufferView }>} Resolved request and payload.
253
+ */
254
+ static async #resolveOne(request, options) {
255
+ Scene3dOptions.assertNotAborted(options.signal)
256
+ if (request.data !== null) {
257
+ return { request, data: request.data }
258
+ }
259
+ if (!options.resolveAsset) {
260
+ throw SceneAssetResolver.#missingData(request)
261
+ }
262
+
263
+ let pending
264
+ try {
265
+ pending = options.resolveAsset(
266
+ SceneAssetResolver.#resolverRequest(request),
267
+ { signal: options.signal }
268
+ )
269
+ } catch (error) {
270
+ throw new ToolkitError(
271
+ `Unable to resolve scene asset: ${request.id}.`,
272
+ {
273
+ code: 'ERR_ASSET_RESOLUTION',
274
+ category: 'runtime',
275
+ source: request.name,
276
+ details: { assetId: request.id },
277
+ cause: error
278
+ }
279
+ )
280
+ }
281
+ let result
282
+ try {
283
+ result = await Scene3dOptions.awaitWithSignal(
284
+ pending,
285
+ options.signal
286
+ )
287
+ } catch (error) {
288
+ if (
289
+ error instanceof ToolkitError &&
290
+ error.code === 'ERR_CANCELLED'
291
+ ) {
292
+ throw error
293
+ }
294
+ throw new ToolkitError(
295
+ `Unable to resolve scene asset: ${request.id}.`,
296
+ {
297
+ code: 'ERR_ASSET_RESOLUTION',
298
+ category: 'runtime',
299
+ source: request.name,
300
+ details: { assetId: request.id },
301
+ cause: error
302
+ }
303
+ )
304
+ }
305
+ Scene3dOptions.assertNotAborted(options.signal)
306
+ const data = SceneAssetResolver.#resolvedData(result)
307
+ if (data === null) throw SceneAssetResolver.#missingData(request)
308
+ return { request, data }
309
+ }
310
+
311
+ /**
312
+ * Creates a clone-safe resolver request without copying absent payload data.
313
+ * @param {object} request Canonical request.
314
+ * @returns {object} Resolver request.
315
+ */
316
+ static #resolverRequest(request) {
317
+ return {
318
+ id: request.id,
319
+ kind: request.kind,
320
+ name: request.name,
321
+ mediaType: request.mediaType,
322
+ byteLength: request.byteLength,
323
+ data: null,
324
+ source: CircuitJsonReadOnlyDocument.copyMetadataValue(
325
+ request.source
326
+ )
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Extracts one supported payload from a resolver return value.
332
+ * @param {unknown} result Resolver return value.
333
+ * @returns {string | ArrayBuffer | ArrayBufferView | null} Payload.
334
+ */
335
+ static #resolvedData(result) {
336
+ if (SceneAssetResolver.#isPayload(result)) return result
337
+ if (result === null || result === undefined) return null
338
+ const fields = Scene3dOptions.dataRecord(result, 'Resolved scene asset')
339
+ return SceneAssetResolver.#isPayload(fields.data) ? fields.data : null
340
+ }
341
+
342
+ /**
343
+ * Copies and bounds one resolved asset payload.
344
+ * @param {object} request Asset request.
345
+ * @param {string | ArrayBuffer | ArrayBufferView} data Resolved payload.
346
+ * @param {{ buffer: ArrayBuffer | SharedArrayBuffer, byteOffset: number, byteLength: number } | null} range Captured binary range.
347
+ * @returns {object} Canonical asset with copied data.
348
+ */
349
+ static #assetWithData(request, data, range) {
350
+ const copiedData =
351
+ typeof data === 'string'
352
+ ? data
353
+ : BinaryDataSnapshot.copyBytes(data, range)
354
+ return ToolkitAsset.create({
355
+ id: request.id,
356
+ kind: request.kind,
357
+ name: request.name,
358
+ mediaType: request.mediaType,
359
+ data: copiedData,
360
+ source: request.source
361
+ })
362
+ }
363
+
364
+ /**
365
+ * Measures a supported payload without copying oversized binary inputs.
366
+ * @param {string | ArrayBuffer | ArrayBufferView} data Payload.
367
+ * @param {{ byteLength: number } | null} [range] Captured binary range.
368
+ * @returns {number} Byte length.
369
+ */
370
+ static #byteLength(data, range = null) {
371
+ if (typeof data === 'string') {
372
+ return CircuitJsonReadOnlyDocument.utf8ByteLength(data)
373
+ }
374
+ return range?.byteLength ?? BinaryDataSnapshot.byteLength(data) ?? 0
375
+ }
376
+
377
+ /**
378
+ * Returns true for supported textual or binary payloads.
379
+ * @param {unknown} value Payload candidate.
380
+ * @returns {boolean} Whether the value is supported.
381
+ */
382
+ static #isPayload(value) {
383
+ return (
384
+ typeof value === 'string' ||
385
+ BinaryDataSnapshot.byteLength(value) !== null
386
+ )
387
+ }
388
+
389
+ /**
390
+ * Creates the shared missing-data failure.
391
+ * @param {object} request Asset request.
392
+ * @returns {ToolkitError} Typed failure.
393
+ */
394
+ static #missingData(request) {
395
+ return new ToolkitError(
396
+ `Scene asset data is required: ${request.id}.`,
397
+ {
398
+ code: 'ERR_ASSET_DATA_REQUIRED',
399
+ category: 'unsupported',
400
+ source: request.name,
401
+ details: { assetId: request.id }
402
+ }
403
+ )
404
+ }
405
+
406
+ /**
407
+ * Creates one asset size-limit failure.
408
+ * @param {object} request Asset request.
409
+ * @param {number} actual Actual bytes.
410
+ * @param {number} maximum Maximum bytes.
411
+ * @param {'asset' | 'total'} scope Limit scope.
412
+ * @returns {ToolkitError} Typed failure.
413
+ */
414
+ static #limitError(request, actual, maximum, scope) {
415
+ return new ToolkitError('Scene asset bytes exceed the safe limit.', {
416
+ code: 'ERR_ASSET_LIMIT',
417
+ category: 'unsupported',
418
+ source: request.name,
419
+ details: {
420
+ assetId: request.id,
421
+ scope,
422
+ actual,
423
+ maximum
424
+ }
425
+ })
426
+ }
427
+
428
+ /**
429
+ * Emits one ordered progress row when a callback is installed.
430
+ * @param {ReturnType<Scene3dOptions['normalize']>} options Normalized options.
431
+ * @param {object} fields Progress fields.
432
+ * @param {object | null} previous Previous row.
433
+ * @returns {object | null} Emitted row or previous value.
434
+ */
435
+ static #progress(options, fields, previous) {
436
+ if (!options.onProgress) return previous
437
+ const row = ToolkitProgress.create(fields, previous)
438
+ options.onProgress(row)
439
+ return row
440
+ }
441
+ }