@vizij/render 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -974,6 +974,48 @@ function InnerRenderedShape({
974
974
  );
975
975
  }
976
976
  },
977
+ roughness: (value) => {
978
+ const standard = material.current;
979
+ if (standard?.roughness !== void 0 && instanceOfRawNumber4(value)) {
980
+ standard.roughness = value;
981
+ standard.needsUpdate = true;
982
+ }
983
+ },
984
+ metalness: (value) => {
985
+ const standard = material.current;
986
+ if (standard?.metalness !== void 0 && instanceOfRawNumber4(value)) {
987
+ standard.metalness = value;
988
+ standard.needsUpdate = true;
989
+ }
990
+ },
991
+ shininess: (value) => {
992
+ const phong = material.current;
993
+ if (phong?.shininess !== void 0 && instanceOfRawNumber4(value)) {
994
+ phong.shininess = value;
995
+ phong.needsUpdate = true;
996
+ }
997
+ },
998
+ emissive: (value) => {
999
+ const standard = material.current;
1000
+ if (standard?.emissive && instanceOfRawRGB3(value)) {
1001
+ standard.emissive.setRGB(value.r, value.g, value.b);
1002
+ standard.needsUpdate = true;
1003
+ }
1004
+ },
1005
+ emissiveIntensity: (value) => {
1006
+ const standard = material.current;
1007
+ if (standard?.emissiveIntensity !== void 0 && instanceOfRawNumber4(value)) {
1008
+ standard.emissiveIntensity = value;
1009
+ standard.needsUpdate = true;
1010
+ }
1011
+ },
1012
+ specular: (value) => {
1013
+ const phong = material.current;
1014
+ if (phong?.specular && instanceOfRawRGB3(value)) {
1015
+ phong.specular.setRGB(value.r, value.g, value.b);
1016
+ phong.needsUpdate = true;
1017
+ }
1018
+ },
977
1019
  ...morphTargetSettings[2]
978
1020
  },
979
1021
  shape
@@ -1094,7 +1136,10 @@ var MATERIAL_FEATURE_KEYS = [
1094
1136
  "opacity",
1095
1137
  "roughness",
1096
1138
  "metalness",
1097
- "shininess"
1139
+ "shininess",
1140
+ "emissive",
1141
+ "emissiveIntensity",
1142
+ "specular"
1098
1143
  ];
