PageSourceSearch

https://threejson.org/core/handler/sceneFriendlyNormalizer.js

js threejson.org collected 2026-10-04 23:35:23 UTC 56,477 bytes, 1,666 lines download raw bytes

1import { resolveLegacyDomainHandlerForBoxModel } from "./businessDomainRegistry.js";
2import { log } from "../util/logger.js";
3import { shouldDeferBoxCoerce } from "./nativeParseMode.js";
4import { extractDefaultModelFromScenePayload } from "./defaultModelDescriptor.js";
5import { coalesceBoxModelList } from "./boxModelListCoalescer.js";
6import {
7  applyFriendlySceneDefaults,
8  getFriendlySceneListEntries,
9  resolveFriendlySceneMap
10} from "./sceneFriendlyMap.js";
11import { ensureThreeJsonIdsOnScenePayload, hasValue, listOr } from "../util/util.js";
12import { normalizeSubSceneHierarchy } from "./subSceneHierarchy.js";
13import { applyLegacyGeometryObjTypeAlias } from "./legacyGeometryAlias.js";
14import { normalizeLightsConfigWithMeta } from "../util/sceneRuntimeDefaults.js";
15import { mergeExtensionMaps } from "../util/extensionsUtil.js";
16import {
17  canonicalizeHelpersForSceneConfig,
18  normalizeHelpersConfig
19} from "../builder/sceneHelperBuilder.js";
20import { normalizeIntroConfig } from "../runtime/sceneIntroConfig.js";
21import { resolvePosition, resolveRotation, resolveScale } from "../util/vectorValue.js";
22import { isCanonicalScenePayload } from "./scenePayloadShape.js";
23import {
24  JSON_ORIGIN_CONFIG,
25  JSON_ORIGIN_LIST,
26  buildConfigRuntimeDedupKeySet,
27  ensureJsonOrigin,
28  shouldDropDuplicateObjectListRuntime
29} from "../util/sceneJsonOrigin.js";
30
31const AUTO_HEAT_OBJ_TYPES = new Set(["heat", "heatmap", "heatmapvolume", "heatmap3d"]);
32const CANONICAL_RUNTIME_OBJ_TYPES = new Set(["scene", "camera", "renderer", "controls", "light", "renderloop"]);
33const CANONICAL_PRIMITIVE_OBJ_TYPES = new Set(["box", "sphere", "cylinder", "cone", "ring", "torus", "capsule"]);
34const LEGACY_EXTERNAL_MODEL_TYPES = new Set([
35  "obj",
36  "gltf",
37  "glb",
38  "stl",
39  "ply",
40  "fbx",
41  "usdz",
42  "usd",
43  "three",
44  "threejson",
45  "object"
46]);
47
48function hasOwn(source, key) {
49  return Boolean(source) && Object.prototype.hasOwnProperty.call(source, key);
50}
51
52function isFiniteNumber(value) {
53  return Number.isFinite(value);
54}
55
56function isPlainObject(value) {
57  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
58}
59
60function cloneJsonPayload(payload) {
61  if (!payload || typeof payload !== "object") {
62    return {};
63  }
64  try {
65    return JSON.parse(JSON.stringify(payload));
66  } catch (_error) {
67    return { ...payload };
68  }
69}
70
71function mergeVector3Config(baseValue, overrideValue, legacyValue, defaultValue = { x: 0, y: 0, z: 0 }) {
72  const base = baseValue && typeof baseValue === "object" ? baseValue : {};
73  const override = overrideValue && typeof overrideValue === "object" ? overrideValue : {};
74  const legacy = legacyValue && typeof legacyValue === "object" ? legacyValue : {};
75  return {
76    x: hasValue(override.x) ? Number(override.x) : hasValue(legacy.x) ? Number(legacy.x) : hasValue(base.x) ? Number(base.x) : defaultValue.x,
77    y: hasValue(override.y) ? Number(override.y) : hasValue(legacy.y) ? Number(legacy.y) : hasValue(base.y) ? Number(base.y) : defaultValue.y,
78    z: hasValue(override.z) ? Number(override.z) : hasValue(legacy.z) ? Number(legacy.z) : hasValue(base.z) ? Number(base.z) : defaultValue.z
79  };
80}
81
82function normalizeCameraConfig(sceneConfig = {}) {
83  const base = sceneConfig && typeof sceneConfig === "object" ? { ...sceneConfig } : {};
84  const next = { ...base };
85  next.position = mergeVector3Config(base.position, null, null, { x: 0, y: 0, z: 5 });
86  return next;
87}
88
89function normalizeControlsConfig(sceneConfig = {}) {
90  const base = sceneConfig && typeof sceneConfig === "object" ? { ...sceneConfig } : {};
91  const next = { ...base };
92  next.target = mergeVector3Config(base.target, null, null, { x: 0, y: 0, z: 0 });
93  return next;
94}
95
96function liftWorldInfoRuntimeSugarToSceneConfig(payload = {}) {
97  if (!isPlainObject(payload)) {
98    return payload;
99  }
100  const wi = isPlainObject(payload.worldInfo) ? payload.worldInfo : null;
101  if (!wi) {
102    return payload;
103  }
104
105  if (!isPlainObject(payload.sceneConfig)) {
106    payload.sceneConfig = {};
107  }
108  const sc = payload.sceneConfig;
109
110  const scCamera = isPlainObject(sc.camera) ? sc.camera : {};
111  const worldCamera = isPlainObject(wi.camera) ? wi.camera : null;
112  const legacyPosition = isPlainObject(wi.cameraPosition) ? wi.cameraPosition : null;
113  if (worldCamera || legacyPosition) {
114    const nextCamera = {
115      ...(worldCamera || {}),
116      ...scCamera
117    };
118    nextCamera.position = mergeVector3Config(
119      worldCamera?.position,
120      scCamera.position,
121      legacyPosition,
122      { x: 0, y: 0, z: 5 }
123    );
124    sc.camera = nextCamera;
125    delete wi.camera;
126    delete wi.cameraPosition;
127  }
128
129  const scControls = isPlainObject(sc.controls) ? sc.controls : {};
130  const worldOrbitControls = isPlainObject(wi.orbitControls) ? wi.orbitControls : {};
131  const worldControls = isPlainObject(wi.controls) ? wi.controls : {};
132  if (Object.keys(worldOrbitControls).length > 0 || Object.keys(worldControls).length > 0) {
133    const controlsSugar = {
134      ...worldOrbitControls,
135      ...worldControls
136    };
137    const nextControls = {
138      ...controlsSugar,
139      ...scControls
140    };
141    nextControls.target = mergeVector3Config(
142      controlsSugar.target,
143      scControls.target,
144      null,
145      { x: 0, y: 0, z: 0 }
146    );
147    sc.controls = nextControls;
148    delete wi.orbitControls;
149    delete wi.controls;
150  }
151
152  return payload;
153}
154
155function normalizeLightsConfig(sceneConfig = {}, worldInfo = {}) {
156  const meta = normalizeLightsConfigWithMeta(sceneConfig, worldInfo);
157  return {
158    hasExplicitLights: meta.hasExplicitLights,
159    lights: meta.lights,
160    lightsMeta: meta.lightsMeta
161  };
162}
163
164function normalizeNativeSceneEntry(worldInfo = {}) {
165  const nativeSceneList = listOr(worldInfo.nativeSceneList);
166  for (let i = 0; i < nativeSceneList.length; i++) {
167    const record = nativeSceneList[i];
168    if (!record || typeof record !== "object" || !hasOwn(record, "jsonData")) {
169      continue;
170    }
171    const source = record.jsonData;
172    if (typeof source === "string") {
173      const trimmed = source.trim();
174      if (!trimmed) {
175        continue;
176      }
177      return {
178        source: record,
179        json: JSON.parse(trimmed)
180      };
181    }
182    if (source && typeof source === "object") {
183      return {
184        source: record,
185        json: source
186      };
187    }
188  }
189  return null;
190}
191
192function hasKeys(value) {
193  return Boolean(value) && typeof value === "object" && Object.keys(value).length > 0;
194}
195
196function cloneRecord(record) {
197  return record && typeof record === "object" ? { ...record } : null;
198}
199
200/** Normalize the common Three.js-shaped material.map.url spelling to ThreeJSON's
201 * canonical material.textureUrl field. Native Three.js export payloads use a separate
202 * loader and do not pass through this friendly-record normalizer. */
203function normalizeMaterialTextureAlias(material) {
204  if (!isPlainObject(material)) {
205    return material;
206  }
207  const next = { ...material };
208  const map = isPlainObject(next.map) ? next.map : null;
209  if (!hasValue(next.textureUrl) && typeof map?.url === "string" && map.url.trim()) {
210    next.textureUrl = map.url.trim();
211    if (!hasOwn(next, "textureRepeat") && isPlainObject(map.repeat)) {
212      next.textureRepeat = {
213        x: Number.isFinite(Number(map.repeat.x)) ? Number(map.repeat.x) : 1,
214        y: Number.isFinite(Number(map.repeat.y)) ? Number(map.repeat.y) : 1
215      };
216    }
217    const remainingMapKeys = Object.keys(map).filter((key) => key !== "url" && key !== "repeat");
218    if (remainingMapKeys.length === 0) {
219      delete next.map;
220    }
221  }
222  return next;
223}
224
225function normalizeSceneObjType(value) {
226  return typeof value === "string" ? value.trim().toLowerCase() : "";
227}
228
229function normalizePrimitiveShapeType(value) {
230  const normalized = normalizeSceneObjType(value);
231  if (normalized === "boxgeometry") {
232    return "box";
233  }
234  if (normalized === "spheregeometry") {
235    return "sphere";
236  }
237  if (normalized === "cylindergeometry") {
238    return "cylinder";
239  }
240  if (normalized === "conegeometry") {
241    return "cone";
242  }
243  if (normalized === "ringgeometry") {
244    return "ring";
245  }
246  if (normalized === "torusgeometry") {
247    return "torus";
248  }
249  if (normalized === "capsulegeometry") {
250    return "capsule";
251  }
252  return normalized;
253}
254
255function applyCanonicalObjType(record, canonicalObjType) {
256  const next = cloneRecord(record);
257  if (!next) {
258    return null;
259  }
260  next.objType = canonicalObjType;
261  return next;
262}
263
264function isRuntimeObjType(value) {
265  return CANONICAL_RUNTIME_OBJ_TYPES.has(normalizeSceneObjType(value));
266}
267
268function resolveDeclaredExternalModelType(record) {
269  const explicitModelFileType = normalizeSceneObjType(record?.modelFileType);
270  if (explicitModelFileType && explicitModelFileType !== "externalmodel") {
271    return explicitModelFileType;
272  }
273  const legacyFileType = normalizeSceneObjType(record?.fileType);
274  if (legacyFileType && legacyFileType !== "externalmodel") {
275    return legacyFileType;
276  }
277  const declaredObjType = normalizeSceneObjType(record?.objType);
278  if (LEGACY_EXTERNAL_MODEL_TYPES.has(declaredObjType)) {
279    return declaredObjType;
280  }
281  return "";
282}
283
284function isGroupRecord(record) {
285  const objType = normalizeSceneObjType(record?.objType);
286  if (objType === "group") {
287    return true;
288  }
289  return Array.isArray(record?.subScene) && record.subScene.length > 0;
290}
291
292function isInfoPanelRecord(record) {
293  const objType = normalizeSceneObjType(record?.objType);
294  if (objType === "infopanel" || objType === "panel") {
295    return true;
296  }
297  if (objType === "css3dpanel") {
298    return false;
299  }
300  return Boolean(
301    record
302    && (record.panel || record.panelWidth || record.panelHeight)
303    && (record.type === "text" || record.type === "html" || record.type === "img")
304  );
305}
306
307function isCss3dPanelRecord(record) {
308  const objType = normalizeSceneObjType(record?.objType);
309  if (objType === "css3dpanel") {
310    return true;
311  }
312  const content = record?.content;
313  if (!content || typeof content !== "object") {
314    return false;
315  }
316  const contentType = String(content.type || "").trim().toLowerCase();
317  return contentType === "html" || contentType === "url";
318}
319
320function isLineRecord(record) {
321  return normalizeSceneObjType(record?.objType) === "line"
322    || (Array.isArray(record?.points) && record.points.length > 0);
323}
324
325function isHeatRecord(record) {
326  const objType = normalizeSceneObjType(record?.objType);
327  return AUTO_HEAT_OBJ_TYPES.has(objType) || Array.isArray(record?.heatMap);
328}
329
330function isWindRecord(record) {
331  return normalizeSceneObjType(record?.objType) === "wind";
332}
333
334function isShaderSurfaceRecord(record) {
335  return normalizeSceneObjType(record?.objType) === "shadersurface";
336}
337
338function isPlaneRecord(record) {
339  return normalizeSceneObjType(record?.objType) === "plane";
340}
341
342function isPointsRecord(record) {
343  const objType = normalizeSceneObjType(record?.objType);
344  return objType === "points" || objType === "particles" || objType === "particle";
345}
346
347function isSpriteRecord(record) {
348  return normalizeSceneObjType(record?.objType) === "sprite";
349}
350
351function isTubeRecord(record) {
352  return normalizeSceneObjType(record?.objType) === "tube";
353}
354
355function isInstancedRecord(record) {
356  return normalizeSceneObjType(record?.objType) === "instanced";
357}
358
359function isSkinnedRecord(record) {
360  return normalizeSceneObjType(record?.objType) === "skinned";
361}
362
363function isAudioRecord(record) {
364  return normalizeSceneObjType(record?.objType) === "audio";
365}
366
367function isPassRecord(record) {
368  return normalizeSceneObjType(record?.objType) === "pass";
369}
370
371function isExternalModelRecord(record) {
372  if (!record?.modelPath) {
373    return false;
374  }
375  const objType = normalizeSceneObjType(record?.objType);
376  if (objType === "skinned") {
377    return false;
378  }
379  return objType === "externalmodel"
380    || resolveDeclaredExternalModelType(record) !== ""
381    || objType === "";
382}
383
384function resolveCanonicalPrimitiveObjType(record, fallbackObjType = "box", options = {}) {
385  if (shouldDeferBoxCoerce(record, options)) {
386    const raw = normalizeSceneObjType(record?.objType);
387    if (raw) {
388      return raw;
389    }
390  }
391  const candidates = [
392    record?.objType,
393    record?.boxType,
394    record?.geometry?.type,
395    fallbackObjType
396  ];
397  for (let i = 0; i < candidates.length; i++) {
398    const normalized = normalizePrimitiveShapeType(candidates[i]);
399    if (CANONICAL_PRIMITIVE_OBJ_TYPES.has(normalized)) {
400      return normalized;
401    }
402  }
403  const geometry = record?.geometry && typeof record.geometry === "object" ? record.geometry : {};
404  if (hasValue(geometry.innerRadius) || hasValue(geometry.outerRadius)) {
405    return "ring";
406  }
407  if (hasValue(geometry.radiusTop) || hasValue(geometry.radiusBottom)) {
408    return "cylinder";
409  }
410  if (hasValue(geometry.tube)) {
411    return "torus";
412  }
413  if (hasValue(geometry.capSegments)) {
414    return "capsule";
415  }
416  if (
417    hasValue(geometry.radius)
418    && !hasValue(geometry.width)
419    && !hasValue(geometry.depth)
420    && !hasValue(geometry.length)
421    && !hasValue(geometry.height)
422  ) {
423    return "sphere";
424  }
425  return "box";
426}
427
428function normalizeInfoPanelRecord(record) {
429  const next = applyCanonicalObjType(record, "infoPanel");
430  if (!next) {
431    return null;
432  }
433  next.panelBoxType = next.panelBoxType || next.boxType || "box";
434  delete next.boxType;
435  return next;
436}
437
438function normalizeCss3dPanelRecord(record) {
439  return applyCanonicalObjType(record, "css3dPanel");
440}
441
442function normalizeExternalModelRecord(record) {
443  const next = applyCanonicalObjType(record, "externalModel");
444  if (!next) {
445    return null;
446  }
447  const modelFileType = resolveDeclaredExternalModelType(next);
448  if (modelFileType) {
449    next.modelFileType = modelFileType;
450  }
451  delete next.fileType;
452  return next;
453}
454
455function normalizeSkinnedModelRecord(record) {
456  const next = applyCanonicalObjType(record, "skinned");
457  if (!next) {
458    return null;
459  }
460  const modelFileType = resolveDeclaredExternalModelType(next);
461  if (modelFileType) {
462    next.modelFileType = modelFileType;
463  }
464  delete next.fileType;
465  return next;
466}
467
468function normalizeInstancedRecord(record) {
469  const next = applyCanonicalObjType(record, "instanced");
470  if (!next) {
471    return null;
472  }
473  if (!Array.isArray(next.transforms)) {
474    next.transforms = [];
475  }
476  next.instance = true;
477  return next;
478}
479
480function normalizeDomainRecord(record, overrides = {}) {
481  const next = applyCanonicalObjType(record, "domain");
482  if (!next) {
483    return null;
484  }
485  if (overrides.domain && !next.domain) {
486    next.domain = overrides.domain;
487  }
488  if (overrides.handler && !next.handler) {
489    next.handler = overrides.handler;
490  }
491  return next;
492}
493
494function normalizeCanonicalObjectRecord(record, options = {}) {
495  if (!record || typeof record !== "object") {
496    return null;
497  }
498  record = applyLegacyGeometryObjTypeAlias(record) ?? record;
499  record = cloneRecord(record) ?? record;
500  const isNativeRecord = normalizeSceneObjType(record.objType) === "native" || record.parseMode === "native";
501  if (!isNativeRecord && hasOwn(record, "material")) {
502    record.material = Array.isArray(record.material)
503      ? record.material.map(normalizeMaterialTextureAlias)
504      : normalizeMaterialTextureAlias(record.material);
505  }
506  if (!isNativeRecord && hasOwn(record, "materialOverrides")) {
507    record.materialOverrides = normalizeMaterialTextureAlias(record.materialOverrides);
508  }
509  if (hasOwn(record, "position")) record.position = resolvePosition(record.position);
510  if (hasOwn(record, "rotation")) record.rotation = resolveRotation(record.rotation);
511  if (hasOwn(record, "scale")) record.scale = resolveScale(record.scale);
512  if (record.panel && typeof record.panel === "object" && !Array.isArray(record.panel)) {
513    record.panel = { ...record.panel };
514    if (hasOwn(record.panel, "position")) record.panel.position = resolvePosition(record.panel.position);
515    if (hasOwn(record.panel, "rotation")) record.panel.rotation = resolveRotation(record.panel.rotation);
516    if (hasOwn(record.panel, "scale")) record.panel.scale = resolveScale(record.panel.scale);
517  }
518  if (Array.isArray(record.transforms)) {
519    record.transforms = record.transforms.map((entry) => ({
520      ...(entry && typeof entry === "object" ? entry : {}),
521      ...(entry && Object.prototype.hasOwnProperty.call(entry, "position") ? { position: resolvePosition(entry.position) } : {}),
522      ...(entry && Object.prototype.hasOwnProperty.call(entry, "rotation") ? { rotation: resolveRotation(entry.rotation) } : {}),
523      ...(entry && Object.prototype.hasOwnProperty.call(entry, "scale") ? { scale: resolveScale(entry.scale) } : {})
524    }));
525  }
526  for (const childKey of ["subScene", "boxModelList", "joins", "inters", "holes"]) {
527    if (Array.isArray(record[childKey])) {
528      record[childKey] = record[childKey]
529        .map((child) => normalizeCanonicalObjectRecord(child, { sceneConfig: options.sceneConfig }))
530        .filter(Boolean);
531    }
532  }
533  if (normalizeSceneObjType(record.objType) === "default") {
534    return null;
535  }
536  if (normalizeSceneObjType(record.objType) === "native") {
537    return applyCanonicalObjType(record, "native");
538  }
539  if (options.forceObjType === "domain" || record.domain || normalizeSceneObjType(record.objType) === "domain") {
540    return normalizeDomainRecord(record, options);
541  }
542  if (options.forceObjType === "group" || isGroupRecord(record)) {
543    return applyCanonicalObjType(record, "group");
544  }
545  if (options.forceObjType === "infoPanel" || isInfoPanelRecord(record)) {
546    return normalizeInfoPanelRecord(record);
547  }
548  if (options.forceObjType === "css3dPanel" || isCss3dPanelRecord(record)) {
549    return normalizeCss3dPanelRecord(record);
550  }
551  if (options.forceObjType === "line" || isLineRecord(record)) {
552    return applyCanonicalObjType(record, "line");
553  }
554  if (options.forceObjType === "heatMap" || isHeatRecord(record)) {
555    return applyCanonicalObjType(record, "heatMap");
556  }
557  if (options.forceObjType === "wind" || isWindRecord(record)) {
558    return applyCanonicalObjType(record, "wind");
559  }
560  if (options.forceObjType === "shaderSurface" || isShaderSurfaceRecord(record)) {
561    return applyCanonicalObjType(record, "shaderSurface");
562  }
563  if (options.forceObjType === "plane" || isPlaneRecord(record)) {
564    return applyCanonicalObjType(record, "plane");
565  }
566  if (options.forceObjType === "points" || isPointsRecord(record)) {
567    return applyCanonicalObjType(record, "points");
568  }
569  if (options.forceObjType === "sprite" || isSpriteRecord(record)) {
570    return applyCanonicalObjType(record, "sprite");
571  }
572  if (options.forceObjType === "tube" || isTubeRecord(record)) {
573    return applyCanonicalObjType(record, "tube");
574  }
575  if (options.forceObjType === "instanced" || isInstancedRecord(record)) {
576    return normalizeInstancedRecord(record);
577  }
578  if (options.forceObjType === "skinned" || isSkinnedRecord(record)) {
579    return normalizeSkinnedModelRecord(record);
580  }
581  if (options.forceObjType === "audio" || isAudioRecord(record)) {
582    return applyCanonicalObjType(record, "audio");
583  }
584  if (options.forceObjType === "pass" || isPassRecord(record)) {
585    return applyCanonicalObjType(record, "pass");
586  }
587  if (options.forceObjType === "externalModel" || isExternalModelRecord(record)) {
588    return normalizeExternalModelRecord(record);
589  }
590  const shapeType = normalizeSceneObjType(options.forceObjType || record.objType);
591  if (shapeType === "shapeplane") {
592    return applyCanonicalObjType(record, "shapePlane");
593  }
594  if (shapeType === "buffermesh") {
595    return applyCanonicalObjType(record, "bufferMesh");
596  }
597  if (shapeType === "editablemesh") {
598    return applyCanonicalObjType(record, "editableMesh");
599  }
600  if (shapeType === "irregularplane") {
601    return applyCanonicalObjType(record, "irregularPlane");
602  }
603  if (shapeType === "shapeextrude") {
604    return applyCanonicalObjType(record, "shapeExtrude");
605  }
606  if (shapeType === "irregulargeometry") {
607    return applyCanonicalObjType(record, "irregularGeometry");
608  }
609  if (isRuntimeObjType(options.forceObjType) || isRuntimeObjType(record.objType)) {
610    return applyCanonicalObjType(record, options.forceObjType || record.objType);
611  }
612  const legacyDomain = resolveLegacyDomainHandlerForBoxModel(record);
613  if (legacyDomain) {
614    return normalizeDomainRecord(record, legacyDomain);
615  }
616  return applyCanonicalObjType(
617    record,
618    resolveCanonicalPrimitiveObjType(record, options.forceObjType, options)
619  );
620}
621
622function appendCanonicalRecords(targetList, sourceList, options = {}) {
623  const list = listOr(sourceList);
624  for (let i = 0; i < list.length; i++) {
625    const normalized = normalizeCanonicalObjectRecord(list[i], options);
626    if (normalized) {
627      targetList.push(normalized);
628    }
629  }
630}
631
632function buildCanonicalRuntimeObjectListFromFriendly(payload, sceneConfig, worldInfo) {
633  const runtimeObjects = [];
634  const sceneHints = sceneConfig?.scene && typeof sceneConfig.scene === "object" ? sceneConfig.scene : {};
635  if (hasKeys(sceneHints)) {
636    runtimeObjects.push({
637      objType: "scene",
638      ...sceneHints
639    });
640  }
641  const hasCameraConfig = hasOwn(sceneConfig, "camera");
642  if (hasCameraConfig) {
643    runtimeObjects.push({
644      objType: "camera",
645      ...normalizeCameraConfig(sceneConfig.camera)
646    });
647  }
648  if (sceneConfig?.renderer && typeof sceneConfig.renderer === "object") {
649    runtimeObjects.push({
650      objType: "renderer",
651      ...sceneConfig.renderer
652    });
653  }
654  const hasControlsConfig = hasOwn(sceneConfig, "controls");
655  if (hasControlsConfig) {
656    runtimeObjects.push({
657      objType: "controls",
658      ...normalizeControlsConfig(sceneConfig.controls)
659    });
660  }
661  const lightConfig = normalizeLightsConfig(sceneConfig, worldInfo);
662  for (let i = 0; i < lightConfig.lights.length; i++) {
663    runtimeObjects.push({
664      objType: "light",
665      ...lightConfig.lights[i]
666    });
667  }
668  if (sceneConfig?.renderLoop && typeof sceneConfig.renderLoop === "object") {
669    runtimeObjects.push({
670      objType: "renderLoop",
671      ...sceneConfig.renderLoop
672    });
673  }
674  return runtimeObjects;
675}
676
677function resolveCanvasMeta(payload, sceneConfig) {
678  return {
679    canvasWidth: isFiniteNumber(Number(payload?.canvasWidth)) ? Number(payload.canvasWidth) : sceneConfig?.canvasWidth,
680    canvasHeight: isFiniteNumber(Number(payload?.canvasHeight)) ? Number(payload.canvasHeight) : sceneConfig?.canvasHeight
681  };
682}
683
684function readFriendlyListSource(payload, worldInfo, definition) {
685  if (!definition || !definition.listName) {
686    return [];
687  }
688  if (definition.scope === "topLevel") {
689    return listOr(payload?.[definition.listName]);
690  }
691  if (definition.scope === "topLevelOrWorldInfo") {
692    const topLevelList = listOr(payload?.[definition.listName]);
693    if (topLevelList.length > 0) {
694      return topLevelList;
695    }
696    return listOr(worldInfo?.[definition.listName]);
697  }
698  if (definition.scope === "worldInfoOrSceneConfig") {
699    const fromWorld = listOr(worldInfo?.[definition.listName]);
700    if (fromWorld.length > 0) {
701      return fromWorld;
702    }
703    const sceneConfig =
704      payload?.sceneConfig && typeof payload.sceneConfig === "object" ? payload.sceneConfig : {};
705    return listOr(sceneConfig[definition.listName]);
706  }
707  return listOr(worldInfo?.[definition.listName]);
708}
709
710function isMeshListAllowedRecord(record) {
711  const objType = normalizeSceneObjType(record?.objType);
712  return objType === "box" || objType === "sphere";
713}
714
715function appendFriendlyConfiguredList(targetList, payload, worldInfo, definition, sceneConfig = {}) {
716  let sourceList = readFriendlyListSource(payload, worldInfo, definition);
717  if (sourceList.length <= 0) {
718    return;
719  }
720  if (definition.preprocess === "coalesceBoxModelList") {
721    sourceList = coalesceBoxModelList(sourceList);
722  }
723  for (let i = 0; i < sourceList.length; i++) {
724    const entry = sourceList[i];
725    if (definition.listName === "meshList" && !isMeshListAllowedRecord(entry)) {
726      log.warn(
727        "[meshList] skipping entry: objType must be box or sphere:",
728        entry?.objType,
729        entry?.name || ""
730      );
731      continue;
732    }
733    const mergedRecord = applyFriendlySceneDefaults(entry, definition);
734    const normalized = normalizeCanonicalObjectRecord(mergedRecord, {
735      forceObjType: definition.objType,
736      sceneConfig
737    });
738    if (normalized) {
739      targetList.push(normalized);
740    }
741  }
742}
743
744function buildCanonicalScenePayloadFromFriendly(sourcePayload) {
745  const worldInfo = sourcePayload.worldInfo && typeof sourcePayload.worldInfo === "object"
746    ? sourcePayload.worldInfo
747    : {};
748  const sceneConfig = sourcePayload.sceneConfig && typeof sourcePayload.sceneConfig === "object"
749    ? sourcePayload.sceneConfig
750    : {};
751  const objectList = buildCanonicalRuntimeObjectListFromFriendly(sourcePayload, sceneConfig, worldInfo);
752  const friendlyListEntries = getFriendlySceneListEntries(sourcePayload);
753  for (let i = 0; i < friendlyListEntries.length; i++) {
754    appendFriendlyConfiguredList(objectList, sourcePayload, worldInfo, friendlyListEntries[i], sceneConfig);
755  }
756  const canvasMeta = resolveCanvasMeta(sourcePayload, sceneConfig);
757  const canonicalPayload = {
758    version: typeof sourcePayload.version === "string" && sourcePayload.version ? sourcePayload.version : "next",
759    objectList
760  };
761  if (typeof sourcePayload.name === "string" && sourcePayload.name) {
762    canonicalPayload.name = sourcePayload.name;
763  } else if (typeof worldInfo.name === "string" && worldInfo.name) {
764    canonicalPayload.name = worldInfo.name;
765  }
766  copySceneDocumentMetadata(sourcePayload, canonicalPayload, canvasMeta);
767  return canonicalPayload;
768}
769
770function copySceneDocumentMetadata(sourcePayload, targetPayload, canvasMeta = {}) {
771  for (const key of ["timeline", "output", "documentType"]) if (sourcePayload[key] !== undefined) targetPayload[key] = clonePlainValue(sourcePayload[key]);
772  if (typeof sourcePayload.threeJsonId === "string" && sourcePayload.threeJsonId) {
773    targetPayload.threeJsonId = sourcePayload.threeJsonId;
774  }
775  if (isFiniteNumber(canvasMeta.canvasWidth)) {
776    targetPayload.canvasWidth = Number(canvasMeta.canvasWidth);
777  }
778  if (isFiniteNumber(canvasMeta.canvasHeight)) {
779    targetPayload.canvasHeight = Number(canvasMeta.canvasHeight);
780  } else if (isFiniteNumber(Number(sourcePayload.canvasWidth))) {
781    targetPayload.canvasWidth = Number(sourcePayload.canvasWidth);
782  }
783  if (!isFiniteNumber(targetPayload.canvasHeight) && isFiniteNumber(Number(sourcePayload.canvasHeight))) {
784    targetPayload.canvasHeight = Number(sourcePayload.canvasHeight);
785  }
786}
787
788function appendCanonicalObjectListWithDualChannel(targetList, sourceList, options = {}) {
789  const sceneConfig = options.sceneConfig && typeof options.sceneConfig === "object"
790    ? options.sceneConfig
791    : {};
792  const configRuntimeRecords = buildCanonicalRuntimeObjectListFromFriendly(
793    options.sourcePayload || {},
794    sceneConfig,
795    {}
796  ).map((record) => ensureJsonOrigin(record, JSON_ORIGIN_CONFIG));
797  const configDedupKeys = buildConfigRuntimeDedupKeySet(configRuntimeRecords);
798
799  const contentRecords = [];
800  const listRuntimeRecords = [];
801  const records = listOr(sourceList);
802  for (let i = 0; i < records.length; i++) {
803    const record = records[i];
804    const objType = normalizeSceneObjType(record?.objType);
805    const normalized = normalizeCanonicalObjectRecord(record, { sceneConfig });
806    if (!normalized) {
807      continue;
808    }
809    if (CANONICAL_RUNTIME_OBJ_TYPES.has(objType)) {
810      if (shouldDropDuplicateObjectListRuntime(normalized, configDedupKeys)) {
811        continue;
812      }
813      listRuntimeRecords.push(ensureJsonOrigin(normalized, JSON_ORIGIN_LIST));
814      continue;
815    }
816    contentRecords.push(normalized);
817  }
818
819  for (let i = 0; i < contentRecords.length; i++) {
820    targetList.push(contentRecords[i]);
821  }
822  for (let i = 0; i < listRuntimeRecords.length; i++) {
823    targetList.push(listRuntimeRecords[i]);
824  }
825  for (let i = 0; i < configRuntimeRecords.length; i++) {
826    targetList.push(configRuntimeRecords[i]);
827  }
828}
829
830function buildCanonicalScenePayloadFromCanonical(sourcePayload) {
831  const objectList = [];
832  const sceneConfig =
833    sourcePayload.sceneConfig && typeof sourcePayload.sceneConfig === "object"
834      ? sourcePayload.sceneConfig
835      : {};
836  appendCanonicalObjectListWithDualChannel(objectList, sourcePayload.objectList, {
837    sceneConfig,
838    sourcePayload
839  });
840  const canonicalPayload = {
841    version: typeof sourcePayload.version === "string" && sourcePayload.version ? sourcePayload.version : "next",
842    objectList
843  };
844  if (typeof sourcePayload.name === "string" && sourcePayload.name) {
845    canonicalPayload.name = sourcePayload.name;
846  }
847  copySceneDocumentMetadata(sourcePayload, canonicalPayload);
848  return canonicalPayload;
849}
850
851function splitCanonicalObjectList(objectList) {
852  const runtime = {
853    scene: null,
854    camera: null,
855    renderer: null,
856    controls: null,
857    renderLoop: null,
858    lights: []
859  };
860  const contentList = [];
861  const records = listOr(objectList);
862  for (let i = 0; i < records.length; i++) {
863    const record = records[i];
864    const objType = normalizeSceneObjType(record?.objType);
865    if (objType === "scene") {
866      runtime.scene = record;
867      continue;
868    }
869    if (objType === "camera") {
870      runtime.camera = record;
871      continue;
872    }
873    if (objType === "renderer") {
874      runtime.renderer = record;
875      continue;
876    }
877    if (objType === "controls") {
878      runtime.controls = record;
879      continue;
880    }
881    if (objType === "renderloop") {
882      runtime.renderLoop = record;
883      continue;
884    }
885    if (objType === "light") {
886      runtime.lights.push(record);
887      continue;
888    }
889    if (objType === "pass") {
890      continue;
891    }
892    if (objType === "boxhelper") {
893      continue;
894    }
895    contentList.push(record);
896  }
897  return {
898    runtime,
899    contentList
900  };
901}
902
903function stripObjType(record) {
904  if (!record || typeof record !== "object") {
905    return {};
906  }
907  const { objType, ...rest } = record;
908  return rest;
909}
910
911function shouldMirrorIntoCompatBoxModelList(record) {
912  const objType = normalizeSceneObjType(record?.objType);
913  if (objType === "domain") {
914    return Boolean(record?.geometry || record?.boxModelList || record?.businessInfo);
915  }
916  return CANONICAL_PRIMITIVE_OBJ_TYPES.has(objType);
917}
918
919function shouldMirrorIntoCompatObjModelList(record) {
920  const declaredType = resolveDeclaredExternalModelType(record);
921  if (declaredType === "obj") {
922    return true;
923  }
924  if (declaredType) {
925    return false;
926  }
927  return typeof record?.modelPath === "string" && /\.obj(?:[?#].*)?$/i.test(record.modelPath);
928}
929
930function buildCompatPayloadFromCanonical(sourcePayload, canonicalPayload, splitState) {
931  const nonRuntimeObjects = splitState.contentList;
932  const worldInfoSource = sourcePayload.worldInfo && typeof sourcePayload.worldInfo === "object"
933    ? sourcePayload.worldInfo
934    : {};
935  const worldInfo = {
936    ...worldInfoSource,
937    modelList: nonRuntimeObjects,
938    objectList: nonRuntimeObjects,
939    boxModelList: [],
940    sphereModelList: [],
941    meshList: [],
942    groupList: [],
943    lineList: [],
944    infoPanelList: [],
945    css3dPanelList: [],
946    heatList: [],
947    windList: [],
948    shaderSurfaceList: [],
949    planeList: [],
950    particleList: [],
951    spriteList: [],
952    tubeList: [],
953    instancedList: [],
954    audioList: [],
955    skinnedList: [],
956    externalModelList: [],
957    objModelList: [],
958    domainModelList: [],
959    shapePlaneList: [],
960    irregularPlaneList: [],
961    bufferMeshList: [],
962    editableMeshList: [],
963    shapeExtrudeList: [],
964    irregularGeometryList: []
965  };
966
967  for (let i = 0; i < nonRuntimeObjects.length; i++) {
968    const record = nonRuntimeObjects[i];
969    const objType = normalizeSceneObjType(record?.objType);
970    if (objType === "sphere") {
971      worldInfo.sphereModelList.push(record);
972    } else if (objType === "box") {
973      worldInfo.meshList.push(record);
974    } else if (shouldMirrorIntoCompatBoxModelList(record)) {
975      worldInfo.boxModelList.push(record);
976    } else if (objType === "group") {
977      worldInfo.groupList.push(record);
978    } else if (objType === "line") {
979      worldInfo.lineList.push(record);
980    } else if (objType === "infopanel") {
981      worldInfo.infoPanelList.push(record);
982    } else if (objType === "css3dpanel") {
983      worldInfo.css3dPanelList.push(record);
984    } else if (objType === "heatmap") {
985      worldInfo.heatList.push(record);
986    } else if (objType === "wind") {
987      worldInfo.windList.push(record);
988    } else if (objType === "shadersurface") {
989      worldInfo.shaderSurfaceList.push(record);
990    } else if (objType === "plane") {
991      worldInfo.planeList.push(record);
992    } else if (objType === "points") {
993      worldInfo.particleList.push(record);
994    } else if (objType === "sprite") {
995      worldInfo.spriteList.push(record);
996    } else if (objType === "tube") {
997      worldInfo.tubeList.push(record);
998    } else if (objType === "instanced") {
999      worldInfo.instancedList.push(record);
1000    } else if (objType === "audio") {
1001      worldInfo.audioList.push(record);
1002    } else if (objType === "skinned") {
1003      worldInfo.skinnedList.push(record);
1004    } else if (objType === "externalmodel") {
1005      worldInfo.externalModelList.push(record);
1006      if (shouldMirrorIntoCompatObjModelList(record)) {
1007        worldInfo.objModelList.push(record);
1008      }
1009    } else if (objType === "domain") {
1010      worldInfo.domainModelList.push(record);
1011    } else if (objType === "shapeplane") {
1012      if (!Array.isArray(worldInfo.shapePlaneList)) {
1013        worldInfo.shapePlaneList = [];
1014      }
1015      worldInfo.shapePlaneList.push(record);
1016    } else if (objType === "irregularplane") {
1017      if (!Array.isArray(worldInfo.irregularPlaneList)) {
1018        worldInfo.irregularPlaneList = [];
1019      }
1020      worldInfo.irregularPlaneList.push(record);
1021    } else if (objType === "buffermesh") {
1022      if (!Array.isArray(worldInfo.bufferMeshList)) {
1023        worldInfo.bufferMeshList = [];
1024      }
1025      worldInfo.bufferMeshList.push(record);
1026    } else if (objType === "editablemesh") {
1027      if (!Array.isArray(worldInfo.editableMeshList)) {
1028        worldInfo.editableMeshList = [];
1029      }
1030      worldInfo.editableMeshList.push(record);
1031    } else if (objType === "shapeextrude") {
1032      if (!Array.isArray(worldInfo.shapeExtrudeList)) {
1033        worldInfo.shapeExtrudeList = [];
1034      }
1035      worldInfo.shapeExtrudeList.push(record);
1036    } else if (objType === "irregulargeometry") {
1037      if (!Array.isArray(worldInfo.irregularGeometryList)) {
1038        worldInfo.irregularGeometryList = [];
1039      }
1040      worldInfo.irregularGeometryList.push(record);
1041    }
1042  }
1043
1044  const sceneConfig = {};
1045  // Font defaults do not become Object3D records; keep them when constructing
1046  // the runtime projection, or an explicitly local font silently uses the CDN.
1047  if (isPlainObject(sourcePayload.sceneConfig?.textFont)) {
1048    sceneConfig.textFont = clonePlainValue(sourcePayload.sceneConfig.textFont);
1049  }
1050  if (isFiniteNumber(canonicalPayload.canvasWidth)) {
1051    sceneConfig.canvasWidth = Number(canonicalPayload.canvasWidth);
1052  }
1053  if (isFiniteNumber(canonicalPayload.canvasHeight)) {
1054    sceneConfig.canvasHeight = Number(canonicalPayload.canvasHeight);
1055  }
1056  if (splitState.runtime.scene) {
1057    sceneConfig.scene = stripSerializationHints(splitState.runtime.scene);
1058  }
1059  if (splitState.runtime.camera) {
1060    sceneConfig.camera = stripSerializationHints(splitState.runtime.camera);
1061  }
1062  if (splitState.runtime.renderer) {
1063    sceneConfig.renderer = stripSerializationHints(splitState.runtime.renderer);
1064  }
1065  if (splitState.runtime.controls) {
1066    sceneConfig.controls = stripSerializationHints(splitState.runtime.controls);
1067  }
1068  if (splitState.runtime.lights.length > 0) {
1069    sceneConfig.lights = splitState.runtime.lights.map(stripSerializationHints);
1070  }
1071  if (splitState.runtime.renderLoop) {
1072    sceneConfig.renderLoop = stripSerializationHints(splitState.runtime.renderLoop);
1073  }
1074  const helpersConfig = normalizeHelpersConfig(sourcePayload.sceneConfig, worldInfoSource);
1075  const canonicalHelpers = canonicalizeHelpersForSceneConfig(helpersConfig);
1076  if (canonicalHelpers) {
1077    sceneConfig.helpers = canonicalHelpers;
1078  }
1079  const introConfig = normalizeIntroConfig(sourcePayload.sceneConfig?.intro);
1080  if (introConfig) {
1081    sceneConfig.intro = introConfig;
1082  }
1083
1084  const mergedExtensions = mergeExtensionMaps(
1085    isPlainObject(sourcePayload.extensions) ? sourcePayload.extensions : null
1086  );
1087  if (Object.keys(mergedExtensions).length > 0) {
1088    sceneConfig.extensions = mergedExtensions;
1089  }
1090
1091  const compatPayload = {
1092    version: canonicalPayload.version,
1093    name: canonicalPayload.name,
1094    sceneConfig,
1095    worldInfo
1096  };
1097  copySceneDocumentMetadata(sourcePayload, compatPayload);
1098  if (typeof sourcePayload.threeJsonId === "string" && sourcePayload.threeJsonId) {
1099    compatPayload.threeJsonId = sourcePayload.threeJsonId;
1100  } else if (typeof canonicalPayload.threeJsonId === "string" && canonicalPayload.threeJsonId) {
1101    compatPayload.threeJsonId = canonicalPayload.threeJsonId;
1102  }
1103  if (isPlainObject(sourcePayload.friendlyMap)) {
1104    compatPayload.friendlyMap = sourcePayload.friendlyMap;
1105  }
1106  if (isFiniteNumber(canonicalPayload.canvasWidth)) {
1107    compatPayload.canvasWidth = Number(canonicalPayload.canvasWidth);
1108  }
1109  if (isFiniteNumber(canonicalPayload.canvasHeight)) {
1110    compatPayload.canvasHeight = Number(canonicalPayload.canvasHeight);
1111  }
1112  return compatPayload;
1113}
1114
1115function clonePlainValue(value) {
1116  if (!isPlainObject(value) && !Array.isArray(value)) {
1117    return value;
1118  }
1119  try {
1120    return JSON.parse(JSON.stringify(value));
1121  } catch (_error) {
1122    if (Array.isArray(value)) {
1123      return value.slice();
1124    }
1125    return { ...value };
1126  }
1127}
1128
1129function stripFriendlyDefaults(value, defaults) {
1130  if (defaults === undefined) {
1131    return clonePlainValue(value);
1132  }
1133  if (isPlainObject(value) && isPlainObject(defaults)) {
1134    const next = {};
1135    const keys = Object.keys(value);
1136    for (let i = 0; i < keys.length; i++) {
1137      const key = keys[i];
1138      const pruned = stripFriendlyDefaults(value[key], defaults[key]);
1139      if (pruned !== undefined) {
1140        next[key] = pruned;
1141      }
1142    }
1143    return Object.keys(next).length > 0 ? next : undefined;
1144  }
1145  if (Array.isArray(value) || Array.isArray(defaults)) {
1146    return clonePlainValue(value);
1147  }
1148  return Object.is(value, defaults) ? undefined : clonePlainValue(value);
1149}
1150
1151function matchesFriendlyDefaults(recordValue, defaults) {
1152  if (defaults === undefined) {
1153    return true;
1154  }
1155  if (isPlainObject(defaults)) {
1156    if (!isPlainObject(recordValue)) {
1157      return false;
1158    }
1159    const keys = Object.keys(defaults);
1160    for (let i = 0; i < keys.length; i++) {
1161      const key = keys[i];
1162      if (!matchesFriendlyDefaults(recordValue[key], defaults[key])) {
1163        return false;
1164      }
1165    }
1166    return true;
1167  }
1168  if (Array.isArray(defaults)) {
1169    return JSON.stringify(recordValue) === JSON.stringify(defaults);
1170  }
1171  return Object.is(recordValue, defaults);
1172}
1173
1174function matchesFriendlyListDefinition(record, definition) {
1175  if (!definition || typeof definition !== "object") {
1176    return false;
1177  }
1178  if (definition.objType && normalizeSceneObjType(record?.objType) !== normalizeSceneObjType(definition.objType)) {
1179    return false;
1180  }
1181  if (isPlainObject(definition.defaults) && Object.keys(definition.defaults).length > 0) {
1182    return matchesFriendlyDefaults(record, definition.defaults);
1183  }
1184  return Boolean(definition.objType);
1185}
1186
1187function pruneFriendlyExportRecord(record, definition = {}) {
1188  const next = clonePlainValue(record);
1189  if (!next || typeof next !== "object") {
1190    return next;
1191  }
1192  if (definition.objType && definition.allowObjTypeOmit
1193    && normalizeSceneObjType(next.objType) === normalizeSceneObjType(definition.objType)) {
1194    delete next.objType;
1195  }
1196  if (isPlainObject(definition.defaults) && Object.keys(definition.defaults).length > 0) {
1197    const pruned = stripFriendlyDefaults(next, definition.defaults);
1198    return pruned && typeof pruned === "object" ? pruned : next;
1199  }
1200  return next;
1201}
1202
1203function resolveFriendlyFallbackListName(record) {
1204  const objType = normalizeSceneObjType(record?.objType);
1205  if (objType === "sphere") {
1206    return "sphereModelList";
1207  }
1208  if (objType === "group") {
1209    return "groupList";
1210  }
1211  if (objType === "line") {
1212    return "lineList";
1213  }
1214  if (objType === "infopanel") {
1215    return "infoPanelList";
1216  }
1217  if (objType === "css3dpanel") {
1218    return "css3dPanelList";
1219  }
1220  if (objType === "heatmap") {
1221    return "heatList";
1222  }
1223  if (objType === "wind") {
1224    return "windList";
1225  }
1226  if (objType === "shadersurface") {
1227    return "shaderSurfaceList";
1228  }
1229  if (objType === "plane") {
1230    return "planeList";
1231  }
1232  if (objType === "points") {
1233    return "particleList";
1234  }
1235  if (objType === "sprite") {
1236    return "spriteList";
1237  }
1238  if (objType === "tube") {
1239    return "tubeList";
1240  }
1241  if (objType === "instanced") {
1242    return "instancedList";
1243  }
1244  if (objType === "skinned") {
1245    return "skinnedList";
1246  }
1247  if (objType === "externalmodel") {
1248    return shouldMirrorIntoCompatObjModelList(record) ? "objModelList" : "externalModelList";
1249  }
1250  if (objType === "domain") {
1251    return "domainModelList";
1252  }
1253  if (objType === "shapeplane") {
1254    return "shapePlaneList";
1255  }
1256  if (objType === "irregularplane") {
1257    return "irregularPlaneList";
1258  }
1259  if (objType === "buffermesh") {
1260    return "bufferMeshList";
1261  }
1262  if (objType === "editablemesh") {
1263    return "editableMeshList";
1264  }
1265  if (objType === "shapeextrude") {
1266    return "shapeExtrudeList";
1267  }
1268  if (objType === "irregulargeometry") {
1269    return "irregularGeometryList";
1270  }
1271  if (objType === "box") {
1272    return "meshList";
1273  }
1274  if (CANONICAL_PRIMITIVE_OBJ_TYPES.has(objType)) {
1275    return "boxModelList";
1276  }
1277  return "modelList";
1278}
1279
1280function createFriendlyWorldInfoBase(sourceWorldInfo = {}, sourcePayload = {}) {
1281  const next = {};
1282  const structuralLists = new Set(getFriendlySceneListEntries(sourcePayload).map(({ listName }) => listName));
1283  for (const key of Object.keys(sourceWorldInfo)) {
1284    if (key === "friendlyMap") {
1285      continue;
1286    }
1287    if (Array.isArray(sourceWorldInfo[key]) && structuralLists.has(key)) {
1288      continue;
1289    }
1290    next[key] = clonePlainValue(sourceWorldInfo[key]);
1291  }
1292  return next;
1293}
1294
1295function cloneNonStructuralSceneRoot(sourcePayload = {}) {
1296  const next = {};
1297  const friendlyListNames = new Set(
1298    getFriendlySceneListEntries(sourcePayload).map((definition) => definition.listName)
1299  );
1300  for (const key of Object.keys(sourcePayload)) {
1301    if (
1302      key === "worldInfo" ||
1303      key === "objectList" ||
1304      key === "sceneConfig" ||
1305      key === "friendlyMap" ||
1306      key === "subSceneList" ||
1307      friendlyListNames.has(key)
1308    ) {
1309      continue;
1310    }
1311    next[key] = clonePlainValue(sourcePayload[key]);
1312  }
1313  return next;
1314}
1315
1316function stripSerializationHints(record) {
1317  const next = stripObjType(record);
1318  delete next.jsonOrigin;
1319  return next;
1320}
1321
1322/**
1323 * Project canonical all-record objectList data into the public standard scheme B used by the
1324 * editor and AI: runtime configuration in sceneConfig, deployable records in objectList.
1325 */
1326function buildStandardScenePayloadFromCanonical(sourcePayload = {}, canonicalPayload = {}) {
1327  const root = cloneNonStructuralSceneRoot(sourcePayload);
1328  // Unknown metadata and native scene embeds are authoring data too. Strip only
1329  // lists actually consumed by the adapter, not every property ending in List.
1330  const residualWorldInfo = createFriendlyWorldInfoBase(sourcePayload.worldInfo || {}, sourcePayload);
1331  if (Object.keys(residualWorldInfo).length) root.worldInfo = residualWorldInfo;
1332  const sceneConfig = isPlainObject(sourcePayload.sceneConfig)
1333    ? clonePlainValue(sourcePayload.sceneConfig)
1334    : {};
1335  const objectList = [];
1336  const claimedConfigTypes = new Set();
1337  let configLightsInitialized = false;
1338  let configPassesInitialized = false;
1339  const runtimeFieldByType = {
1340    scene: "scene",
1341    camera: "camera",
1342    renderer: "renderer",
1343    controls: "controls",
1344    renderloop: "renderLoop"
1345  };
1346  const records = listOr(canonicalPayload.objectList);
1347
1348  for (let i = 0; i < records.length; i += 1) {
1349    const record = clonePlainValue(records[i]);
1350    const objType = normalizeSceneObjType(record?.objType);
1351    if (objType === "pass") {
1352      if (!configPassesInitialized) {
1353        sceneConfig.passList = [];
1354        configPassesInitialized = true;
1355      }
1356      sceneConfig.passList.push(stripSerializationHints(record));
1357      continue;
1358    }
1359    if (objType === "light") {
1360      const fromConfig = record.jsonOrigin === JSON_ORIGIN_CONFIG || record.jsonOrigin !== JSON_ORIGIN_LIST;
1361      if (fromConfig) {
1362        if (!configLightsInitialized) {
1363          sceneConfig.lights = [];
1364          configLightsInitialized = true;
1365        }
1366        sceneConfig.lights.push(stripSerializationHints(record));
1367      } else {
1368        delete record.jsonOrigin;
1369        objectList.push(record);
1370      }
1371      continue;
1372    }
1373    const runtimeField = runtimeFieldByType[objType];
1374    if (runtimeField) {
1375      const fromConfig =
1376        record.jsonOrigin === JSON_ORIGIN_CONFIG ||
1377        (record.jsonOrigin !== JSON_ORIGIN_LIST && !claimedConfigTypes.has(objType));
1378      if (fromConfig) {
1379        sceneConfig[runtimeField] = stripSerializationHints(record);
1380        claimedConfigTypes.add(objType);
1381      } else {
1382        delete record.jsonOrigin;
1383        objectList.push(record);
1384      }
1385      continue;
1386    }
1387    delete record.jsonOrigin;
1388    objectList.push(record);
1389  }
1390
1391  const payload = {
1392    ...root,
1393    version: canonicalPayload.version || sourcePayload.version || "next"
1394  };
1395  if (Object.keys(sceneConfig).length > 0) {
1396    payload.sceneConfig = sceneConfig;
1397  }
1398  if (objectList.length > 0) {
1399    payload.objectList = objectList;
1400  }
1401  if (typeof canonicalPayload.name === "string" && canonicalPayload.name) {
1402    payload.name = canonicalPayload.name;
1403  }
1404  if (typeof canonicalPayload.threeJsonId === "string" && canonicalPayload.threeJsonId) {
1405    payload.threeJsonId = canonicalPayload.threeJsonId;
1406  }
1407  return payload;
1408}
1409
1410function buildFriendlyScenePayloadFromCanonical(sourcePayload = {}, canonicalPayload = {}, options = {}) {
1411  const splitState = splitCanonicalObjectList(canonicalPayload.objectList);
1412  const sourceWorldInfo = sourcePayload.worldInfo && typeof sourcePayload.worldInfo === "object"
1413    ? sourcePayload.worldInfo
1414    : {};
1415  const customFriendlyMap = isPlainObject(options.friendlyMap) ? clonePlainValue(options.friendlyMap) : {};
1416  const resolvedFriendlyMap = resolveFriendlySceneMap({ friendlyMap: customFriendlyMap });
1417  const customListNames = Object.keys(customFriendlyMap);
1418  const worldInfo = createFriendlyWorldInfoBase(sourceWorldInfo, sourcePayload);
1419
1420  for (let i = 0; i < splitState.contentList.length; i++) {
1421    const record = splitState.contentList[i];
1422    let targetListName = "";
1423    for (let j = 0; j < customListNames.length; j++) {
1424      const listName = customListNames[j];
1425      const definition = resolvedFriendlyMap[listName];
1426      if (matchesFriendlyListDefinition(record, definition)) {
1427        targetListName = listName;
1428        break;
1429      }
1430    }
1431    if (!targetListName) {
1432      targetListName = resolveFriendlyFallbackListName(record);
1433    }
1434    const definition = resolvedFriendlyMap[targetListName] || {};
1435    if (!Array.isArray(worldInfo[targetListName])) {
1436      worldInfo[targetListName] = [];
1437    }
1438    worldInfo[targetListName].push(pruneFriendlyExportRecord(record, definition));
1439  }
1440
1441  const sceneConfig = isPlainObject(sourcePayload.sceneConfig)
1442    ? clonePlainValue(sourcePayload.sceneConfig)
1443    : {};
1444  if (isFiniteNumber(canonicalPayload.canvasWidth)) {
1445    sceneConfig.canvasWidth = Number(canonicalPayload.canvasWidth);
1446  }
1447  if (isFiniteNumber(canonicalPayload.canvasHeight)) {
1448    sceneConfig.canvasHeight = Number(canonicalPayload.canvasHeight);
1449  }
1450  if (splitState.runtime.scene) {
1451    sceneConfig.scene = stripSerializationHints(splitState.runtime.sce
1451ne);
1452  }
1453  if (splitState.runtime.camera) {
1454    sceneConfig.camera = stripSerializationHints(splitState.runtime.camera);
1455  }
1456  if (splitState.runtime.renderer) {
1457    sceneConfig.renderer = stripSerializationHints(splitState.runtime.renderer);
1458  }
1459  if (splitState.runtime.controls) {
1460    sceneConfig.controls = stripSerializationHints(splitState.runtime.controls);
1461  }
1462  if (splitState.runtime.lights.length > 0) {
1463    sceneConfig.lights = splitState.runtime.lights.map(stripSerializationHints);
1464  }
1465  if (splitState.runtime.renderLoop) {
1466    sceneConfig.renderLoop = stripSerializationHints(splitState.runtime.renderLoop);
1467  }
1468  const helpersFromSource = normalizeHelpersConfig(sourcePayload?.sceneConfig, sourcePayload?.worldInfo);
1469  const canonicalHelpers = canonicalizeHelpersForSceneConfig(helpersFromSource);
1470  if (canonicalHelpers) {
1471    sceneConfig.helpers = canonicalHelpers;
1472  }
1473  const introFromSource = normalizeIntroConfig(sourcePayload?.sceneConfig?.intro);
1474  if (introFromSource) {
1475    sceneConfig.intro = introFromSource;
1476  }
1477
1478  const mergedExtensions = mergeExtensionMaps(
1479    isPlainObject(worldInfo.extensions) ? worldInfo.extensions : null,
1480    isPlainObject(sceneConfig.extensions) ? sceneConfig.extensions : null
1481  );
1482  if (Object.keys(mergedExtensions).length > 0) {
1483    sceneConfig.extensions = mergedExtensions;
1484  }
1485
1486  const friendlyPayload = {
1487    ...cloneNonStructuralSceneRoot(sourcePayload),
1488    version: canonicalPayload.version || sourcePayload.version || "next",
1489    sceneConfig,
1490    worldInfo
1491  };
1492  if (typeof canonicalPayload.name === "string" && canonicalPayload.name) {
1493    friendlyPayload.name = canonicalPayload.name;
1494  }
1495  if (typeof sourcePayload.threeJsonId === "string" && sourcePayload.threeJsonId) {
1496    friendlyPayload.threeJsonId = sourcePayload.threeJsonId;
1497  } else if (typeof canonicalPayload.threeJsonId === "string" && canonicalPayload.threeJsonId) {
1498    friendlyPayload.threeJsonId = canonicalPayload.threeJsonId;
1499  }
1500  if (customListNames.length > 0) {
1501    friendlyPayload.friendlyMap = customFriendlyMap;
1502  }
1503  return friendlyPayload;
1504}
1505
1506function applySubSceneNormalizationToCanonical(sourcePayload, canonicalPayload, options = {}) {
1507  const subSceneList = Array.isArray(sourcePayload?.subSceneList) ? sourcePayload.subSceneList : [];
1508  const policy = options.subSceneNormalizePolicy === "strict" ? "strict" : "warn";
1509  const sceneDocId = typeof canonicalPayload?.threeJsonId === "string"
1510    ? canonicalPayload.threeJsonId.trim()
1511    : typeof sourcePayload?.threeJsonId === "string"
1512      ? sourcePayload.threeJsonId.trim()
1513      : "";
1514  const { payload: normalizedCanonical } = normalizeSubSceneHierarchy(canonicalPayload, {
1515    policy,
1516    sceneDocId,
1517    subSceneList
1518  });
1519  return normalizedCanonical;
1520}
1521
1522function buildNormalizedScenePayload(sourcePayload, canonicalPayload, defaultModelTemplate = null, options = {}) {
1523  ensureThreeJsonIdsOnScenePayload(canonicalPayload);
1524  const canonicalWithSubScene = applySubSceneNormalizationToCanonical(sourcePayload, canonicalPayload, options);
1525  const splitState = splitCanonicalObjectList(canonicalWithSubScene.objectList);
1526  const compatPayload = buildCompatPayloadFromCanonical(sourcePayload, canonicalWithSubScene, splitState);
1527  ensureThreeJsonIdsOnScenePayload(compatPayload);
1528  const helpersConfig = normalizeHelpersConfig(compatPayload.sceneConfig, compatPayload.worldInfo);
1529  const lightConfig =
1530    splitState.runtime.lights.length > 0
1531      ? {
1532        lights: splitState.runtime.lights.map(stripObjType),
1533        hasExplicitLights: true,
1534        lightsMeta: {
1535          lightsKeyPresent: true,
1536          explicitEmpty: false,
1537          explicitLights: splitState.runtime.lights.map(stripObjType),
1538          hasExplicitNonEmpty: true
1539        }
1540      }
1541      : normalizeLightsConfig(compatPayload.sceneConfig, compatPayload.worldInfo);
1542  return {
1543    sourcePayload,
1544    payload: canonicalWithSubScene,
1545    compatPayload,
1546    defaultModelTemplate,
1547    friendlyMap: resolveFriendlySceneMap(sourcePayload),
1548    worldInfo: compatPayload.worldInfo,
1549    sceneConfig: compatPayload.sceneConfig,
1550    sceneHints: stripObjType(splitState.runtime.scene),
1551    cameraConfig: stripObjType(splitState.runtime.camera),
1552    controlsConfig: stripObjType(splitState.runtime.controls),
1553    rendererConfig: stripObjType(splitState.runtime.renderer),
1554    renderLoopConfig: stripObjType(splitState.runtime.renderLoop),
1555    lightsConfig: lightConfig.lights,
1556    hasExplicitLights: lightConfig.hasExplicitLights,
1557    lightsMeta: lightConfig.lightsMeta,
1558    helpersConfig,
1559    objectList: canonicalWithSubScene.objectList,
1560    nativeSceneEntry: normalizeNativeSceneEntry(sourcePayload.worldInfo || compatPayload.worldInfo),
1561    canvasWidth: compatPayload.canvasWidth ?? compatPayload.sceneConfig.canvasWidth,
1562    canvasHeight: compatPayload.canvasHeight ?? compatPayload.sceneConfig.canvasHeight
1563  };
1564}
1565
1566function hasSceneConfigPrimaryRuntime(payload = {}) {
1567  const sceneConfig = payload.sceneConfig;
1568  if (!sceneConfig || typeof sceneConfig !== "object") {
1569    return false;
1570  }
1571  if (hasOwn(sceneConfig, "camera") || hasOwn(sceneConfig, "controls") || hasOwn(sceneConfig, "renderer")) {
1572    return true;
1573  }
1574  if (hasOwn(sceneConfig, "scene") || hasOwn(sceneConfig, "renderLoop")) {
1575    return true;
1576  }
1577  return Array.isArray(sceneConfig.lights) && sceneConfig.lights.length > 0;
1578}
1579
1580function isLoadableScenePayload(payload = {}) {
1581  if (!payload || typeof payload !== "object") {
1582    return false;
1583  }
1584  if (payload.worldInfo && typeof payload.worldInfo === "object") {
1585    return true;
1586  }
1587  if (Array.isArray(payload.objectList) && payload.objectList.length > 0) {
1588    return true;
1589  }
1590  return hasSceneConfigPrimaryRuntime(payload);
1591}
1592
1593function normalizeCanonicalScenePayload(payload = {}, options = {}) {
1594  const sourcePayload = cloneJsonPayload(payload);
1595  ensureThreeJsonIdsOnScenePayload(sourcePayload);
1596  const extracted = extractDefaultModelFromScenePayload(sourcePayload);
1597  const canonicalPayload = buildCanonicalScenePayloadFromCanonical(extracted.payload);
1598  return buildNormalizedScenePayload(sourcePayload, canonicalPayload, extracted.template, options);
1599}
1600
1601function normalizeFriendlyScenePayload(payload = {}, options = {}) {
1602  const sourcePayload = cloneJsonPayload(payload);
1603  ensureThreeJsonIdsOnScenePayload(sourcePayload);
1604  const liftedPayload = liftWorldInfoRuntimeSugarToSceneConfig(sourcePayload);
1605  const extracted = extractDefaultModelFromScenePayload(liftedPayload);
1606  const canonicalPayload = buildCanonicalScenePayloadFromFriendly(extracted.payload);
1607  return buildNormalizedScenePayload(liftedPayload, canonicalPayload, extracted.template, options);
1608}
1609
1610function hasNonEmptyObjectList(payload = {}) {
1611  return Array.isArray(payload?.objectList) && payload.objectList.length > 0;
1612}
1613
1614function hasFriendlyWorldInfo(payload = {}) {
1615  return payload.worldInfo && typeof payload.worldInfo === "object";
1616}
1617
1618function shouldNormalizeAsFriendly(payload = {}) {
1619  if (!hasFriendlyWorldInfo(payload)) {
1620    return false;
1621  }
1622  // When plan B (objectList + sceneConfig) coexists with friendly JSON, objectList wins
1623  if (hasNonEmptyObjectList(payload)) {
1624    return false;
1625  }
1626  return true;
1627}
1628
1629/**
1630 * Decide which JSON form a scene package should show in editor/Code view.
1631 * @param {object} [payload]
1632 * @returns {"friendly"|"standard"}
1633 */
1634function detectScenePayloadViewFormat(payload = {}) {
1635  return shouldNormalizeAsFriendly(payload) ? "friendly" : "standard";
1636}
1637
1638function normalizeScenePayload(payload = {}, options = {}) {
1639  if (shouldNormalizeAsFriendly(payload)) {
1640    return normalizeFriendlyScenePayload(payload, options);
1641  }
1642  // objectList-only, objectList + sceneConfig, and sceneConfig-only main runtime all use canonical normalization
1643  if (
1644    isCanonicalScenePayload(payload)
1645    || hasNonEmptyObjectList(payload)
1646    || hasSceneConfigPrimaryRuntime(payload)
1647  ) {
1648    return normalizeCanonicalScenePayload(payload, options);
1649  }
1650  return normalizeFriendlyScenePayload(payload, options);
1651}
1652
1653export {
1654  CANONICAL_PRIMITIVE_OBJ_TYPES,
1655  buildFriendlyScenePayloadFromCanonical,
1656  buildStandardScenePayloadFromCanonical,
1657  detectScenePayloadViewFormat,
1658  hasSceneConfigPrimaryRuntime,
1659  isCanonicalScenePayload,
1660  isLoadableScenePayload,
1661  normalizeCanonicalObjectRecord,
1662  normalizeFriendlyScenePayload,
1663  normalizeSceneObjType,
1664  normalizeScenePayload,
1665  shouldNormalizeAsFriendly
1666};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.