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.