1099
1144
  var MATERIAL_NAME_SUFFIXES = [
1100
1145
  " color",
@@ -1103,7 +1148,10 @@ var MATERIAL_NAME_SUFFIXES = [
1103
1148
  " opacity",
1104
1149
  " roughness",
1105
1150
  " metalness",
1106
- " shininess"
1151
+ " shininess",
1152
+ " emissive intensity",
1153
+ " emissive",
1154
+ " specular"
1107
1155
  ];
1108
1156
  function deriveMaterialName(shape, values) {
1109
1157
  for (const key of MATERIAL_FEATURE_KEYS) {
@@ -1217,12 +1265,13 @@ import { createRef } from "react";
1217
1265
  import { mapValues as mapValues2 } from "lodash";
1218
1266
 
1219
1267
  // src/functions/create-world-element.ts
1268
+ import { createBrowserSafeId } from "@vizij/utils";
1220
1269
  function createDefaultGroup(partialBody) {
1221
1270
  const translation = partialBody.features?.translation || { animated: false, value: { x: 0, y: 0 } };
1222
1271
  const rotation = partialBody.features?.rotation || { animated: false, value: { x: 0, y: 0, z: 0 } };
1223
1272
  const scale = partialBody.features?.scale || { animated: false, value: { x: 1, y: 1, z: 1 } };
1224
1273
  return {
1225
- id: partialBody.id || crypto.randomUUID(),
1274
+ id: partialBody.id || createBrowserSafeId(),
1226
1275
  name: partialBody.name || "new-body",
1227
1276
  type: partialBody.type || "group",
1228
1277
  tags: partialBody.tags || [],
@@ -1283,7 +1332,32 @@ function removeFromTree(state, nodesToRemove) {
1283
1332
  });
1284
1333
  }
1285
1334
 
1335
+ // src/memoryInvestigation.ts
1336
+ function getGlobalMemoryInvestigation() {
1337
+ return globalThis;
1338
+ }
1339
+ function updateRenderState(updater) {
1340
+ const globalObj = getGlobalMemoryInvestigation();
1341
+ if (!globalObj.__VIZIJ_MEMORY_INVESTIGATION__?.enabled) {
1342
+ return;
1343
+ }
1344
+ if (!globalObj.__vizijMemoryDebugState) {
1345
+ globalObj.__vizijMemoryDebugState = {};
1346
+ }
1347
+ if (!globalObj.__vizijMemoryDebugState.render) {
1348
+ globalObj.__vizijMemoryDebugState.render = {};
1349
+ }
1350
+ updater(globalObj.__vizijMemoryDebugState.render);
1351
+ }
1352
+ function recordRenderCounter(key, delta = 1) {
1353
+ updateRenderState((renderState) => {
1354
+ const current = typeof renderState[key] === "number" ? Number(renderState[key]) : 0;
1355
+ renderState[key] = current + delta;
1356
+ });
1357
+ }
1358
+
1286
1359
  // src/functions/create-animatable.ts
1360
+ import { createBrowserSafeId as createBrowserSafeId2 } from "@vizij/utils";
1287
1361
  function createAnimatable(value) {
1288
1362
  if (!value.type) {
1289
1363
  return null;
@@ -1293,7 +1367,7 @@ function createAnimatable(value) {
1293
1367
  }
1294
1368
  if (value.type === "euler") {
1295
1369
  const newAnimatable = {
1296
- id: value.id ?? crypto.randomUUID(),
1370
+ id: value.id ?? createBrowserSafeId2(),
1297
1371
  name: `${value.name} Rotation`,
1298
1372
  type: value.type,
1299
1373
  default: value.default ?? { x: 0, y: 0, z: 0 },
@@ -1310,7 +1384,7 @@ function createAnimatable(value) {
1310
1384
  return newAnimatable;
1311
1385
  } else if (value.type === "vector3") {
1312
1386
  const newAnimatable = {
1313
- id: value.id ?? crypto.randomUUID(),
1387
+ id: value.id ?? createBrowserSafeId2(),
1314
1388
  name: `${value.name} Vector3`,
1315
1389
  type: value.type,
1316
1390
  default: value.default ?? { x: 0, y: 0, z: 0 },
@@ -1327,7 +1401,7 @@ function createAnimatable(value) {
1327
1401
  return newAnimatable;
1328
1402
  } else if (value.type === "string") {
1329
1403
  const newAnimatable = {
1330
- id: value.id ?? crypto.randomUUID(),
1404
+ id: value.id ?? createBrowserSafeId2(),
1331
1405
  name: value.name,
1332
1406
  type: value.type,
1333
1407
  default: value.default ?? "Hello World",
@@ -1340,7 +1414,7 @@ function createAnimatable(value) {
1340
1414
  return newAnimatable;
1341
1415
  } else if (value.type === "number") {
1342
1416
  const newAnimatable = {
1343
- id: value.id ?? crypto.randomUUID(),
1417
+ id: value.id ?? createBrowserSafeId2(),
1344
1418
  name: value.name,
1345
1419
  type: value.type,
1346
1420
  default: value.default ?? 0,
@@ -1353,7 +1427,7 @@ function createAnimatable(value) {
1353
1427
  return newAnimatable;
1354
1428
  } else if (value.type === "boolean") {
1355
1429
  const newAnimatable = {
1356
- id: value.id ?? crypto.randomUUID(),
1430
+ id: value.id ?? createBrowserSafeId2(),
1357
1431
  name: value.name,
1358
1432
  type: value.type,
1359
1433
  default: value.default ?? false,
@@ -1366,7 +1440,7 @@ function createAnimatable(value) {
1366
1440
  return newAnimatable;
1367
1441
  } else if (value.type === "rgb") {
1368
1442
  const newAnimatable = {
1369
- id: value.id ?? crypto.randomUUID(),
1443
+ id: value.id ?? createBrowserSafeId2(),
1370
1444
  name: value.name,
1371
1445
  type: value.type,
1372
1446
  default: value.default ?? { r: 0, g: 0, b: 0 },
@@ -1383,7 +1457,7 @@ function createAnimatable(value) {
1383
1457
  return newAnimatable;
1384
1458
  } else if (value.type === "hsl") {
1385
1459
  const newAnimatable = {
1386
- id: value.id ?? crypto.randomUUID(),
1460
+ id: value.id ?? createBrowserSafeId2(),
1387
1461
  name: value.name,
1388
1462
  type: value.type,
1389
1463
  default: value.default ?? { h: 0, s: 0, l: 0 },
@@ -1403,6 +1477,35 @@ function createAnimatable(value) {
1403
1477
  }
1404
1478
  createAnimatable({ type: "euler", name: "Rotation" });
1405
1479
 
1480
+ // src/functions/exportable-bodies.ts
1481
+ function asGroupEntries(world) {
1482
+ return Object.values(world).filter((entry) => entry.type === "group");
1483
+ }
1484
+ function selectExportableGroupEntries(world, filterIds) {
1485
+ const groupEntries = asGroupEntries(world);
1486
+ const filterSet = Array.isArray(filterIds) && filterIds.length > 0 ? new Set(filterIds) : null;
1487
+ if (filterSet) {
1488
+ return groupEntries.filter((entry) => filterSet.has(entry.id));
1489
+ }
1490
+ const rootBoundsGroups = groupEntries.filter(
1491
+ (entry) => Boolean(entry.rootBounds)
1492
+ );
1493
+ if (rootBoundsGroups.length > 0) {
1494
+ return rootBoundsGroups;
1495
+ }
1496
+ const explicitRootGroups = groupEntries.filter(
1497
+ (entry) => entry.root === true
1498
+ );
1499
+ if (explicitRootGroups.length > 0) {
1500
+ return explicitRootGroups;
1501
+ }
1502
+ const topLevelGroups = groupEntries.filter((entry) => !entry.parent);
1503
+ if (topLevelGroups.length > 0) {
1504
+ return topLevelGroups;
1505
+ }
1506
+ return groupEntries;
1507
+ }
1508
+
1406
1509
  // src/store.ts
1407
1510
  THREE3.Object3D.DEFAULT_UP.set(0, 0, 1);
1408
1511
  enableMapSet();
@@ -1478,23 +1581,17 @@ var VizijSlice = (set, get) => ({
1478
1581
  },
1479
1582
  getExportableBodies: (filterIds) => {
1480
1583
  const worldData = get().world;
1481
- if (!filterIds) {
1482
- const bodies = Object.values(worldData).filter((entry) => entry.type === "group" && entry.rootBounds).map((entry) => {
1483
- const firstNs = Object.keys(entry.refs)[0];
1484
- const refGroup = entry.refs[firstNs].current;
1485
- return refGroup;
1486
- });
1487
- return bodies;
1488
- } else {
1489
- const bodies = Object.values(worldData).filter(
1490
- (entry) => entry.type === "group" && entry.rootBounds && filterIds.includes(entry.id)
1491
- ).map((entry) => {
1492
- const firstNs = Object.keys(entry.refs)[0];
1493
- const refGroup = entry.refs[firstNs].current;
1494
- return refGroup;
1495
- });
1496
- return bodies;
1497
- }
1584
+ const candidateGroups = selectExportableGroupEntries(
1585
+ worldData,
1586
+ filterIds
1587
+ );
1588
+ return candidateGroups.flatMap((entry) => {
1589
+ const refs = Object.values(
1590
+ entry.refs ?? {}
1591
+ );
1592
+ const resolved = refs.find((ref) => ref?.current)?.current ?? null;
1593
+ return resolved ? [resolved] : [];
1594
+ });
1498
1595
  },
1499
1596
  setGeometry: (id, geometry) => {
1500
1597
  set(
@@ -1692,6 +1789,7 @@ var VizijSlice = (set, get) => ({
1692
1789
  );
1693
1790
  },
1694
1791
  setVizij: (scene, animatables) => {
1792
+ recordRenderCounter("rootReplacementCount");
1695
1793
  set({
1696
1794
  world: scene,
1697
1795
  animatables
@@ -1699,6 +1797,7 @@ var VizijSlice = (set, get) => ({
1699
1797
  },
1700
1798
  addWorldElements(world, animatables, replace) {
1701
1799
  if (replace) {
1800
+ recordRenderCounter("rootReplacementCount");
1702
1801
  set({ world, animatables });
1703
1802
  } else {
1704
1803
  set((state) => ({
@@ -1751,7 +1850,7 @@ import {
1751
1850
  import { jsx as jsx6 } from "react/jsx-runtime";
1752
1851
  function SelectionGlowEffect({
1753
1852
  enabled = false,
1754
- color = "#ff1010ff",
1853
+ color = "#ff1010",
1755
1854
  opacity = 0.9,
1756
1855
  thresholdAngle = 2
1757
1856
  }) {
@@ -1857,6 +1956,13 @@ function Vizij({
1857
1956
  onPointerMissed
1858
1957
  }) {
1859
1958
  const ctx = useContext3(VizijContext);
1959
+ useEffect7(() => {
1960
+ recordRenderCounter("canvasMountCount");
1961
+ recordRenderCounter("mountedCanvasCount");
1962
+ return () => {
1963
+ recordRenderCounter("mountedCanvasCount", -1);
1964
+ };
1965
+ }, []);
1860
1966
  if (ctx) {
1861
1967
  return /* @__PURE__ */ jsx7(
1862
1968
  Canvas,
@@ -1925,7 +2031,7 @@ function InnerVizij({
1925
2031
  makeDefault: true,
1926
2032
  position: [0, 0, 100],
1927
2033
  near: 0.1,
1928
- far: 101
2034
+ far: 1e3
1929
2035
  }
1930
2036
  ),
1931
2037
  /* @__PURE__ */ jsx7(Suspense, { fallback: null, children: /* @__PURE__ */ jsx7(
@@ -2103,6 +2209,7 @@ import { Object3D as Object3D7 } from "three";
2103
2209
 
2104
2210
  // src/functions/gltf-loading/import-group.ts
2105
2211
  import { Object3D as Object3D6 } from "three";
2212
+ import { createBrowserSafeId as createBrowserSafeId5 } from "@vizij/utils";
2106
2213
 
2107
2214
  // src/functions/util.ts
2108
2215
  import { createRef as createRef2 } from "react";
@@ -2112,8 +2219,12 @@ function namespaceArrayToRefs(namespaces) {
2112
2219
 
2113
2220
  // src/functions/gltf-loading/import-mesh.ts
2114
2221
  import { Object3D as Object3D5 } from "three";
2222
+ import { createBrowserSafeId as createBrowserSafeId4 } from "@vizij/utils";
2115
2223
 
2116
2224
  // src/functions/gltf-loading/import-geometry.ts
2225
+ import {
2226
+ createBrowserSafeId as createBrowserSafeId3
2227
+ } from "@vizij/utils";
2117
2228
  function sanitizeMorphKey(name, fallbackIndex, used) {
2118
2229
  const baseName = name && name.trim().length > 0 ? name.trim() : `morph_${fallbackIndex + 1}`;
2119
2230
  const slug = baseName.toLowerCase().replace(/[^a-z0-9]+/gi, "_").replace(/^_+|_+$/g, "");
@@ -2137,7 +2248,7 @@ function importGeometry(geometry, mesh) {
2137
2248
  const usedKeys = /* @__PURE__ */ new Set();
2138
2249
  Object.entries(mesh.morphTargetDictionary ?? {}).forEach(
2139
2250
  ([name, index]) => {
2140
- const morphId = crypto.randomUUID();
2251
+ const morphId = createBrowserSafeId3();
2141
2252
  const featureKey = sanitizeMorphKey(name, index, usedKeys);
2142
2253
  morphIds.push(featureKey);
2143
2254
  features[featureKey] = {
@@ -2173,7 +2284,7 @@ function importMesh(mesh, namespaces, colorLookup) {
2173
2284
  let animatables = {};
2174
2285
  let newColorLookup = {};
2175
2286
  const translationAnimatable = {
2176
- id: crypto.randomUUID(),
2287
+ id: createBrowserSafeId4(),
2177
2288
  name: `${mesh.name ?? "Mesh"} translation`,
2178
2289
  type: "vector3",
2179
2290
  default: { x: mesh.position.x, y: mesh.position.y, z: mesh.position.z },
@@ -2189,7 +2300,7 @@ function importMesh(mesh, namespaces, colorLookup) {
2189
2300
  [translationAnimatable.id]: translationAnimatable
2190
2301
  };
2191
2302
  const rotationAnimatable = {
2192
- id: crypto.randomUUID(),
2303
+ id: createBrowserSafeId4(),
2193
2304
  name: `${mesh.name ?? "Mesh"} rotation`,
2194
2305
  type: "euler",
2195
2306
  default: { x: mesh.rotation.x, y: mesh.rotation.y, z: mesh.rotation.z },
@@ -2202,7 +2313,7 @@ function importMesh(mesh, namespaces, colorLookup) {
2202
2313
  };
2203
2314
  animatables = { ...animatables, [rotationAnimatable.id]: rotationAnimatable };
2204
2315
  const scaleAnimatable = {
2205
- id: crypto.randomUUID(),
2316
+ id: createBrowserSafeId4(),
2206
2317
  name: `${mesh.name ?? "Mesh"} scale`,
2207
2318
  type: "vector3",
2208
2319
  default: { x: mesh.scale.x, y: mesh.scale.y, z: mesh.scale.z },
@@ -2213,56 +2324,120 @@ function importMesh(mesh, namespaces, colorLookup) {
2213
2324
  }
2214
2325
  };
2215
2326
  animatables = { ...animatables, [scaleAnimatable.id]: scaleAnimatable };
2216
- const color = mesh.material.color;
2217
- let useEmissive = false;
2218
- if (color.r === 0 && color.g === 0 && color.b === 0 && mesh.material.emissive) {
2219
- color.copy(mesh.material.emissive);
2220
- useEmissive = true;
2221
- }
2222
- const colorName = mesh.material.name;
2223
- const colorAnimatable = {
2224
- id: crypto.randomUUID(),
2225
- name: mesh.material.name ?? `${mesh.name ?? "Mesh"} color`,
2226
- type: "rgb",
2227
- default: { r: color.r, g: color.g, b: color.b },
2228
- constraints: {
2229
- min: [0, 0, 0],
2230
- max: [1, 1, 1]
2231
- },
2327
+ const material = mesh.material;
2328
+ const color = material.color;
2329
+ const colorName = material.name;
2330
+ const label = (suffix) => colorName ? `${colorName} ${suffix}` : `${mesh.name ?? "Mesh"} ${suffix}`;
2331
+ const makeNumberAnimatable = (suffix, value, constraints) => ({
2332
+ id: createBrowserSafeId4(),
2333
+ name: label(suffix),
2334
+ type: "number",
2335
+ default: value,
2336
+ constraints,
2232
2337
  pub: {
2233
2338
  public: true,
2234
- output: mesh.material.name ? `${mesh.material.name} color` : `${mesh.name ?? "Mesh"} color`
2339
+ output: label(suffix)
2235
2340
  }
2236
- };
2237
- const opacityAnimatable = {
2238
- id: crypto.randomUUID(),
2239
- name: mesh.material.name ? `${mesh.material.name} opacity` : `${mesh.name ?? "Mesh"} opacity`,
2240
- type: "number",
2241
- default: mesh.material.opacity,
2341
+ });
2342
+ const makeColorAnimatable = (suffix, value) => ({
2343
+ id: createBrowserSafeId4(),
2344
+ name: label(suffix),
2345
+ type: "rgb",
2346
+ default: { r: value.r, g: value.g, b: value.b },
2242
2347
  constraints: {
2243
- min: 0,
2244
- max: 1
2348
+ min: [0, 0, 0],
2349
+ max: [1, 1, 1]
2245
2350
  },
2246
2351
  pub: {
2247
2352
  public: true,
2248
- output: mesh.material.name ? `${mesh.material.name} opacity` : `${mesh.name ?? "Mesh"} opacity`
2353
+ output: label(suffix)
2249
2354
  }
2355
+ });
2356
+ const colorAnimatable = {
2357
+ ...makeColorAnimatable("color", color),
2358
+ name: material.name ?? `${mesh.name ?? "Mesh"} color`
2250
2359
  };
2251
- let colorId = colorAnimatable.id;
2252
- let opacityId = opacityAnimatable.id;
2360
+ const opacityAnimatable = makeNumberAnimatable("opacity", material.opacity, {
2361
+ min: 0,
2362
+ max: 1
2363
+ });
2364
+ const materialAnimatables = [
2365
+ colorAnimatable,
2366
+ opacityAnimatable
2367
+ ];
2368
+ let roughnessAnimatable;
2369
+ let metalnessAnimatable;
2370
+ let emissiveAnimatable;
2371
+ let emissiveIntensityAnimatable;
2372
+ let shininessAnimatable;
2373
+ let specularAnimatable;
2374
+ if (material.isMeshStandardMaterial) {
2375
+ roughnessAnimatable = makeNumberAnimatable(
2376
+ "roughness",
2377
+ material.roughness,
2378
+ {
2379
+ min: 0,
2380
+ max: 1
2381
+ }
2382
+ );
2383
+ metalnessAnimatable = makeNumberAnimatable(
2384
+ "metalness",
2385
+ material.metalness,
2386
+ {
2387
+ min: 0,
2388
+ max: 1
2389
+ }
2390
+ );
2391
+ emissiveAnimatable = makeColorAnimatable("emissive", material.emissive);
2392
+ emissiveIntensityAnimatable = makeNumberAnimatable(
2393
+ "emissive intensity",
2394
+ material.emissiveIntensity,
2395
+ { min: 0 }
2396
+ );
2397
+ materialAnimatables.push(
2398
+ roughnessAnimatable,
2399
+ metalnessAnimatable,
2400
+ emissiveAnimatable,
2401
+ emissiveIntensityAnimatable
2402
+ );
2403
+ } else if (mesh.material.isMeshPhongMaterial) {
2404
+ const phong = mesh.material;
2405
+ shininessAnimatable = makeNumberAnimatable("shininess", phong.shininess, {
2406
+ min: 0
2407
+ });
2408
+ emissiveAnimatable = makeColorAnimatable("emissive", phong.emissive);
2409
+ emissiveIntensityAnimatable = makeNumberAnimatable(
2410
+ "emissive intensity",
2411
+ phong.emissiveIntensity,
2412
+ { min: 0 }
2413
+ );
2414
+ specularAnimatable = makeColorAnimatable("specular", phong.specular);
2415
+ materialAnimatables.push(
2416
+ shininessAnimatable,
2417
+ emissiveAnimatable,
2418
+ emissiveIntensityAnimatable,
2419
+ specularAnimatable
2420
+ );
2421
+ }
2422
+ let materialIds;
2253
2423
  if (colorName && colorLookup[colorName]) {
2254
- colorId = colorLookup[colorName][0];
2255
- opacityId = colorLookup[colorName][1];
2256
- useEmissive = colorLookup[colorName][2];
2424
+ materialIds = colorLookup[colorName];
2257
2425
  } else {
2258
- animatables = { ...animatables, [colorAnimatable.id]: colorAnimatable };
2259
- animatables = { ...animatables, [opacityAnimatable.id]: opacityAnimatable };
2426
+ materialIds = {
2427
+ color: colorAnimatable.id,
2428
+ opacity: opacityAnimatable.id,
2429
+ roughness: roughnessAnimatable?.id,
2430
+ metalness: metalnessAnimatable?.id,
2431
+ emissive: emissiveAnimatable?.id,
2432
+ emissiveIntensity: emissiveIntensityAnimatable?.id,
2433
+ shininess: shininessAnimatable?.id,
2434
+ specular: specularAnimatable?.id
2435
+ };
2436
+ materialAnimatables.forEach((animatable) => {
2437
+ animatables = { ...animatables, [animatable.id]: animatable };
2438
+ });
2260
2439
  if (colorName) {
2261
- newColorLookup[colorName] = [
2262
- colorAnimatable.id,
2263
- opacityAnimatable.id,
2264
- useEmissive
2265
- ];
2440
+ newColorLookup[colorName] = materialIds;
2266
2441
  }
2267
2442
  }
2268
2443
  const [geometryFeatures, geometryAnimatables, morphTargets] = importGeometry(
@@ -2281,13 +2456,22 @@ function importMesh(mesh, namespaces, colorLookup) {
2281
2456
  world = { ...world, ...newWorldItems };
2282
2457
  animatables = { ...animatables, ...newAnimatables };
2283
2458
  children.push(childId);
2459
+ } else if (shouldImportAsGroupChild(child)) {
2460
+ const [newWorldItems, newAnimatables, childId, newMeshColors] = importGroup(child, namespaces, {
2461
+ ...colorLookup,
2462
+ ...newColorLookup
2463
+ });
2464
+ newColorLookup = { ...newColorLookup, ...newMeshColors };
2465
+ world = { ...world, ...newWorldItems };
2466
+ animatables = { ...animatables, ...newAnimatables };
2467
+ children.push(childId);
2284
2468
  }
2285
2469
  });
2286
2470
  const newShape = {
2287
2471
  id: mesh.uuid,
2288
2472
  name: mesh.name,
2289
2473
  geometry: mesh.geometry,
2290
- material: getShapeMaterial(mesh, useEmissive),
2474
+ material: getShapeMaterial(mesh),
2291
2475
  type: "shape",
2292
2476
  tags: [],
2293
2477
  morphTargets,
@@ -2295,8 +2479,19 @@ function importMesh(mesh, namespaces, colorLookup) {
2295
2479
  translation: { animated: true, value: translationAnimatable.id },
2296
2480
  rotation: { animated: true, value: rotationAnimatable.id },
2297
2481
  scale: { animated: true, value: scaleAnimatable.id },
2298
- color: { animated: true, value: colorId },
2299
- opacity: { animated: true, value: opacityId },
2482
+ color: { animated: true, value: materialIds.color },
2483
+ opacity: { animated: true, value: materialIds.opacity },
2484
+ ...materialIds.roughness ? { roughness: { animated: true, value: materialIds.roughness } } : {},
2485
+ ...materialIds.metalness ? { metalness: { animated: true, value: materialIds.metalness } } : {},
2486
+ ...materialIds.emissive ? { emissive: { animated: true, value: materialIds.emissive } } : {},
2487
+ ...materialIds.emissiveIntensity ? {
2488
+ emissiveIntensity: {
2489
+ animated: true,
2490
+ value: materialIds.emissiveIntensity
2491
+ }
2492
+ } : {},
2493
+ ...materialIds.shininess ? { shininess: { animated: true, value: materialIds.shininess } } : {},
2494
+ ...materialIds.specular ? { specular: { animated: true, value: materialIds.specular } } : {},
2300
2495
  ...geometryFeatures
2301
2496
  },
2302
2497
  children: children.length > 0 ? children : void 0,
@@ -2305,11 +2500,8 @@ function importMesh(mesh, namespaces, colorLookup) {
2305
2500
  world = { ...world, [newShape.id]: newShape };
2306
2501
  return [world, animatables, newShape.id, newColorLookup];
2307
2502
  }
2308
- function getShapeMaterial(mesh, useEmissive) {
2503
+ function getShapeMaterial(mesh) {
2309
2504
  const material = mesh.material;
2310
- if (useEmissive) {
2311
- return "basic" /* Basic */;
2312
- }
2313
2505
  if (material.isMeshStandardMaterial) {
2314
2506
  return "standard" /* Standard */;
2315
2507
  } else if (material.isMeshPhongMaterial) {
@@ -2324,6 +2516,24 @@ function getShapeMaterial(mesh, useEmissive) {
2324
2516
  return "standard" /* Standard */;
2325
2517
  }
2326
2518
  }
2519
+ function shouldImportAsGroupChild(child) {
2520
+ if (!child.isObject3D) {
2521
+ return false;
2522
+ }
2523
+ if (child.isMesh) {
2524
+ return false;
2525
+ }
2526
+ if (child.isCamera) {
2527
+ return false;
2528
+ }
2529
+ if (child.isLight) {
2530
+ return false;
2531
+ }
2532
+ if (child.isBone) {
2533
+ return false;
2534
+ }
2535
+ return true;
2536
+ }
2327
2537
 
2328
2538
  // src/functions/gltf-loading/import-group.ts
2329
2539
  Object3D6.DEFAULT_UP.set(0, 0, 1);
@@ -2333,7 +2543,7 @@ function importGroup(group, namespaces, colorLookup, rootBounds) {
2333
2543
  let newColorLookup = {};
2334
2544
  const children = [];
2335
2545
  const translationAnimatable = {
2336
- id: crypto.randomUUID(),
2546
+ id: createBrowserSafeId5(),
2337
2547
  name: `${group.name ?? "Group"} translation`,
2338
2548
  type: "vector3",
2339
2549
  default: { x: group.position.x, y: group.position.y, z: group.position.z },
@@ -2349,7 +2559,7 @@ function importGroup(group, namespaces, colorLookup, rootBounds) {
2349
2559
  [translationAnimatable.id]: translationAnimatable
2350
2560
  };
2351
2561
  const rotationAnimatable = {
2352
- id: crypto.randomUUID(),
2562
+ id: createBrowserSafeId5(),
2353
2563
  name: `${group.name ?? "Group"} rotation`,
2354
2564
  type: "euler",
2355
2565
  default: { x: group.rotation.x, y: group.rotation.y, z: group.rotation.z },
@@ -2362,7 +2572,7 @@ function importGroup(group, namespaces, colorLookup, rootBounds) {
2362
2572
  };
2363
2573
  animatables = { ...animatables, [rotationAnimatable.id]: rotationAnimatable };
2364
2574
  const scaleAnimatable = {
2365
- id: crypto.randomUUID(),
2575
+ id: createBrowserSafeId5(),
2366
2576
  name: `${group.name ?? "Group"} scale`,
2367
2577
  type: "vector3",
2368
2578
  default: { x: group.scale.x, y: group.scale.y, z: group.scale.z },
@@ -2383,7 +2593,7 @@ function importGroup(group, namespaces, colorLookup, rootBounds) {
2383
2593
  world = { ...world, ...newWorldItems };
2384
2594
  animatables = { ...animatables, ...newAnimatables };
2385
2595
  children.push(childId);
2386
- } else if (child.isGroup || child.isObject3D && child.children.length !== 0) {
2596
+ } else if (shouldImportAsGroupChild2(child)) {
2387
2597
  const [newWorldItems, newAnimatables, childId, newMeshColors] = importGroup(child, namespaces, {
2388
2598
  ...colorLookup,
2389
2599
  ...newColorLookup
@@ -2412,6 +2622,24 @@ function importGroup(group, namespaces, colorLookup, rootBounds) {
2412
2622
  world = { ...world, [newGroup.id]: newGroup };
2413
2623
  return [world, animatables, newGroup.id, newColorLookup];
2414
2624
  }
2625
+ function shouldImportAsGroupChild2(child) {
2626
+ if (!child.isObject3D) {
2627
+ return false;
2628
+ }
2629
+ if (child.isMesh) {
2630
+ return false;
2631
+ }
2632
+ if (child.isCamera) {
2633
+ return false;
2634
+ }
2635
+ if (child.isLight) {
2636
+ return false;
2637
+ }
2638
+ if (child.isBone) {
2639
+ return false;
2640
+ }
2641
+ return true;
2642
+ }
2415
2643
 
2416
2644
  // src/functions/gltf-loading/import-scene.ts
2417
2645
  Object3D7.DEFAULT_UP.set(0, 0, 1);
@@ -2537,7 +2765,10 @@ var MATERIAL_NAME_FEATURE_KEYS = [
2537
2765
  "opacity",
2538
2766
  "roughness",
2539
2767
  "metalness",
2540
- "shininess"
2768
+ "shininess",
2769
+ "emissive",
2770
+ "emissiveIntensity",
2771
+ "specular"
2541
2772
  ];
2542
2773
  var MATERIAL_NAME_SUFFIXES2 = [
2543
2774
  " color",
@@ -2546,7 +2777,10 @@ var MATERIAL_NAME_SUFFIXES2 = [
2546
2777
  " opacity",
2547
2778
  " roughness",
2548
2779
  " metalness",
2549
- " shininess"
2780
+ " shininess",
2781
+ " emissive intensity",
2782
+ " emissive",
2783
+ " specular"
2550
2784
  ];
2551
2785
  function applyStoredRenderableNames(object, data) {
2552
2786
  if (typeof data.name === "string" && data.name.length > 0) {
@@ -2722,6 +2956,13 @@ function searchParserJsonForBundle(parserJson) {
2722
2956
  if (!parserJson || typeof parserJson !== "object") {
2723
2957
  return null;
2724
2958
  }
2959
+ const rootExtensions = parserJson && typeof parserJson === "object" ? parserJson.extensions : null;
2960
+ if (rootExtensions && typeof rootExtensions === "object") {
2961
+ const match = readExtensionValue(rootExtensions);
2962
+ if (match) {
2963
+ return cloneBundle(match.value);
2964
+ }
2965
+ }
2725
2966
  const nodes = Array.isArray(parserJson.nodes) ? parserJson.nodes : [];
2726
2967
  for (const node of nodes) {
2727
2968
  const extensions = node && typeof node === "object" ? node.extensions : null;
@@ -2759,12 +3000,29 @@ function applyVizijBundle(object, bundle) {
2759
3000
  const userData = object.userData && typeof object.userData === "object" ? object.userData : {};
2760
3001
  const originalExtensions = userData.gltfExtensions;
2761
3002
  let applied = false;
3003
+ const staleCarriers = [];
2762
3004
  if (bundle) {
2763
3005
  userData.gltfExtensions = {
2764
3006
  ...originalExtensions ?? {},
2765
3007
  VIZIJ_bundle: bundle
2766
3008
  };
2767
3009
  object.userData = userData;
3010
+ object.traverse((node) => {
3011
+ if (node === object) {
3012
+ return;
3013
+ }
3014
+ const extensions = node.userData?.gltfExtensions;
3015
+ if (!extensions || !(BUNDLE_KEYS[0] in extensions)) {
3016
+ return;
3017
+ }
3018
+ staleCarriers.push({ node, extensions });
3019
+ const { [BUNDLE_KEYS[0]]: _stale, ...rest } = extensions;
3020
+ if (Object.keys(rest).length > 0) {
3021
+ node.userData.gltfExtensions = rest;
3022
+ } else {
3023
+ delete node.userData.gltfExtensions;
3024
+ }
3025
+ });
2768
3026
  applied = true;
2769
3027
  }
2770
3028
  return () => {
@@ -2781,6 +3039,10 @@ function applyVizijBundle(object, bundle) {
2781
3039
  delete object.userData;
2782
3040
  }
2783
3041
  }
3042
+ for (const { node, extensions } of staleCarriers) {
3043
+ node.userData.gltfExtensions = extensions;
3044
+ }
3045
+ staleCarriers.length = 0;
2784
3046
  applied = false;
2785
3047
  };
2786
3048
  }
@@ -3157,24 +3419,87 @@ function extractVizijAnimations(parserJson, clips) {
3157
3419
 
3158
3420
  // src/functions/load-gltf.ts
3159
3421
  THREE5.Object3D.DEFAULT_UP.set(0, 0, 1);
3422
+ var GLB_MAGIC = 1179937895;
3423
+ var GLB_VERSION = 2;
3424
+ var GLB_HEADER_BYTES = 12;
3425
+ var GLB_CHUNK_HEADER_BYTES = 8;
3426
+ var GLB_JSON_CHUNK_TYPE = 1313821514;
3160
3427
  var EmptyModelError = class extends Error {
3161
3428
  constructor(message) {
3162
3429
  super(message);
3163
3430
  this.name = "EmptyModelError";
3164
3431
  }
3165
3432
  };
3433
+ function parseGlbJsonChunk(buffer) {
3434
+ if (buffer.byteLength < GLB_HEADER_BYTES + GLB_CHUNK_HEADER_BYTES) {
3435
+ return void 0;
3436
+ }
3437
+ const view = new DataView(buffer);
3438
+ const magic = view.getUint32(0, true);
3439
+ const version = view.getUint32(4, true);
3440
+ if (magic !== GLB_MAGIC || version !== GLB_VERSION) {
3441
+ return void 0;
3442
+ }
3443
+ const chunkLength = view.getUint32(GLB_HEADER_BYTES, true);
3444
+ const chunkType = view.getUint32(GLB_HEADER_BYTES + 4, true);
3445
+ if (chunkType !== GLB_JSON_CHUNK_TYPE) {
3446
+ return void 0;
3447
+ }
3448
+ const chunkStart = GLB_HEADER_BYTES + GLB_CHUNK_HEADER_BYTES;
3449
+ const chunkEnd = chunkStart + chunkLength;
3450
+ if (chunkEnd > buffer.byteLength) {
3451
+ return void 0;
3452
+ }
3453
+ try {
3454
+ const chunkBytes = new Uint8Array(buffer, chunkStart, chunkLength);
3455
+ const jsonText = new TextDecoder().decode(chunkBytes);
3456
+ return JSON.parse(jsonText);
3457
+ } catch {
3458
+ return void 0;
3459
+ }
3460
+ }
3461
+ async function resolveParserJson(parserJson, fallback) {
3462
+ if (parserJson && typeof parserJson === "object") {
3463
+ return parserJson;
3464
+ }
3465
+ if (fallback.arrayBuffer) {
3466
+ const fromArrayBuffer = parseGlbJsonChunk(fallback.arrayBuffer);
3467
+ if (fromArrayBuffer && typeof fromArrayBuffer === "object") {
3468
+ return fromArrayBuffer;
3469
+ }
3470
+ }
3471
+ if (fallback.blob) {
3472
+ try {
3473
+ const blobBuffer = typeof fallback.blob.arrayBuffer === "function" ? await fallback.blob.arrayBuffer() : await new Response(fallback.blob).arrayBuffer();
3474
+ const fromBlob = parseGlbJsonChunk(blobBuffer);
3475
+ if (fromBlob && typeof fromBlob === "object") {
3476
+ return fromBlob;
3477
+ }
3478
+ } catch {
3479
+ }
3480
+ }
3481
+ if (fallback.url && typeof fetch === "function") {
3482
+ try {
3483
+ const response = await fetch(fallback.url);
3484
+ if (response.ok) {
3485
+ const binary = await response.arrayBuffer();
3486
+ const fromUrl = parseGlbJsonChunk(binary);
3487
+ if (fromUrl && typeof fromUrl === "object") {
3488
+ return fromUrl;
3489
+ }
3490
+ }
3491
+ } catch {
3492
+ }
3493
+ }
3494
+ return parserJson;
3495
+ }
3166
3496
  async function loadGLTF(url, namespaces, aggressiveImport = false, rootBounds) {
3167
- const modelLoader = new GLTFLoader();
3168
- modelLoader.setDRACOLoader(new DRACOLoader());
3169
- const modelData = await modelLoader.loadAsync(url);
3170
- const actualizedNamespaces = namespaces.length > 0 ? namespaces : ["default"];
3171
- const asset = parseScene(
3172
- modelData.scene,
3173
- actualizedNamespaces,
3497
+ const asset = await loadGLTFWithBundleInternal(
3498
+ url,
3499
+ namespaces,
3174
3500
  aggressiveImport,
3175
3501
  rootBounds,
3176
- modelData?.parser?.json,
3177
- modelData.animations
3502
+ "url"
3178
3503
  );
3179
3504
  return [asset.world, asset.animatables, asset.animations];
3180
3505
  }
@@ -3183,11 +3508,13 @@ async function loadGLTFFromBlob(blob, namespaces, aggressiveImport = false, root
3183
3508
  if (typeof URL !== "undefined" && typeof URL.createObjectURL === "function") {
3184
3509
  const objectUrl = URL.createObjectURL(blob);
3185
3510
  try {
3186
- const asset = await loadGLTFWithBundle(
3511
+ const asset = await loadGLTFWithBundleInternal(
3187
3512
  objectUrl,
3188
3513
  actualizedNamespaces,
3189
3514
  aggressiveImport,
3190
- rootBounds
3515
+ rootBounds,
3516
+ "blob",
3517
+ { blob }
3191
3518
  );
3192
3519
  return [asset.world, asset.animatables, asset.animations];
3193
3520
  } finally {
@@ -3196,19 +3523,22 @@ async function loadGLTFFromBlob(blob, namespaces, aggressiveImport = false, root
3196
3523
  }
3197
3524
  const arrayBuffer = typeof blob.arrayBuffer === "function" ? await blob.arrayBuffer() : await new Response(blob).arrayBuffer();
3198
3525
  return new Promise((resolve, reject) => {
3199
- const loader = new GLTFLoader();
3200
- loader.setDRACOLoader(new DRACOLoader());
3526
+ const loader = createTrackedGLTFLoader();
3201
3527
  loader.parse(
3202
3528
  arrayBuffer,
3203
3529
  "",
3204
- (gltf) => {
3530
+ async (gltf) => {
3205
3531
  try {
3532
+ const parserJson = await resolveParserJson(
3533
+ gltf?.parser?.json,
3534
+ { arrayBuffer }
3535
+ );
3206
3536
  const asset = parseScene(
3207
3537
  gltf.scene,
3208
3538
  actualizedNamespaces,
3209
3539
  aggressiveImport,
3210
3540
  rootBounds,
3211
- gltf?.parser?.json,
3541
+ parserJson,
3212
3542
  gltf.animations
3213
3543
  );
3214
3544
  resolve([asset.world, asset.animatables, asset.animations]);
@@ -3235,32 +3565,59 @@ function parseScene(scene, namespaces, aggressiveImport, rootBounds, parserJson,
3235
3565
  );
3236
3566
  const bundle = extractVizijBundle(scene, parserJson);
3237
3567
  const animations = extractVizijAnimations(parserJson, clips);
3238
- return { world, animatables, bundle, animations };
3568
+ return { world, animatables, bundle, animations, scene };
3239
3569
  }
3240
- async function loadGLTFWithBundle(url, namespaces, aggressiveImport = false, rootBounds) {
3570
+ function createTrackedGLTFLoader() {
3571
+ recordRenderCounter("gltfLoadCount");
3572
+ recordRenderCounter("dracoLoaderCreationCount");
3241
3573
  const modelLoader = new GLTFLoader();
3242
3574
  modelLoader.setDRACOLoader(new DRACOLoader());
3575
+ return modelLoader;
3576
+ }
3577
+ async function loadGLTFWithBundleInternal(url, namespaces, aggressiveImport = false, rootBounds, sourceKind = "url", parserJsonFallback) {
3578
+ if (sourceKind === "url") {
3579
+ recordRenderCounter("gltfUrlLoadCount");
3580
+ } else {
3581
+ recordRenderCounter("gltfBlobLoadCount");
3582
+ }
3583
+ const modelLoader = createTrackedGLTFLoader();
3243
3584
  const modelData = await modelLoader.loadAsync(url);
3585
+ const parserJson = await resolveParserJson(modelData?.parser?.json, {
3586
+ url,
3587
+ ...parserJsonFallback
3588
+ });
3244
3589
  const actualizedNamespaces = namespaces.length > 0 ? namespaces : ["default"];
3245
3590
  return parseScene(
3246
3591
  modelData.scene,
3247
3592
  actualizedNamespaces,
3248
3593
  aggressiveImport,
3249
3594
  rootBounds,
3250
- modelData?.parser?.json,
3595
+ parserJson,
3251
3596
  modelData.animations
3252
3597
  );
3253
3598
  }
3599
+ async function loadGLTFWithBundle(url, namespaces, aggressiveImport = false, rootBounds, parserJsonFallback) {
3600
+ return loadGLTFWithBundleInternal(
3601
+ url,
3602
+ namespaces,
3603
+ aggressiveImport,
3604
+ rootBounds,
3605
+ "url",
3606
+ parserJsonFallback
3607
+ );
3608
+ }
3254
3609
  async function loadGLTFFromBlobWithBundle(blob, namespaces, aggressiveImport = false, rootBounds) {
3255
3610
  const actualizedNamespaces = namespaces.length > 0 ? namespaces : ["default"];
3256
3611
  if (typeof URL !== "undefined" && typeof URL.createObjectURL === "function") {
3257
3612
  const objectUrl = URL.createObjectURL(blob);
3258
3613
  try {
3259
- return await loadGLTFWithBundle(
3614
+ return await loadGLTFWithBundleInternal(
3260
3615
  objectUrl,
3261
3616
  actualizedNamespaces,
3262
3617
  aggressiveImport,
3263
- rootBounds
3618
+ rootBounds,
3619
+ "blob",
3620
+ { blob }
3264
3621
  );
3265
3622
  } finally {
3266
3623
  URL.revokeObjectURL(objectUrl);
@@ -3268,19 +3625,22 @@ async function loadGLTFFromBlobWithBundle(blob, namespaces, aggressiveImport = f
3268
3625
  }
3269
3626
  const arrayBuffer = typeof blob.arrayBuffer === "function" ? await blob.arrayBuffer() : await new Response(blob).arrayBuffer();
3270
3627
  return new Promise((resolve, reject) => {
3271
- const loader = new GLTFLoader();
3272
- loader.setDRACOLoader(new DRACOLoader());
3628
+ const loader = createTrackedGLTFLoader();
3273
3629
  loader.parse(
3274
3630
  arrayBuffer,
3275
3631
  "",
3276
- (gltf) => {
3632
+ async (gltf) => {
3277
3633
  try {
3634
+ const parserJson = await resolveParserJson(
3635
+ gltf?.parser?.json,
3636
+ { arrayBuffer }
3637
+ );
3278
3638
  const asset = parseScene(
3279
3639
  gltf.scene,
3280
3640
  actualizedNamespaces,
3281
3641
  aggressiveImport,
3282
3642
  rootBounds,
3283
- gltf?.parser?.json,
3643
+ parserJson,
3284
3644
  gltf.animations
3285
3645
  );
3286
3646
  resolve(asset);
@@ -3302,6 +3662,9 @@ async function loadGLTFFromBlobWithBundle(blob, namespaces, aggressiveImport = f
3302
3662
  // src/functions/load-gltf-blob.ts
3303
3663
  import { DRACOLoader as DRACOLoader2, GLTFLoader as GLTFLoader2 } from "three-stdlib";
3304
3664
  var loadGltfFromBlob = (blob, namespaces) => {
3665
+ recordRenderCounter("gltfLoadCount");
3666
+ recordRenderCounter("gltfBlobLoadCount");
3667
+ recordRenderCounter("dracoLoaderCreationCount");
3305
3668
  return new Promise((resolve, reject) => {
3306
3669
  const loader = new GLTFLoader2();
3307
3670
  loader.setDRACOLoader(new DRACOLoader2());
@@ -3332,6 +3695,160 @@ var loadGltfFromBlob = (blob, namespaces) => {
3332
3695
  import { GLTFExporter } from "three-stdlib";
3333
3696
  import * as THREE6 from "three";
3334
3697
  THREE6.Object3D.DEFAULT_UP.set(0, 0, 1);
3698
+ function normalizeExportError(error) {
3699
+ if (error instanceof Error) {
3700
+ return error;
3701
+ }
3702
+ if (typeof error === "string") {
3703
+ return new Error(error);
3704
+ }
3705
+ return new Error("Failed to export scene.");
3706
+ }
3707
+ function triggerBlobDownload(blob, filename) {
3708
+ const url = URL.createObjectURL(blob);
3709
+ const link = document.createElement("a");
3710
+ link.href = url;
3711
+ link.download = filename;
3712
+ link.style.display = "none";
3713
+ document.body.appendChild(link);
3714
+ link.click();
3715
+ document.body.removeChild(link);
3716
+ setTimeout(() => URL.revokeObjectURL(url), 0);
3717
+ }
3718
+ var GLB_MAGIC2 = 1179937895;
3719
+ var GLB_VERSION2 = 2;
3720
+ var GLB_JSON_CHUNK_TYPE2 = 1313821514;
3721
+ var GLB_HEADER_BYTES2 = 12;
3722
+ var GLB_CHUNK_HEADER_BYTES2 = 8;
3723
+ var GLB_JSON_PADDING_BYTE = 32;
3724
+ function isNearlyEqual(a, b, epsilon = 1e-6) {
3725
+ return Math.abs(a - b) <= epsilon;
3726
+ }
3727
+ function isIdentityTransformNode(node) {
3728
+ const translation = node.translation;
3729
+ if (Array.isArray(translation) && (translation.length !== 3 || !isNearlyEqual(Number(translation[0]), 0) || !isNearlyEqual(Number(translation[1]), 0) || !isNearlyEqual(Number(translation[2]), 0))) {
3730
+ return false;
3731
+ }
3732
+ const rotation = node.rotation;
3733
+ if (Array.isArray(rotation) && (rotation.length !== 4 || !isNearlyEqual(Number(rotation[0]), 0) || !isNearlyEqual(Number(rotation[1]), 0) || !isNearlyEqual(Number(rotation[2]), 0) || !isNearlyEqual(Number(rotation[3]), 1))) {
3734
+ return false;
3735
+ }
3736
+ const scale = node.scale;
3737
+ if (Array.isArray(scale) && (scale.length !== 3 || !isNearlyEqual(Number(scale[0]), 1) || !isNearlyEqual(Number(scale[1]), 1) || !isNearlyEqual(Number(scale[2]), 1))) {
3738
+ return false;
3739
+ }
3740
+ return true;
3741
+ }
3742
+ function isPassThroughWrapperNode(node) {
3743
+ if (!node || typeof node !== "object") {
3744
+ return false;
3745
+ }
3746
+ if (!Array.isArray(node.children) || node.children.length === 0) {
3747
+ return false;
3748
+ }
3749
+ const hasOnlyNumericChildren = node.children.every(
3750
+ (index) => Number.isInteger(index)
3751
+ );
3752
+ if (!hasOnlyNumericChildren) {
3753
+ return false;
3754
+ }
3755
+ if (node.mesh !== void 0 || node.camera !== void 0 || node.skin !== void 0) {
3756
+ return false;
3757
+ }
3758
+ if (node.extensions && typeof node.extensions === "object" && Object.keys(node.extensions).length > 0) {
3759
+ return false;
3760
+ }
3761
+ return isIdentityTransformNode(node);
3762
+ }
3763
+ function normalizeExportedSceneJson(json, fallbackSceneName) {
3764
+ if (!json || typeof json !== "object") {
3765
+ return false;
3766
+ }
3767
+ if (!Array.isArray(json.scenes) || !Array.isArray(json.nodes)) {
3768
+ return false;
3769
+ }
3770
+ const sceneIndexRaw = json.scene;
3771
+ const sceneIndex = typeof sceneIndexRaw === "number" && Number.isInteger(sceneIndexRaw) ? sceneIndexRaw : 0;
3772
+ const sceneDef = json.scenes[sceneIndex];
3773
+ if (!sceneDef || typeof sceneDef !== "object") {
3774
+ return false;
3775
+ }
3776
+ let changed = false;
3777
+ let wrapperNodeName;
3778
+ if (Array.isArray(sceneDef.nodes) && sceneDef.nodes.length === 1) {
3779
+ const wrapperIndex = sceneDef.nodes[0];
3780
+ if (Number.isInteger(wrapperIndex)) {
3781
+ const wrapperNode = json.nodes[wrapperIndex];
3782
+ if (isPassThroughWrapperNode(wrapperNode)) {
3783
+ sceneDef.nodes = [...wrapperNode.children];
3784
+ wrapperNodeName = typeof wrapperNode.name === "string" ? wrapperNode.name : void 0;
3785
+ changed = true;
3786
+ }
3787
+ }
3788
+ }
3789
+ const currentSceneName = typeof sceneDef.name === "string" ? sceneDef.name.trim() : "";
3790
+ if (currentSceneName === "AuxScene") {
3791
+ const nextSceneName = (wrapperNodeName?.trim() || fallbackSceneName?.trim() || "Scene").trim();
3792
+ if (nextSceneName.length > 0 && nextSceneName !== currentSceneName) {
3793
+ sceneDef.name = nextSceneName;
3794
+ changed = true;
3795
+ }
3796
+ }
3797
+ return changed;
3798
+ }
3799
+ function sanitizeExportedGlb(buffer, fallbackSceneName) {
3800
+ if (buffer.byteLength < GLB_HEADER_BYTES2 + GLB_CHUNK_HEADER_BYTES2) {
3801
+ return buffer;
3802
+ }
3803
+ const originalBytes = new Uint8Array(buffer);
3804
+ const view = new DataView(buffer);
3805
+ const magic = view.getUint32(0, true);
3806
+ const version = view.getUint32(4, true);
3807
+ if (magic !== GLB_MAGIC2 || version !== GLB_VERSION2) {
3808
+ return buffer;
3809
+ }
3810
+ const jsonChunkLength = view.getUint32(GLB_HEADER_BYTES2, true);
3811
+ const jsonChunkType = view.getUint32(GLB_HEADER_BYTES2 + 4, true);
3812
+ if (jsonChunkType !== GLB_JSON_CHUNK_TYPE2) {
3813
+ return buffer;
3814
+ }
3815
+ const jsonChunkStart = GLB_HEADER_BYTES2 + GLB_CHUNK_HEADER_BYTES2;
3816
+ const jsonChunkEnd = jsonChunkStart + jsonChunkLength;
3817
+ if (jsonChunkEnd > originalBytes.length) {
3818
+ return buffer;
3819
+ }
3820
+ let jsonPayload;
3821
+ try {
3822
+ const jsonText = new TextDecoder().decode(
3823
+ originalBytes.slice(jsonChunkStart, jsonChunkEnd)
3824
+ );
3825
+ jsonPayload = JSON.parse(jsonText);
3826
+ } catch {
3827
+ return buffer;
3828
+ }
3829
+ const changed = normalizeExportedSceneJson(jsonPayload, fallbackSceneName);
3830
+ if (!changed) {
3831
+ return buffer;
3832
+ }
3833
+ const encodedJson = new TextEncoder().encode(JSON.stringify(jsonPayload));
3834
+ const paddedJsonLength = encodedJson.length + 3 & ~3;
3835
+ const paddedJson = new Uint8Array(paddedJsonLength);
3836
+ paddedJson.fill(GLB_JSON_PADDING_BYTE);
3837
+ paddedJson.set(encodedJson);
3838
+ const remainingChunks = originalBytes.slice(jsonChunkEnd);
3839
+ const totalLength = GLB_HEADER_BYTES2 + GLB_CHUNK_HEADER_BYTES2 + paddedJsonLength + remainingChunks.length;
3840
+ const sanitized = new ArrayBuffer(totalLength);
3841
+ const sanitizedBytes = new Uint8Array(sanitized);
3842
+ const sanitizedView = new DataView(sanitized);
3843
+ sanitizedView.setUint32(0, GLB_MAGIC2, true);
3844
+ sanitizedView.setUint32(4, GLB_VERSION2, true);
3845
+ sanitizedView.setUint32(8, totalLength, true);
3846
+ sanitizedView.setUint32(GLB_HEADER_BYTES2, paddedJsonLength, true);
3847
+ sanitizedView.setUint32(GLB_HEADER_BYTES2 + 4, GLB_JSON_CHUNK_TYPE2, true);
3848
+ sanitizedBytes.set(paddedJson, jsonChunkStart);
3849
+ sanitizedBytes.set(remainingChunks, jsonChunkStart + paddedJsonLength);
3850
+ return sanitized;
3851
+ }
3335
3852
  function exportScene(data, fileNameOrOptions = "scene.glb") {
3336
3853
  const options = typeof fileNameOrOptions === "string" ? { fileName: fileNameOrOptions } : fileNameOrOptions ?? {};
3337
3854
  const fileName = options.fileName ?? "scene.glb";
@@ -3346,7 +3863,15 @@ function exportScene(data, fileNameOrOptions = "scene.glb") {
3346
3863
  }
3347
3864
  }
3348
3865
  }));
3349
- const detachBundle = shouldAttachBundle && options.bundle ? applyVizijBundle(data, options.bundle) : () => {
3866
+ const sourceRoot = data;
3867
+ const exportRoot = sourceRoot instanceof THREE6.Scene ? sourceRoot : sourceRoot.clone(true);
3868
+ const exportTarget = exportRoot instanceof THREE6.Scene ? exportRoot : (() => {
3869
+ const scene = new THREE6.Scene();
3870
+ scene.name = data.name?.trim() || "Scene";
3871
+ scene.add(exportRoot);
3872
+ return scene;
3873
+ })();
3874
+ const detachBundle = shouldAttachBundle && options.bundle ? applyVizijBundle(exportRoot, options.bundle) : () => {
3350
3875
  };
3351
3876
  const binary = options.binary ?? true;
3352
3877
  const exporterOptions = {
@@ -3360,33 +3885,40 @@ function exportScene(data, fileNameOrOptions = "scene.glb") {
3360
3885
  }
3361
3886
  try {
3362
3887
  exporter.parse(
3363
- data,
3888
+ exportTarget,
3364
3889
  (gltf) => {
3365
3890
  detachBundle();
3366
3891
  if (!(gltf instanceof ArrayBuffer)) {
3367
- throw new Error("Failed to export scene!");
3892
+ const error = new Error("Failed to export scene.");
3893
+ options.onError?.(error);
3894
+ return;
3368
3895
  }
3369
- const link = document.createElement("a");
3370
- link.href = URL.createObjectURL(
3371
- new Blob([gltf], {
3372
- type: "application/octet-stream"
3373
- })
3896
+ const sanitizedGltf = sanitizeExportedGlb(
3897
+ gltf,
3898
+ data.name?.trim() || void 0
3374
3899
  );
3375
3900
  const trimmed = fileName.trim();
3376
3901
  const safeFileName = trimmed.length > 0 ? trimmed : "scene.glb";
3377
3902
  const downloadName = safeFileName.toLowerCase().endsWith(".glb") ? safeFileName : `${safeFileName}.glb`;
3378
- link.download = downloadName;
3379
- link.click();
3380
- URL.revokeObjectURL(link.href);
3903
+ triggerBlobDownload(
3904
+ new Blob([sanitizedGltf], {
3905
+ type: "application/octet-stream"
3906
+ }),
3907
+ downloadName
3908
+ );
3909
+ options.onComplete?.();
3381
3910
  },
3382
- () => {
3911
+ (error) => {
3383
3912
  detachBundle();
3913
+ options.onError?.(normalizeExportError(error));
3384
3914
  },
3385
3915
  exporterOptions
3386
3916
  );
3387
3917
  } catch (error) {
3388
3918
  detachBundle();
3389
- throw error;
3919
+ const normalizedError = normalizeExportError(error);
3920
+ options.onError?.(normalizedError);
3921
+ throw normalizedError;
3390
3922
  }
3391
3923
  }
3392
3924
  export {
@@ -3405,6 +3937,7 @@ export {
3405
3937
  loadGLTFFromBlobWithBundle,
3406
3938
  loadGLTFWithBundle,
3407
3939
  loadGltfFromBlob,
3940
+ parseGlbJsonChunk,
3408
3941
  useDefaultVizijStore,
3409
3942
  useFeatures,
3410
3943
  useVizijStore,