@vizij/render 0.1.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,13 +1,13 @@
1
1
  // src/vizij.tsx
2
2
  import { Suspense, memo as memo6, useContext as useContext3, useEffect as useEffect7 } from "react";
3
3
  import { ErrorBoundary } from "react-error-boundary";
4
- import { Object3D as Object3D4, SRGBColorSpace, NoToneMapping } from "three";
4
+ import { Object3D as Object3D4, SRGBColorSpace } from "three";
5
5
  import { Canvas, useThree } from "@react-three/fiber";
6
6
  import { Line as Line3, OrthographicCamera, Text } from "@react-three/drei";
7
7
  import { useShallow as useShallow7 } from "zustand/react/shallow";
8
8
 
9
9
  // src/renderables/renderable.tsx
10
- import { memo as memo5, useMemo as useMemo5 } from "react";
10
+ import { memo as memo5 } from "react";
11
11
  import { useShallow as useShallow5 } from "zustand/react/shallow";
12
12
 
13
13
  // src/hooks/use-vizij-store.ts
@@ -84,35 +84,33 @@ function useFeatures(namespace, features, callbacks, debugInfo) {
84
84
  }, [features, callbacks, store, debugInfo, namespace]);
85
85
  }
86
86
 
87
- // src/functions/create-stored-data.ts
88
- import { omit } from "lodash";
89
-
90
87
  // src/functions/create-stored-features.ts
91
- import { mapValues } from "lodash";
92
88
  function createStoredFeatures(objectFeatures, animatableValues) {
93
- return mapValues(objectFeatures, (feat) => {
94
- if (feat.animated) {
95
- const storedFeat = {
96
- animated: true,
97
- value: animatableValues[feat.value],
98
- ...feat.label ? { label: feat.label } : {}
99
- };
100
- return storedFeat;
101
- } else {
89
+ const stored = Object.entries(objectFeatures).map(
90
+ ([key, feat]) => {
91
+ if (feat.animated) {
92
+ const storedFeat = {
93
+ animated: true,
94
+ value: animatableValues[feat.value],
95
+ ...feat.label ? { label: feat.label } : {}
96
+ };
97
+ return [key, storedFeat];
98
+ }
102
99
  const staticFeature = feat;
103
- return staticFeature.label ? { ...staticFeature } : staticFeature;
100
+ return [key, staticFeature.label ? { ...staticFeature } : staticFeature];
104
101
  }
105
- });
102
+ );
103
+ return Object.fromEntries(stored);
106
104
  }
107
105
 
108
106
  // src/functions/create-stored-data.ts
109
107
  function createStoredRenderable(data, animatableValues) {
110
- const d = omit(data, "refs", "geometry");
108
+ const { geometry: _geometry, ...stored } = data;
111
109
  const storedFeatures = createStoredFeatures(
112
110
  data.features,
113
111
  animatableValues
114
112
  );
115
- return { ...d, features: storedFeatures };
113
+ return { ...stored, features: storedFeatures };
116
114
  }
117
115
 
118
116
  // src/renderables/group.tsx
@@ -125,7 +123,9 @@ function InnerRenderedGroup({
125
123
  }) {
126
124
  const ref = useRef(null);
127
125
  const group = useVizijStore(useShallow((state) => state.world[id]));
128
- const refIsNull = !group.refs[namespace]?.current;
126
+ const refIsNull = useVizijStore(
127
+ (state) => !state.refs[id]?.[namespace]?.current
128
+ );
129
129
  const animatables = useVizijStore(useShallow((state) => state.animatables));
130
130
  const setHoveredElement = useVizijStore(
131
131
  useShallow((state) => state.setHoveredElement)
@@ -267,7 +267,9 @@ function InnerRenderedEllipse({
267
267
  const ellipse = useVizijStore(
268
268
  useShallow2((state) => state.world[id])
269
269
  );
270
- const refIsNull = !ellipse.refs[namespace]?.current;
270
+ const refIsNull = useVizijStore(
271
+ (state) => !state.refs[id]?.[namespace]?.current
272
+ );
271
273
  const animatables = useVizijStore(useShallow2((state) => state.animatables));
272
274
  const animatableValues = useMemo2(() => {
273
275
  const av = {};
@@ -561,7 +563,9 @@ function InnerRenderedRectangle({
561
563
  const rectangle = useVizijStore(
562
564
  useShallow3((state) => state.world[id])
563
565
  );
564
- const refIsNull = !rectangle.refs[namespace]?.current;
566
+ const refIsNull = useVizijStore(
567
+ (state) => !state.refs[id]?.[namespace]?.current
568
+ );
565
569
  const animatables = useVizijStore(useShallow3((state) => state.animatables));
566
570
  const animatableValues = useMemo3(() => {
567
571
  const av = {};
@@ -833,6 +837,61 @@ import {
833
837
  instanceOfRawRGB as instanceOfRawRGB3,
834
838
  instanceOfRawVector3 as instanceOfRawVector34
835
839
  } from "@vizij/utils";
840
+
841
+ // src/functions/material-name.ts
842
+ var MATERIAL_FEATURE_KEYS = [
843
+ "color",
844
+ "opacity",
845
+ "roughness",
846
+ "metalness",
847
+ "shininess",
848
+ "emissive",
849
+ "emissiveIntensity",
850
+ "specular"
851
+ ];
852
+ var MATERIAL_NAME_SUFFIXES = [
853
+ " color",
854
+ " colours",
855
+ " colour",
856
+ " opacity",
857
+ " roughness",
858
+ " metalness",
859
+ " shininess",
860
+ " emissive intensity",
861
+ " emissive",
862
+ " specular"
863
+ ];
864
+ function deriveMaterialName(shape, values) {
865
+ for (const key of MATERIAL_FEATURE_KEYS) {
866
+ const feature = shape.features[key];
867
+ if (feature && feature.animated) {
868
+ const animatable = values[feature.value];
869
+ const candidate = extractMaterialName(animatable?.name);
870
+ if (candidate) {
871
+ return candidate;
872
+ }
873
+ }
874
+ }
875
+ return void 0;
876
+ }
877
+ function extractMaterialName(name) {
878
+ if (!name) {
879
+ return void 0;
880
+ }
881
+ const trimmed = name.trim();
882
+ if (!trimmed) {
883
+ return void 0;
884
+ }
885
+ const lowered = trimmed.toLowerCase();
886
+ for (const suffix of MATERIAL_NAME_SUFFIXES) {
887
+ if (lowered.endsWith(suffix)) {
888
+ return trimmed.slice(0, trimmed.length - suffix.length).trim();
889
+ }
890
+ }
891
+ return trimmed;
892
+ }
893
+
894
+ // src/renderables/shape.tsx
836
895
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
837
896
  THREE2.Object3D.DEFAULT_UP.set(0, 0, 1);
838
897
  function InnerRenderedShape({
@@ -843,10 +902,9 @@ function InnerRenderedShape({
843
902
  const refGroup = useRef4(null);
844
903
  const ref = useRef4(null);
845
904
  const shape = useVizijStore(useShallow4((state) => state.world[id]));
846
- const refs = useVizijStore(
847
- useShallow4((state) => state.world[id].refs)
905
+ const refIsNull = useVizijStore(
906
+ (state) => !state.refs[id]?.[namespace]?.current
848
907
  );
849
- const refIsNull = !refs[namespace]?.current;
850
908
  const animatables = useVizijStore(useShallow4((state) => state.animatables));
851
909
  const animatableValues = useMemo4(() => {
852
910
  const av = {};
@@ -974,6 +1032,48 @@ function InnerRenderedShape({
974
1032
  );
975
1033
  }
976
1034
  },
1035
+ roughness: (value) => {
1036
+ const standard = material.current;
1037
+ if (standard?.roughness !== void 0 && instanceOfRawNumber4(value)) {
1038
+ standard.roughness = value;
1039
+ standard.needsUpdate = true;
1040
+ }
1041
+ },
1042
+ metalness: (value) => {
1043
+ const standard = material.current;
1044
+ if (standard?.metalness !== void 0 && instanceOfRawNumber4(value)) {
1045
+ standard.metalness = value;
1046
+ standard.needsUpdate = true;
1047
+ }
1048
+ },
1049
+ shininess: (value) => {
1050
+ const phong = material.current;
1051
+ if (phong?.shininess !== void 0 && instanceOfRawNumber4(value)) {
1052
+ phong.shininess = value;
1053
+ phong.needsUpdate = true;
1054
+ }
1055
+ },
1056
+ emissive: (value) => {
1057
+ const standard = material.current;
1058
+ if (standard?.emissive && instanceOfRawRGB3(value)) {
1059
+ standard.emissive.setRGB(value.r, value.g, value.b);
1060
+ standard.needsUpdate = true;
1061
+ }
1062
+ },
1063
+ emissiveIntensity: (value) => {
1064
+ const standard = material.current;
1065
+ if (standard?.emissiveIntensity !== void 0 && instanceOfRawNumber4(value)) {
1066
+ standard.emissiveIntensity = value;
1067
+ standard.needsUpdate = true;
1068
+ }
1069
+ },
1070
+ specular: (value) => {
1071
+ const phong = material.current;
1072
+ if (phong?.specular && instanceOfRawRGB3(value)) {
1073
+ phong.specular.setRGB(value.r, value.g, value.b);
1074
+ phong.needsUpdate = true;
1075
+ }
1076
+ },
977
1077
  ...morphTargetSettings[2]
978
1078
  },
979
1079
  shape
@@ -1089,119 +1189,44 @@ function InnerRenderedShape({
1089
1189
  );
1090
1190
  }
1091
1191
  var RenderedShape = memo4(InnerRenderedShape);
1092
- var MATERIAL_FEATURE_KEYS = [
1093
- "color",
1094
- "opacity",
1095
- "roughness",
1096
- "metalness",
1097
- "shininess"
1098
- ];
1099
- var MATERIAL_NAME_SUFFIXES = [
1100
- " color",
1101
- " colours",
1102
- " colour",
1103
- " opacity",
1104
- " roughness",
1105
- " metalness",
1106
- " shininess"
1107
- ];
1108
- function deriveMaterialName(shape, values) {
1109
- for (const key of MATERIAL_FEATURE_KEYS) {
1110
- const feature = shape.features[key];
1111
- if (feature && feature.animated) {
1112
- const animatable = values[feature.value];
1113
- const candidate = extractMaterialName(animatable?.name);
1114
- if (candidate) {
1115
- return candidate;
1116
- }
1117
- }
1118
- }
1119
- return void 0;
1120
- }
1121
- function extractMaterialName(name) {
1122
- if (!name) {
1123
- return void 0;
1124
- }
1125
- const trimmed = name.trim();
1126
- if (!trimmed) {
1127
- return void 0;
1128
- }
1129
- const lowered = trimmed.toLowerCase();
1130
- for (const suffix of MATERIAL_NAME_SUFFIXES) {
1131
- if (lowered.endsWith(suffix)) {
1132
- return trimmed.slice(0, trimmed.length - suffix.length).trim();
1133
- }
1134
- }
1135
- return trimmed;
1136
- }
1137
1192
 
1138
1193
  // src/renderables/renderable.tsx
1139
- import { Fragment as Fragment3, jsx as jsx5 } from "react/jsx-runtime";
1194
+ import { jsx as jsx5 } from "react/jsx-runtime";
1140
1195
  function InnerRenderable({
1141
1196
  id,
1142
1197
  namespace,
1143
1198
  chain
1144
1199
  }) {
1145
1200
  const type = useVizijStore(useShallow5((state) => state.world[id].type));
1146
- const refs = useVizijStore(useShallow5((state) => state.world[id].refs));
1147
- const resolvedNamespaces = useMemo5(() => {
1148
- let namespaces = [namespace];
1149
- if (namespace in refs) {
1150
- namespaces = [namespace];
1151
- } else {
1152
- namespaces = Object.keys(refs);
1153
- }
1154
- return namespaces;
1155
- }, [namespace, refs]);
1156
- if (resolvedNamespaces.length === 0) {
1157
- return null;
1201
+ const key = `${namespace}.${id}`;
1202
+ switch (type) {
1203
+ case "group":
1204
+ return /* @__PURE__ */ jsx5(RenderedGroup, { id, namespace, chain }, key);
1205
+ case "ellipse":
1206
+ return /* @__PURE__ */ jsx5(
1207
+ RenderedEllipse,
1208
+ {
1209
+ id,
1210
+ namespace,
1211
+ chain
1212
+ },
1213
+ key
1214
+ );
1215
+ case "rectangle":
1216
+ return /* @__PURE__ */ jsx5(
1217
+ RenderedRectangle,
1218
+ {
1219
+ id,
1220
+ namespace,
1221
+ chain
1222
+ },
1223
+ key
1224
+ );
1225
+ case "shape":
1226
+ return /* @__PURE__ */ jsx5(RenderedShape, { id, namespace, chain }, key);
1227
+ default:
1228
+ return null;
1158
1229
  }
1159
- return /* @__PURE__ */ jsx5(Fragment3, { children: resolvedNamespaces.map((ns) => {
1160
- switch (type) {
1161
- case "group":
1162
- return /* @__PURE__ */ jsx5(
1163
- RenderedGroup,
1164
- {
1165
- id,
1166
- namespace: ns,
1167
- chain
1168
- },
1169
- `${ns}.${id}`
1170
- );
1171
- case "ellipse":
1172
- return /* @__PURE__ */ jsx5(
1173
- RenderedEllipse,
1174
- {
1175
- id,
1176
- namespace: ns,
1177
- chain
1178
- },
1179
- `${ns}.${id}`
1180
- );
1181
- case "rectangle":
1182
- return /* @__PURE__ */ jsx5(
1183
- RenderedRectangle,
1184
- {
1185
- id,
1186
- namespace: ns,
1187
- chain
1188
- },
1189
- `${ns}.${id}`
1190
- );
1191
- case "shape":
1192
- return /* @__PURE__ */ jsx5(
1193
- RenderedShape,
1194
- {
1195
- id,
1196
- namespace: ns,
1197
- chain
1198
- },
1199
- `${ns}.${id}`
1200
- );
1201
- default:
1202
- return null;
1203
- }
1204
- }) });
1205
1230
  }
1206
1231
  var Renderable = memo5(InnerRenderable);
1207
1232
 
@@ -1212,21 +1237,17 @@ import { produce, enableMapSet } from "immer";
1212
1237
  import * as THREE3 from "three";
1213
1238
  import { getLookup as getLookup2 } from "@vizij/utils";
1214
1239
 
1215
- // src/actions/create-new-element.ts
1216
- import { createRef } from "react";
1217
- import { mapValues as mapValues2 } from "lodash";
1218
-
1219
1240
  // src/functions/create-world-element.ts
1241
+ import { createBrowserSafeId } from "@vizij/utils";
1220
1242
  function createDefaultGroup(partialBody) {
1221
1243
  const translation = partialBody.features?.translation || { animated: false, value: { x: 0, y: 0 } };
1222
1244
  const rotation = partialBody.features?.rotation || { animated: false, value: { x: 0, y: 0, z: 0 } };
1223
1245
  const scale = partialBody.features?.scale || { animated: false, value: { x: 1, y: 1, z: 1 } };
1224
1246
  return {
1225
- id: partialBody.id || crypto.randomUUID(),
1247
+ id: partialBody.id || createBrowserSafeId(),
1226
1248
  name: partialBody.name || "new-body",
1227
1249
  type: partialBody.type || "group",
1228
1250
  tags: partialBody.tags || [],
1229
- refs: partialBody.refs || {},
1230
1251
  features: { translation, rotation, scale },
1231
1252
  root: partialBody.root ?? false,
1232
1253
  children: partialBody.children || []
@@ -1243,11 +1264,9 @@ function createNewElement(state, type, root = false) {
1243
1264
  });
1244
1265
  if (Object.entries(state.world).length === 0) {
1245
1266
  const name = `New-Root`;
1246
- const refs = { default: createRef() };
1247
1267
  const newElement = createDefaultGroup({
1248
1268
  name,
1249
- root: true,
1250
- refs
1269
+ root: true
1251
1270
  });
1252
1271
  state.world[newElement.id] = newElement;
1253
1272
  state.elementSelection = [buildSelection(newElement.id)];
@@ -1259,11 +1278,7 @@ function createNewElement(state, type, root = false) {
1259
1278
  return;
1260
1279
  }
1261
1280
  const name = `New-Body`;
1262
- const refs = mapValues2(
1263
- worldRootEntry.refs,
1264
- () => createRef()
1265
- );
1266
- const newChild = createDefaultGroup({ name, root, refs });
1281
+ const newChild = createDefaultGroup({ name, root });
1267
1282
  worldRootEntry.children.push(newChild.id);
1268
1283
  state.world[newChild.id] = newChild;
1269
1284
  state.elementSelection = [buildSelection(newChild.id)];
@@ -1283,7 +1298,32 @@ function removeFromTree(state, nodesToRemove) {
1283
1298
  });
1284
1299
  }
1285
1300
 
1301
+ // src/memoryInvestigation.ts
1302
+ function getGlobalMemoryInvestigation() {
1303
+ return globalThis;
1304
+ }
1305
+ function updateRenderState(updater) {
1306
+ const globalObj = getGlobalMemoryInvestigation();
1307
+ if (!globalObj.__VIZIJ_MEMORY_INVESTIGATION__?.enabled) {
1308
+ return;
1309
+ }
1310
+ if (!globalObj.__vizijMemoryDebugState) {
1311
+ globalObj.__vizijMemoryDebugState = {};
1312
+ }
1313
+ if (!globalObj.__vizijMemoryDebugState.render) {
1314
+ globalObj.__vizijMemoryDebugState.render = {};
1315
+ }
1316
+ updater(globalObj.__vizijMemoryDebugState.render);
1317
+ }
1318
+ function recordRenderCounter(key, delta = 1) {
1319
+ updateRenderState((renderState) => {
1320
+ const current = typeof renderState[key] === "number" ? Number(renderState[key]) : 0;
1321
+ renderState[key] = current + delta;
1322
+ });
1323
+ }
1324
+
1286
1325
  // src/functions/create-animatable.ts
1326
+ import { createBrowserSafeId as createBrowserSafeId2 } from "@vizij/utils";
1287
1327
  function createAnimatable(value) {
1288
1328
  if (!value.type) {
1289
1329
  return null;
@@ -1293,7 +1333,7 @@ function createAnimatable(value) {
1293
1333
  }
1294
1334
  if (value.type === "euler") {
1295
1335
  const newAnimatable = {
1296
- id: value.id ?? crypto.randomUUID(),
1336
+ id: value.id ?? createBrowserSafeId2(),
1297
1337
  name: `${value.name} Rotation`,
1298
1338
  type: value.type,
1299
1339
  default: value.default ?? { x: 0, y: 0, z: 0 },
@@ -1310,7 +1350,7 @@ function createAnimatable(value) {
1310
1350
  return newAnimatable;
1311
1351
  } else if (value.type === "vector3") {
1312
1352
  const newAnimatable = {
1313
- id: value.id ?? crypto.randomUUID(),
1353
+ id: value.id ?? createBrowserSafeId2(),
1314
1354
  name: `${value.name} Vector3`,
1315
1355
  type: value.type,
1316
1356
  default: value.default ?? { x: 0, y: 0, z: 0 },
@@ -1327,7 +1367,7 @@ function createAnimatable(value) {
1327
1367
  return newAnimatable;
1328
1368
  } else if (value.type === "string") {
1329
1369
  const newAnimatable = {
1330
- id: value.id ?? crypto.randomUUID(),
1370
+ id: value.id ?? createBrowserSafeId2(),
1331
1371
  name: value.name,
1332
1372
  type: value.type,
1333
1373
  default: value.default ?? "Hello World",
@@ -1340,7 +1380,7 @@ function createAnimatable(value) {
1340
1380
  return newAnimatable;
1341
1381
  } else if (value.type === "number") {
1342
1382
  const newAnimatable = {
1343
- id: value.id ?? crypto.randomUUID(),
1383
+ id: value.id ?? createBrowserSafeId2(),
1344
1384
  name: value.name,
1345
1385
  type: value.type,
1346
1386
  default: value.default ?? 0,
@@ -1353,7 +1393,7 @@ function createAnimatable(value) {
1353
1393
  return newAnimatable;
1354
1394
  } else if (value.type === "boolean") {
1355
1395
  const newAnimatable = {
1356
- id: value.id ?? crypto.randomUUID(),
1396
+ id: value.id ?? createBrowserSafeId2(),
1357
1397
  name: value.name,
1358
1398
  type: value.type,
1359
1399
  default: value.default ?? false,
@@ -1366,7 +1406,7 @@ function createAnimatable(value) {
1366
1406
  return newAnimatable;
1367
1407
  } else if (value.type === "rgb") {
1368
1408
  const newAnimatable = {
1369
- id: value.id ?? crypto.randomUUID(),
1409
+ id: value.id ?? createBrowserSafeId2(),
1370
1410
  name: value.name,
1371
1411
  type: value.type,
1372
1412
  default: value.default ?? { r: 0, g: 0, b: 0 },
@@ -1383,7 +1423,7 @@ function createAnimatable(value) {
1383
1423
  return newAnimatable;
1384
1424
  } else if (value.type === "hsl") {
1385
1425
  const newAnimatable = {
1386
- id: value.id ?? crypto.randomUUID(),
1426
+ id: value.id ?? createBrowserSafeId2(),
1387
1427
  name: value.name,
1388
1428
  type: value.type,
1389
1429
  default: value.default ?? { h: 0, s: 0, l: 0 },
@@ -1438,6 +1478,7 @@ enableMapSet();
1438
1478
  var VizijSlice = (set, get) => ({
1439
1479
  // worldRef: createRef<THREE.Group>(),
1440
1480
  world: {},
1481
+ refs: {},
1441
1482
  animatables: {},
1442
1483
  values: /* @__PURE__ */ new Map(),
1443
1484
  renderHit: false,
@@ -1511,12 +1552,10 @@ var VizijSlice = (set, get) => ({
1511
1552
  worldData,
1512
1553
  filterIds
1513
1554
  );
1555
+ const refs = get().refs;
1514
1556
  return candidateGroups.flatMap((entry) => {
1515
- const refs = Object.values(
1516
- entry.refs ?? {}
1517
- );
1518
- const resolved = refs.find((ref) => ref?.current)?.current ?? null;
1519
- return resolved ? [resolved] : [];
1557
+ const mounted = Object.values(refs[entry.id] ?? {}).map((ref) => ref.current).find((object) => Boolean(object));
1558
+ return mounted ? [mounted] : [];
1520
1559
  });
1521
1560
  },
1522
1561
  setGeometry: (id, geometry) => {
@@ -1715,6 +1754,7 @@ var VizijSlice = (set, get) => ({
1715
1754
  );
1716
1755
  },
1717
1756
  setVizij: (scene, animatables) => {
1757
+ recordRenderCounter("rootReplacementCount");
1718
1758
  set({
1719
1759
  world: scene,
1720
1760
  animatables
@@ -1722,6 +1762,7 @@ var VizijSlice = (set, get) => ({
1722
1762
  },
1723
1763
  addWorldElements(world, animatables, replace) {
1724
1764
  if (replace) {
1765
+ recordRenderCounter("rootReplacementCount");
1725
1766
  set({ world, animatables });
1726
1767
  } else {
1727
1768
  set((state) => ({
@@ -1736,14 +1777,12 @@ var VizijSlice = (set, get) => ({
1736
1777
  }));
1737
1778
  },
1738
1779
  setReference: (id, namespace, ref) => {
1739
- set(
1740
- produce((state) => {
1741
- state.world[id].refs[namespace].current = ref.current;
1742
- if (ref.current?.children && state.world[id].refs[namespace].current) {
1743
- state.world[id].refs[namespace].current.children = ref.current.children;
1744
- }
1745
- })
1746
- );
1780
+ set((state) => ({
1781
+ refs: {
1782
+ ...state.refs,
1783
+ [id]: { ...state.refs[id], [namespace]: ref }
1784
+ }
1785
+ }));
1747
1786
  }
1748
1787
  });
1749
1788
  var useDefaultVizijStore = create()(
@@ -1758,8 +1797,57 @@ var createVizijStore = (initial) => create()(
1758
1797
  }))
1759
1798
  );
1760
1799
 
1800
+ // src/types/shape.ts
1801
+ var ShapeMaterial = /* @__PURE__ */ ((ShapeMaterial2) => {
1802
+ ShapeMaterial2["Standard"] = "standard";
1803
+ ShapeMaterial2["Phong"] = "phong";
1804
+ ShapeMaterial2["Basic"] = "basic";
1805
+ ShapeMaterial2["Lambert"] = "lambert";
1806
+ ShapeMaterial2["Normal"] = "normal";
1807
+ return ShapeMaterial2;
1808
+ })(ShapeMaterial || {});
1809
+
1810
+ // src/types/tone-mapping.ts
1811
+ import {
1812
+ ACESFilmicToneMapping,
1813
+ AgXToneMapping,
1814
+ NeutralToneMapping,
1815
+ NoToneMapping
1816
+ } from "three";
1817
+ var TONE_MAPPING_MODES = [
1818
+ "none",
1819
+ "agx",
1820
+ "aces",
1821
+ "neutral"
1822
+ ];
1823
+ var TONE_MAPPING_LABELS = {
1824
+ none: "None (web match)",
1825
+ agx: "AgX (Blender)",
1826
+ aces: "ACES Filmic",
1827
+ neutral: "Neutral"
1828
+ };
1829
+ var DEFAULT_TONE_MAPPING_MODE = "none";
1830
+ var TONE_MAPPING_CONSTANTS = {
1831
+ none: NoToneMapping,
1832
+ agx: AgXToneMapping,
1833
+ aces: ACESFilmicToneMapping,
1834
+ neutral: NeutralToneMapping
1835
+ };
1836
+ function isToneMappingMode(value) {
1837
+ return typeof value === "string" && Object.prototype.hasOwnProperty.call(TONE_MAPPING_CONSTANTS, value);
1838
+ }
1839
+ function resolveToneMapping(mode) {
1840
+ if (isToneMappingMode(mode)) {
1841
+ return TONE_MAPPING_CONSTANTS[mode];
1842
+ }
1843
+ return TONE_MAPPING_CONSTANTS[DEFAULT_TONE_MAPPING_MODE];
1844
+ }
1845
+ function normalizeToneMappingMode(value) {
1846
+ return isToneMappingMode(value) ? value : DEFAULT_TONE_MAPPING_MODE;
1847
+ }
1848
+
1761
1849
  // src/effects/selection-glow-effect.tsx
1762
- import { Fragment as Fragment4, useEffect as useEffect6, useMemo as useMemo6, useRef as useRef5 } from "react";
1850
+ import { Fragment as Fragment3, useEffect as useEffect6, useMemo as useMemo5, useRef as useRef5 } from "react";
1763
1851
  import { useFrame } from "@react-three/fiber";
1764
1852
  import { useShallow as useShallow6 } from "zustand/react/shallow";
1765
1853
  import {
@@ -1774,7 +1862,7 @@ import {
1774
1862
  import { jsx as jsx6 } from "react/jsx-runtime";
1775
1863
  function SelectionGlowEffect({
1776
1864
  enabled = false,
1777
- color = "#ff1010ff",
1865
+ color = "#ff1010",
1778
1866
  opacity = 0.9,
1779
1867
  thresholdAngle = 2
1780
1868
  }) {
@@ -1786,7 +1874,7 @@ function SelectionGlowEffect({
1786
1874
  if (!enabled || selections.length === 0) {
1787
1875
  return null;
1788
1876
  }
1789
- return /* @__PURE__ */ jsx6(Fragment4, { children: selections.map((selection) => /* @__PURE__ */ jsx6(
1877
+ return /* @__PURE__ */ jsx6(Fragment3, { children: selections.map((selection) => /* @__PURE__ */ jsx6(
1790
1878
  SelectionOutline,
1791
1879
  {
1792
1880
  selection,
@@ -1806,19 +1894,19 @@ function SelectionOutline({
1806
1894
  const target = useVizijStore(
1807
1895
  useShallow6((state) => {
1808
1896
  const entry = state.world[selection.id];
1809
- const ref = entry?.refs?.[selection.namespace];
1897
+ const ref = state.refs[selection.id]?.[selection.namespace];
1810
1898
  const geometry = entry?.geometry ?? null;
1811
1899
  return { ref, geometry };
1812
1900
  })
1813
1901
  );
1814
1902
  const sourceRef = target.ref;
1815
- const edgesGeometry = useMemo6(() => {
1903
+ const edgesGeometry = useMemo5(() => {
1816
1904
  if (!target.geometry) return null;
1817
1905
  const edges = new EdgesGeometry(target.geometry, thresholdAngle);
1818
1906
  return edges;
1819
1907
  }, [target.geometry, thresholdAngle]);
1820
1908
  useEffect6(() => () => edgesGeometry?.dispose(), [edgesGeometry]);
1821
- const material = useMemo6(() => {
1909
+ const material = useMemo5(() => {
1822
1910
  const mat = new LineBasicMaterial({
1823
1911
  color: new Color(color),
1824
1912
  transparent: true,
@@ -1868,7 +1956,7 @@ function copyWorldTransform(source, target) {
1868
1956
  }
1869
1957
 
1870
1958
  // src/vizij.tsx
1871
- import { Fragment as Fragment5, jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
1959
+ import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
1872
1960
  Object3D4.DEFAULT_UP.set(0, 0, 1);
1873
1961
  function Vizij({
1874
1962
  style,
@@ -1877,9 +1965,17 @@ function Vizij({
1877
1965
  namespace = "default",
1878
1966
  showSafeArea = false,
1879
1967
  showSelectionGlow = false,
1968
+ toneMapping = "none",
1880
1969
  onPointerMissed
1881
1970
  }) {
1882
1971
  const ctx = useContext3(VizijContext);
1972
+ useEffect7(() => {
1973
+ recordRenderCounter("canvasMountCount");
1974
+ recordRenderCounter("mountedCanvasCount");
1975
+ return () => {
1976
+ recordRenderCounter("mountedCanvasCount", -1);
1977
+ };
1978
+ }, []);
1883
1979
  if (ctx) {
1884
1980
  return /* @__PURE__ */ jsx7(
1885
1981
  Canvas,
@@ -1890,7 +1986,6 @@ function Vizij({
1890
1986
  onPointerMissed,
1891
1987
  gl: {
1892
1988
  outputColorSpace: SRGBColorSpace,
1893
- toneMapping: NoToneMapping,
1894
1989
  antialias: true
1895
1990
  },
1896
1991
  children: /* @__PURE__ */ jsx7(
@@ -1899,7 +1994,8 @@ function Vizij({
1899
1994
  rootId,
1900
1995
  namespace,
1901
1996
  showSafeArea,
1902
- showSelectionGlow
1997
+ showSelectionGlow,
1998
+ toneMapping
1903
1999
  }
1904
2000
  )
1905
2001
  }
@@ -1913,7 +2009,6 @@ function Vizij({
1913
2009
  onPointerMissed,
1914
2010
  gl: {
1915
2011
  outputColorSpace: SRGBColorSpace,
1916
- toneMapping: NoToneMapping,
1917
2012
  antialias: true
1918
2013
  },
1919
2014
  children: /* @__PURE__ */ jsx7(
@@ -1922,7 +2017,8 @@ function Vizij({
1922
2017
  rootId,
1923
2018
  namespace,
1924
2019
  showSafeArea,
1925
- showSelectionGlow
2020
+ showSelectionGlow,
2021
+ toneMapping
1926
2022
  }
1927
2023
  )
1928
2024
  }
@@ -1934,21 +2030,23 @@ function InnerVizij({
1934
2030
  namespace = "default",
1935
2031
  container,
1936
2032
  showSafeArea,
1937
- showSelectionGlow
2033
+ showSelectionGlow,
2034
+ toneMapping = "none"
1938
2035
  }) {
1939
2036
  const sceneParentSizing = container ? {
1940
2037
  width: container.width * container.resolution,
1941
2038
  height: container.height * container.resolution
1942
2039
  } : void 0;
1943
- return /* @__PURE__ */ jsxs4(Fragment5, { children: [
2040
+ return /* @__PURE__ */ jsxs4(Fragment4, { children: [
1944
2041
  /* @__PURE__ */ jsx7("ambientLight", { intensity: Math.PI / 2 }),
2042
+ /* @__PURE__ */ jsx7(ToneMappingController, { mode: toneMapping }),
1945
2043
  /* @__PURE__ */ jsx7(
1946
2044
  OrthographicCamera,
1947
2045
  {
1948
2046
  makeDefault: true,
1949
2047
  position: [0, 0, 100],
1950
2048
  near: 0.1,
1951
- far: 101
2049
+ far: 1e3
1952
2050
  }
1953
2051
  ),
1954
2052
  /* @__PURE__ */ jsx7(Suspense, { fallback: null, children: /* @__PURE__ */ jsx7(
@@ -1964,6 +2062,31 @@ function InnerVizij({
1964
2062
  ] });
1965
2063
  }
1966
2064
  var MemoizedInnerVizij = memo6(InnerVizij);
2065
+ function ToneMappingController({ mode }) {
2066
+ const gl = useThree((state) => state.gl);
2067
+ const scene = useThree((state) => state.scene);
2068
+ useEffect7(() => {
2069
+ const resolved = resolveToneMapping(mode);
2070
+ if (gl.toneMapping === resolved) {
2071
+ return;
2072
+ }
2073
+ gl.toneMapping = resolved;
2074
+ scene.traverse((object) => {
2075
+ const material = object.material;
2076
+ if (!material) {
2077
+ return;
2078
+ }
2079
+ if (Array.isArray(material)) {
2080
+ material.forEach((mat) => {
2081
+ mat.needsUpdate = true;
2082
+ });
2083
+ } else {
2084
+ material.needsUpdate = true;
2085
+ }
2086
+ });
2087
+ }, [gl, scene, mode]);
2088
+ return null;
2089
+ }
1967
2090
  function InnerWorld({
1968
2091
  rootId,
1969
2092
  namespace = "default",
@@ -2053,16 +2176,6 @@ var defaultRootBounds = {
2053
2176
  size: { x: 5, y: 4 }
2054
2177
  };
2055
2178
 
2056
- // src/types/shape.ts
2057
- var ShapeMaterial = /* @__PURE__ */ ((ShapeMaterial2) => {
2058
- ShapeMaterial2["Standard"] = "standard";
2059
- ShapeMaterial2["Phong"] = "phong";
2060
- ShapeMaterial2["Basic"] = "basic";
2061
- ShapeMaterial2["Lambert"] = "lambert";
2062
- ShapeMaterial2["Normal"] = "normal";
2063
- return ShapeMaterial2;
2064
- })(ShapeMaterial || {});
2065
-
2066
2179
  // src/hooks/use-vizij-store-subscription.ts
2067
2180
  import { useContext as useContext4, useEffect as useEffect8 } from "react";
2068
2181
  function useVizijStoreSubscription(selector, listener) {
@@ -2096,7 +2209,6 @@ import * as THREE5 from "three";
2096
2209
  import { GLTFLoader, DRACOLoader } from "three-stdlib";
2097
2210
 
2098
2211
  // src/functions/gltf-loading/traverse-three.ts
2099
- import { createRef as createRef3 } from "react";
2100
2212
  import * as THREE4 from "three";
2101
2213
 
2102
2214
  // src/functions/gltf-loading/map-features.ts
@@ -2126,21 +2238,25 @@ import { Object3D as Object3D7 } from "three";
2126
2238
 
2127
2239
  // src/functions/gltf-loading/import-group.ts
2128
2240
  import { Object3D as Object3D6 } from "three";
2129
-
2130
- // src/functions/util.ts
2131
- import { createRef as createRef2 } from "react";
2132
- function namespaceArrayToRefs(namespaces) {
2133
- return namespaces.reduce((acc, ns) => ({ ...acc, [ns]: createRef2() }), {});
2134
- }
2241
+ import { createBrowserSafeId as createBrowserSafeId5 } from "@vizij/utils";
2135
2242
 
2136
2243
  // src/functions/gltf-loading/import-mesh.ts
2137
2244
  import { Object3D as Object3D5 } from "three";
2245
+ import { createBrowserSafeId as createBrowserSafeId4 } from "@vizij/utils";
2138
2246
 
2139
2247
  // src/functions/gltf-loading/import-geometry.ts
2140
- function sanitizeMorphKey(name, fallbackIndex, used) {
2248
+ import {
2249
+ createBrowserSafeId as createBrowserSafeId3
2250
+ } from "@vizij/utils";
2251
+
2252
+ // src/functions/gltf-loading/morph-keys.ts
2253
+ function slugifyMorphName(name, fallbackIndex) {
2141
2254
  const baseName = name && name.trim().length > 0 ? name.trim() : `morph_${fallbackIndex + 1}`;
2142
2255
  const slug = baseName.toLowerCase().replace(/[^a-z0-9]+/gi, "_").replace(/^_+|_+$/g, "");
2143
- const safeBase = slug.length > 0 ? slug : `morph_${fallbackIndex + 1}`;
2256
+ return slug.length > 0 ? slug : `morph_${fallbackIndex + 1}`;
2257
+ }
2258
+ function sanitizeMorphKey(name, fallbackIndex, used) {
2259
+ const safeBase = slugifyMorphName(name, fallbackIndex);
2144
2260
  let candidate = safeBase;
2145
2261
  let counter = 1;
2146
2262
  while (used.has(candidate)) {
@@ -2149,6 +2265,12 @@ function sanitizeMorphKey(name, fallbackIndex, used) {
2149
2265
  used.add(candidate);
2150
2266
  return candidate;
2151
2267
  }
2268
+ function deriveMorphFeatureKeys(names) {
2269
+ const used = /* @__PURE__ */ new Set();
2270
+ return names.map((name, index) => sanitizeMorphKey(name, index, used));
2271
+ }
2272
+
2273
+ // src/functions/gltf-loading/import-geometry.ts
2152
2274
  function importGeometry(geometry, mesh) {
2153
2275
  const features = {};
2154
2276
  const animatableValues = {};
@@ -2160,7 +2282,7 @@ function importGeometry(geometry, mesh) {
2160
2282
  const usedKeys = /* @__PURE__ */ new Set();
2161
2283
  Object.entries(mesh.morphTargetDictionary ?? {}).forEach(
2162
2284
  ([name, index]) => {
2163
- const morphId = crypto.randomUUID();
2285
+ const morphId = createBrowserSafeId3();
2164
2286
  const featureKey = sanitizeMorphKey(name, index, usedKeys);
2165
2287
  morphIds.push(featureKey);
2166
2288
  features[featureKey] = {
@@ -2191,12 +2313,12 @@ function importGeometry(geometry, mesh) {
2191
2313
 
2192
2314
  // src/functions/gltf-loading/import-mesh.ts
2193
2315
  Object3D5.DEFAULT_UP.set(0, 0, 1);
2194
- function importMesh(mesh, namespaces, colorLookup) {
2316
+ function importMesh(mesh, colorLookup) {
2195
2317
  let world = {};
2196
2318
  let animatables = {};
2197
2319
  let newColorLookup = {};
2198
2320
  const translationAnimatable = {
2199
- id: crypto.randomUUID(),
2321
+ id: createBrowserSafeId4(),
2200
2322
  name: `${mesh.name ?? "Mesh"} translation`,
2201
2323
  type: "vector3",
2202
2324
  default: { x: mesh.position.x, y: mesh.position.y, z: mesh.position.z },
@@ -2212,7 +2334,7 @@ function importMesh(mesh, namespaces, colorLookup) {
2212
2334
  [translationAnimatable.id]: translationAnimatable
2213
2335
  };
2214
2336
  const rotationAnimatable = {
2215
- id: crypto.randomUUID(),
2337
+ id: createBrowserSafeId4(),
2216
2338
  name: `${mesh.name ?? "Mesh"} rotation`,
2217
2339
  type: "euler",
2218
2340
  default: { x: mesh.rotation.x, y: mesh.rotation.y, z: mesh.rotation.z },
@@ -2225,7 +2347,7 @@ function importMesh(mesh, namespaces, colorLookup) {
2225
2347
  };
2226
2348
  animatables = { ...animatables, [rotationAnimatable.id]: rotationAnimatable };
2227
2349
  const scaleAnimatable = {
2228
- id: crypto.randomUUID(),
2350
+ id: createBrowserSafeId4(),
2229
2351
  name: `${mesh.name ?? "Mesh"} scale`,
2230
2352
  type: "vector3",
2231
2353
  default: { x: mesh.scale.x, y: mesh.scale.y, z: mesh.scale.z },
@@ -2236,56 +2358,120 @@ function importMesh(mesh, namespaces, colorLookup) {
2236
2358
  }
2237
2359
  };
2238
2360
  animatables = { ...animatables, [scaleAnimatable.id]: scaleAnimatable };
2239
- const color = mesh.material.color;
2240
- let useEmissive = false;
2241
- if (color.r === 0 && color.g === 0 && color.b === 0 && mesh.material.emissive) {
2242
- color.copy(mesh.material.emissive);
2243
- useEmissive = true;
2244
- }
2245
- const colorName = mesh.material.name;
2246
- const colorAnimatable = {
2247
- id: crypto.randomUUID(),
2248
- name: mesh.material.name ?? `${mesh.name ?? "Mesh"} color`,
2249
- type: "rgb",
2250
- default: { r: color.r, g: color.g, b: color.b },
2251
- constraints: {
2252
- min: [0, 0, 0],
2253
- max: [1, 1, 1]
2254
- },
2361
+ const material = mesh.material;
2362
+ const color = material.color;
2363
+ const colorName = material.name;
2364
+ const label = (suffix) => colorName ? `${colorName} ${suffix}` : `${mesh.name ?? "Mesh"} ${suffix}`;
2365
+ const makeNumberAnimatable = (suffix, value, constraints) => ({
2366
+ id: createBrowserSafeId4(),
2367
+ name: label(suffix),
2368
+ type: "number",
2369
+ default: value,
2370
+ constraints,
2255
2371
  pub: {
2256
2372
  public: true,
2257
- output: mesh.material.name ? `${mesh.material.name} color` : `${mesh.name ?? "Mesh"} color`
2373
+ output: label(suffix)
2258
2374
  }
2259
- };
2260
- const opacityAnimatable = {
2261
- id: crypto.randomUUID(),
2262
- name: mesh.material.name ? `${mesh.material.name} opacity` : `${mesh.name ?? "Mesh"} opacity`,
2263
- type: "number",
2264
- default: mesh.material.opacity,
2375
+ });
2376
+ const makeColorAnimatable = (suffix, value) => ({
2377
+ id: createBrowserSafeId4(),
2378
+ name: label(suffix),
2379
+ type: "rgb",
2380
+ default: { r: value.r, g: value.g, b: value.b },
2265
2381
  constraints: {
2266
- min: 0,
2267
- max: 1
2382
+ min: [0, 0, 0],
2383
+ max: [1, 1, 1]
2268
2384
  },
2269
2385
  pub: {
2270
2386
  public: true,
2271
- output: mesh.material.name ? `${mesh.material.name} opacity` : `${mesh.name ?? "Mesh"} opacity`
2387
+ output: label(suffix)
2272
2388
  }
2389
+ });
2390
+ const colorAnimatable = {
2391
+ ...makeColorAnimatable("color", color),
2392
+ name: material.name ?? `${mesh.name ?? "Mesh"} color`
2273
2393
  };
2274
- let colorId = colorAnimatable.id;
2275
- let opacityId = opacityAnimatable.id;
2394
+ const opacityAnimatable = makeNumberAnimatable("opacity", material.opacity, {
2395
+ min: 0,
2396
+ max: 1
2397
+ });
2398
+ const materialAnimatables = [
2399
+ colorAnimatable,
2400
+ opacityAnimatable
2401
+ ];
2402
+ let roughnessAnimatable;
2403
+ let metalnessAnimatable;
2404
+ let emissiveAnimatable;
2405
+ let emissiveIntensityAnimatable;
2406
+ let shininessAnimatable;
2407
+ let specularAnimatable;
2408
+ if (material.isMeshStandardMaterial) {
2409
+ roughnessAnimatable = makeNumberAnimatable(
2410
+ "roughness",
2411
+ material.roughness,
2412
+ {
2413
+ min: 0,
2414
+ max: 1
2415
+ }
2416
+ );
2417
+ metalnessAnimatable = makeNumberAnimatable(
2418
+ "metalness",
2419
+ material.metalness,
2420
+ {
2421
+ min: 0,
2422
+ max: 1
2423
+ }
2424
+ );
2425
+ emissiveAnimatable = makeColorAnimatable("emissive", material.emissive);
2426
+ emissiveIntensityAnimatable = makeNumberAnimatable(
2427
+ "emissive intensity",
2428
+ material.emissiveIntensity,
2429
+ { min: 0 }
2430
+ );
2431
+ materialAnimatables.push(
2432
+ roughnessAnimatable,
2433
+ metalnessAnimatable,
2434
+ emissiveAnimatable,
2435
+ emissiveIntensityAnimatable
2436
+ );
2437
+ } else if (mesh.material.isMeshPhongMaterial) {
2438
+ const phong = mesh.material;
2439
+ shininessAnimatable = makeNumberAnimatable("shininess", phong.shininess, {
2440
+ min: 0
2441
+ });
2442
+ emissiveAnimatable = makeColorAnimatable("emissive", phong.emissive);
2443
+ emissiveIntensityAnimatable = makeNumberAnimatable(
2444
+ "emissive intensity",
2445
+ phong.emissiveIntensity,
2446
+ { min: 0 }
2447
+ );
2448
+ specularAnimatable = makeColorAnimatable("specular", phong.specular);
2449
+ materialAnimatables.push(
2450
+ shininessAnimatable,
2451
+ emissiveAnimatable,
2452
+ emissiveIntensityAnimatable,
2453
+ specularAnimatable
2454
+ );
2455
+ }
2456
+ let materialIds;
2276
2457
  if (colorName && colorLookup[colorName]) {
2277
- colorId = colorLookup[colorName][0];
2278
- opacityId = colorLookup[colorName][1];
2279
- useEmissive = colorLookup[colorName][2];
2458
+ materialIds = colorLookup[colorName];
2280
2459
  } else {
2281
- animatables = { ...animatables, [colorAnimatable.id]: colorAnimatable };
2282
- animatables = { ...animatables, [opacityAnimatable.id]: opacityAnimatable };
2460
+ materialIds = {
2461
+ color: colorAnimatable.id,
2462
+ opacity: opacityAnimatable.id,
2463
+ roughness: roughnessAnimatable?.id,
2464
+ metalness: metalnessAnimatable?.id,
2465
+ emissive: emissiveAnimatable?.id,
2466
+ emissiveIntensity: emissiveIntensityAnimatable?.id,
2467
+ shininess: shininessAnimatable?.id,
2468
+ specular: specularAnimatable?.id
2469
+ };
2470
+ materialAnimatables.forEach((animatable) => {
2471
+ animatables = { ...animatables, [animatable.id]: animatable };
2472
+ });
2283
2473
  if (colorName) {
2284
- newColorLookup[colorName] = [
2285
- colorAnimatable.id,
2286
- opacityAnimatable.id,
2287
- useEmissive
2288
- ];
2474
+ newColorLookup[colorName] = materialIds;
2289
2475
  }
2290
2476
  }
2291
2477
  const [geometryFeatures, geometryAnimatables, morphTargets] = importGeometry(
@@ -2296,7 +2482,7 @@ function importMesh(mesh, namespaces, colorLookup) {
2296
2482
  const children = [];
2297
2483
  mesh.children.forEach((child) => {
2298
2484
  if (child.isMesh) {
2299
- const [newWorldItems, newAnimatables, childId, newMeshColors] = importMesh(child, namespaces, {
2485
+ const [newWorldItems, newAnimatables, childId, newMeshColors] = importMesh(child, {
2300
2486
  ...colorLookup,
2301
2487
  ...newColorLookup
2302
2488
  });
@@ -2305,7 +2491,7 @@ function importMesh(mesh, namespaces, colorLookup) {
2305
2491
  animatables = { ...animatables, ...newAnimatables };
2306
2492
  children.push(childId);
2307
2493
  } else if (shouldImportAsGroupChild(child)) {
2308
- const [newWorldItems, newAnimatables, childId, newMeshColors] = importGroup(child, namespaces, {
2494
+ const [newWorldItems, newAnimatables, childId, newMeshColors] = importGroup(child, {
2309
2495
  ...colorLookup,
2310
2496
  ...newColorLookup
2311
2497
  });
@@ -2319,7 +2505,7 @@ function importMesh(mesh, namespaces, colorLookup) {
2319
2505
  id: mesh.uuid,
2320
2506
  name: mesh.name,
2321
2507
  geometry: mesh.geometry,
2322
- material: getShapeMaterial(mesh, useEmissive),
2508
+ material: getShapeMaterial(mesh),
2323
2509
  type: "shape",
2324
2510
  tags: [],
2325
2511
  morphTargets,
@@ -2327,21 +2513,28 @@ function importMesh(mesh, namespaces, colorLookup) {
2327
2513
  translation: { animated: true, value: translationAnimatable.id },
2328
2514
  rotation: { animated: true, value: rotationAnimatable.id },
2329
2515
  scale: { animated: true, value: scaleAnimatable.id },
2330
- color: { animated: true, value: colorId },
2331
- opacity: { animated: true, value: opacityId },
2516
+ color: { animated: true, value: materialIds.color },
2517
+ opacity: { animated: true, value: materialIds.opacity },
2518
+ ...materialIds.roughness ? { roughness: { animated: true, value: materialIds.roughness } } : {},
2519
+ ...materialIds.metalness ? { metalness: { animated: true, value: materialIds.metalness } } : {},
2520
+ ...materialIds.emissive ? { emissive: { animated: true, value: materialIds.emissive } } : {},
2521
+ ...materialIds.emissiveIntensity ? {
2522
+ emissiveIntensity: {
2523
+ animated: true,
2524
+ value: materialIds.emissiveIntensity
2525
+ }
2526
+ } : {},
2527
+ ...materialIds.shininess ? { shininess: { animated: true, value: materialIds.shininess } } : {},
2528
+ ...materialIds.specular ? { specular: { animated: true, value: materialIds.specular } } : {},
2332
2529
  ...geometryFeatures
2333
2530
  },
2334
- children: children.length > 0 ? children : void 0,
2335
- refs: namespaceArrayToRefs(namespaces)
2531
+ children: children.length > 0 ? children : void 0
2336
2532
  };
2337
2533
  world = { ...world, [newShape.id]: newShape };
2338
2534
  return [world, animatables, newShape.id, newColorLookup];
2339
2535
  }
2340
- function getShapeMaterial(mesh, useEmissive) {
2536
+ function getShapeMaterial(mesh) {
2341
2537
  const material = mesh.material;
2342
- if (useEmissive) {
2343
- return "basic" /* Basic */;
2344
- }
2345
2538
  if (material.isMeshStandardMaterial) {
2346
2539
  return "standard" /* Standard */;
2347
2540
  } else if (material.isMeshPhongMaterial) {
@@ -2377,13 +2570,13 @@ function shouldImportAsGroupChild(child) {
2377
2570
 
2378
2571
  // src/functions/gltf-loading/import-group.ts
2379
2572
  Object3D6.DEFAULT_UP.set(0, 0, 1);
2380
- function importGroup(group, namespaces, colorLookup, rootBounds) {
2573
+ function importGroup(group, colorLookup, rootBounds) {
2381
2574
  let world = {};
2382
2575
  let animatables = {};
2383
2576
  let newColorLookup = {};
2384
2577
  const children = [];
2385
2578
  const translationAnimatable = {
2386
- id: crypto.randomUUID(),
2579
+ id: createBrowserSafeId5(),
2387
2580
  name: `${group.name ?? "Group"} translation`,
2388
2581
  type: "vector3",
2389
2582
  default: { x: group.position.x, y: group.position.y, z: group.position.z },
@@ -2399,7 +2592,7 @@ function importGroup(group, namespaces, colorLookup, rootBounds) {
2399
2592
  [translationAnimatable.id]: translationAnimatable
2400
2593
  };
2401
2594
  const rotationAnimatable = {
2402
- id: crypto.randomUUID(),
2595
+ id: createBrowserSafeId5(),
2403
2596
  name: `${group.name ?? "Group"} rotation`,
2404
2597
  type: "euler",
2405
2598
  default: { x: group.rotation.x, y: group.rotation.y, z: group.rotation.z },
@@ -2412,7 +2605,7 @@ function importGroup(group, namespaces, colorLookup, rootBounds) {
2412
2605
  };
2413
2606
  animatables = { ...animatables, [rotationAnimatable.id]: rotationAnimatable };
2414
2607
  const scaleAnimatable = {
2415
- id: crypto.randomUUID(),
2608
+ id: createBrowserSafeId5(),
2416
2609
  name: `${group.name ?? "Group"} scale`,
2417
2610
  type: "vector3",
2418
2611
  default: { x: group.scale.x, y: group.scale.y, z: group.scale.z },
@@ -2425,7 +2618,7 @@ function importGroup(group, namespaces, colorLookup, rootBounds) {
2425
2618
  animatables = { ...animatables, [scaleAnimatable.id]: scaleAnimatable };
2426
2619
  group.children.forEach((child) => {
2427
2620
  if (child.isMesh) {
2428
- const [newWorldItems, newAnimatables, childId, newMeshColors] = importMesh(child, namespaces, {
2621
+ const [newWorldItems, newAnimatables, childId, newMeshColors] = importMesh(child, {
2429
2622
  ...colorLookup,
2430
2623
  ...newColorLookup
2431
2624
  });
@@ -2434,7 +2627,7 @@ function importGroup(group, namespaces, colorLookup, rootBounds) {
2434
2627
  animatables = { ...animatables, ...newAnimatables };
2435
2628
  children.push(childId);
2436
2629
  } else if (shouldImportAsGroupChild2(child)) {
2437
- const [newWorldItems, newAnimatables, childId, newMeshColors] = importGroup(child, namespaces, {
2630
+ const [newWorldItems, newAnimatables, childId, newMeshColors] = importGroup(child, {
2438
2631
  ...colorLookup,
2439
2632
  ...newColorLookup
2440
2633
  });
@@ -2456,8 +2649,7 @@ function importGroup(group, namespaces, colorLookup, rootBounds) {
2456
2649
  },
2457
2650
  root: Boolean(rootBounds),
2458
2651
  rootBounds,
2459
- children,
2460
- refs: namespaceArrayToRefs(namespaces)
2652
+ children
2461
2653
  };
2462
2654
  world = { ...world, [newGroup.id]: newGroup };
2463
2655
  return [world, animatables, newGroup.id, newColorLookup];
@@ -2483,12 +2675,11 @@ function shouldImportAsGroupChild2(child) {
2483
2675
 
2484
2676
  // src/functions/gltf-loading/import-scene.ts
2485
2677
  Object3D7.DEFAULT_UP.set(0, 0, 1);
2486
- function importScene(scene, namespaces, rootBounds) {
2678
+ function importScene(scene, rootBounds) {
2487
2679
  let world = {};
2488
2680
  let animatables = {};
2489
2681
  const [newWorldItems, newAnimatables] = importGroup(
2490
2682
  scene,
2491
- namespaces,
2492
2683
  {},
2493
2684
  rootBounds
2494
2685
  );
@@ -2499,7 +2690,7 @@ function importScene(scene, namespaces, rootBounds) {
2499
2690
 
2500
2691
  // src/functions/gltf-loading/traverse-three.ts
2501
2692
  THREE4.Object3D.DEFAULT_UP.set(0, 0, 1);
2502
- function traverseThree(group, namespaces, aggressiveImport = false, rootBounds) {
2693
+ function traverseThree(group, aggressiveImport = false, rootBounds) {
2503
2694
  const worldData = {};
2504
2695
  const animatableData = {};
2505
2696
  let hasRobotData = false;
@@ -2522,11 +2713,7 @@ function traverseThree(group, namespaces, aggressiveImport = false, rootBounds)
2522
2713
  switch (data.type) {
2523
2714
  case "group":
2524
2715
  loadedData = {
2525
- ...data,
2526
- refs: namespaces.reduce(
2527
- (acc, ns) => ({ ...acc, [ns]: createRef3() }),
2528
- {}
2529
- )
2716
+ ...data
2530
2717
  };
2531
2718
  [mappedFeatures, animatableValues] = mapFeatures(data.features);
2532
2719
  isGroupFeatures(mappedFeatures);
@@ -2537,11 +2724,7 @@ function traverseThree(group, namespaces, aggressiveImport = false, rootBounds)
2537
2724
  case "shape":
2538
2725
  loadedData = {
2539
2726
  ...data,
2540
- geometry: child.geometry,
2541
- refs: namespaces.reduce(
2542
- (acc, ns) => ({ ...acc, [ns]: createRef3() }),
2543
- {}
2544
- )
2727
+ geometry: child.geometry
2545
2728
  };
2546
2729
  [mappedFeatures, animatableValues] = mapFeatures(data.features);
2547
2730
  isShapeFeatures(mappedFeatures);
@@ -2551,11 +2734,7 @@ function traverseThree(group, namespaces, aggressiveImport = false, rootBounds)
2551
2734
  break;
2552
2735
  case "ellipse":
2553
2736
  loadedData = {
2554
- ...data,
2555
- refs: namespaces.reduce(
2556
- (acc, ns) => ({ ...acc, [ns]: createRef3() }),
2557
- {}
2558
- )
2737
+ ...data
2559
2738
  };
2560
2739
  [mappedFeatures, animatableValues] = mapFeatures(data.features);
2561
2740
  isEllipseFeatures(mappedFeatures);
@@ -2565,11 +2744,7 @@ function traverseThree(group, namespaces, aggressiveImport = false, rootBounds)
2565
2744
  break;
2566
2745
  case "rectangle":
2567
2746
  loadedData = {
2568
- ...data,
2569
- refs: namespaces.reduce(
2570
- (acc, ns) => ({ ...acc, [ns]: createRef3() }),
2571
- {}
2572
- )
2747
+ ...data
2573
2748
  };
2574
2749
  [mappedFeatures, animatableValues] = mapFeatures(data.features);
2575
2750
  isRectangleFeatures(mappedFeatures);
@@ -2592,7 +2767,6 @@ function traverseThree(group, namespaces, aggressiveImport = false, rootBounds)
2592
2767
  }
2593
2768
  const [newWorldData, newAnimatableData] = importScene(
2594
2769
  group,
2595
- namespaces,
2596
2770
  derivedRootBounds
2597
2771
  );
2598
2772
  Object.assign(worldData, newWorldData);
@@ -2605,7 +2779,10 @@ var MATERIAL_NAME_FEATURE_KEYS = [
2605
2779
  "opacity",
2606
2780
  "roughness",
2607
2781
  "metalness",
2608
- "shininess"
2782
+ "shininess",
2783
+ "emissive",
2784
+ "emissiveIntensity",
2785
+ "specular"
2609
2786
  ];
2610
2787
  var MATERIAL_NAME_SUFFIXES2 = [
2611
2788
  " color",
@@ -2614,7 +2791,10 @@ var MATERIAL_NAME_SUFFIXES2 = [
2614
2791
  " opacity",
2615
2792
  " roughness",
2616
2793
  " metalness",
2617
- " shininess"
2794
+ " shininess",
2795
+ " emissive intensity",
2796
+ " emissive",
2797
+ " specular"
2618
2798
  ];
2619
2799
  function applyStoredRenderableNames(object, data) {
2620
2800
  if (typeof data.name === "string" && data.name.length > 0) {
@@ -2834,12 +3014,29 @@ function applyVizijBundle(object, bundle) {
2834
3014
  const userData = object.userData && typeof object.userData === "object" ? object.userData : {};
2835
3015
  const originalExtensions = userData.gltfExtensions;
2836
3016
  let applied = false;
3017
+ const staleCarriers = [];
2837
3018
  if (bundle) {
2838
3019
  userData.gltfExtensions = {
2839
3020
  ...originalExtensions ?? {},
2840
3021
  VIZIJ_bundle: bundle
2841
3022
  };
2842
3023
  object.userData = userData;
3024
+ object.traverse((node) => {
3025
+ if (node === object) {
3026
+ return;
3027
+ }
3028
+ const extensions = node.userData?.gltfExtensions;
3029
+ if (!extensions || !(BUNDLE_KEYS[0] in extensions)) {
3030
+ return;
3031
+ }
3032
+ staleCarriers.push({ node, extensions });
3033
+ const { [BUNDLE_KEYS[0]]: _stale, ...rest } = extensions;
3034
+ if (Object.keys(rest).length > 0) {
3035
+ node.userData.gltfExtensions = rest;
3036
+ } else {
3037
+ delete node.userData.gltfExtensions;
3038
+ }
3039
+ });
2843
3040
  applied = true;
2844
3041
  }
2845
3042
  return () => {
@@ -2856,6 +3053,10 @@ function applyVizijBundle(object, bundle) {
2856
3053
  delete object.userData;
2857
3054
  }
2858
3055
  }
3056
+ for (const { node, extensions } of staleCarriers) {
3057
+ node.userData.gltfExtensions = extensions;
3058
+ }
3059
+ staleCarriers.length = 0;
2859
3060
  applied = false;
2860
3061
  };
2861
3062
  }
@@ -3281,16 +3482,6 @@ async function resolveParserJson(parserJson, fallback) {
3281
3482
  return fromArrayBuffer;
3282
3483
  }
3283
3484
  }
3284
- if (fallback.blob) {
3285
- try {
3286
- const blobBuffer = typeof fallback.blob.arrayBuffer === "function" ? await fallback.blob.arrayBuffer() : await new Response(fallback.blob).arrayBuffer();
3287
- const fromBlob = parseGlbJsonChunk(blobBuffer);
3288
- if (fromBlob && typeof fromBlob === "object") {
3289
- return fromBlob;
3290
- }
3291
- } catch {
3292
- }
3293
- }
3294
3485
  if (fallback.url && typeof fetch === "function") {
3295
3486
  try {
3296
3487
  const response = await fetch(fallback.url);
@@ -3306,81 +3497,21 @@ async function resolveParserJson(parserJson, fallback) {
3306
3497
  }
3307
3498
  return parserJson;
3308
3499
  }
3309
- async function loadGLTF(url, namespaces, aggressiveImport = false, rootBounds) {
3310
- const modelLoader = new GLTFLoader();
3311
- modelLoader.setDRACOLoader(new DRACOLoader());
3312
- const modelData = await modelLoader.loadAsync(url);
3313
- const parserJson = await resolveParserJson(modelData?.parser?.json, {
3314
- url
3315
- });
3316
- const actualizedNamespaces = namespaces.length > 0 ? namespaces : ["default"];
3317
- const asset = parseScene(
3318
- modelData.scene,
3319
- actualizedNamespaces,
3500
+ async function loadGLTF(url, aggressiveImport = false, rootBounds) {
3501
+ const asset = await loadGLTFWithBundle(url, aggressiveImport, rootBounds);
3502
+ return [asset.world, asset.animatables, asset.animations];
3503
+ }
3504
+ async function loadGLTFFromBlob(blob, aggressiveImport = false, rootBounds) {
3505
+ const asset = await loadGLTFFromBlobWithBundle(
3506
+ blob,
3320
3507
  aggressiveImport,
3321
- rootBounds,
3322
- parserJson,
3323
- modelData.animations
3508
+ rootBounds
3324
3509
  );
3325
3510
  return [asset.world, asset.animatables, asset.animations];
3326
3511
  }
3327
- async function loadGLTFFromBlob(blob, namespaces, aggressiveImport = false, rootBounds) {
3328
- const actualizedNamespaces = namespaces.length > 0 ? namespaces : ["default"];
3329
- if (typeof URL !== "undefined" && typeof URL.createObjectURL === "function") {
3330
- const objectUrl = URL.createObjectURL(blob);
3331
- try {
3332
- const asset = await loadGLTFWithBundle(
3333
- objectUrl,
3334
- actualizedNamespaces,
3335
- aggressiveImport,
3336
- rootBounds,
3337
- { blob }
3338
- );
3339
- return [asset.world, asset.animatables, asset.animations];
3340
- } finally {
3341
- URL.revokeObjectURL(objectUrl);
3342
- }
3343
- }
3344
- const arrayBuffer = typeof blob.arrayBuffer === "function" ? await blob.arrayBuffer() : await new Response(blob).arrayBuffer();
3345
- return new Promise((resolve, reject) => {
3346
- const loader = new GLTFLoader();
3347
- loader.setDRACOLoader(new DRACOLoader());
3348
- loader.parse(
3349
- arrayBuffer,
3350
- "",
3351
- async (gltf) => {
3352
- try {
3353
- const parserJson = await resolveParserJson(
3354
- gltf?.parser?.json,
3355
- { arrayBuffer }
3356
- );
3357
- const asset = parseScene(
3358
- gltf.scene,
3359
- actualizedNamespaces,
3360
- aggressiveImport,
3361
- rootBounds,
3362
- parserJson,
3363
- gltf.animations
3364
- );
3365
- resolve([asset.world, asset.animatables, asset.animations]);
3366
- } catch (error) {
3367
- if (error instanceof Error) {
3368
- reject(error);
3369
- } else {
3370
- reject(new Error(String(error)));
3371
- }
3372
- }
3373
- },
3374
- (error) => {
3375
- reject(new Error(`Error loading GLTF: ${error.message}`));
3376
- }
3377
- );
3378
- });
3379
- }
3380
- function parseScene(scene, namespaces, aggressiveImport, rootBounds, parserJson, clips) {
3512
+ function parseScene(scene, aggressiveImport, rootBounds, parserJson, clips) {
3381
3513
  const [world, animatables] = traverseThree(
3382
3514
  scene,
3383
- namespaces,
3384
3515
  aggressiveImport,
3385
3516
  rootBounds
3386
3517
  );
@@ -3388,105 +3519,49 @@ function parseScene(scene, namespaces, aggressiveImport, rootBounds, parserJson,
3388
3519
  const animations = extractVizijAnimations(parserJson, clips);
3389
3520
  return { world, animatables, bundle, animations, scene };
3390
3521
  }
3391
- async function loadGLTFWithBundle(url, namespaces, aggressiveImport = false, rootBounds, parserJsonFallback) {
3522
+ function createTrackedGLTFLoader() {
3523
+ recordRenderCounter("gltfLoadCount");
3524
+ recordRenderCounter("dracoLoaderCreationCount");
3392
3525
  const modelLoader = new GLTFLoader();
3393
3526
  modelLoader.setDRACOLoader(new DRACOLoader());
3527
+ return modelLoader;
3528
+ }
3529
+ async function loadGLTFWithBundle(url, aggressiveImport = false, rootBounds, parserJsonFallback) {
3530
+ recordRenderCounter("gltfUrlLoadCount");
3531
+ const modelLoader = createTrackedGLTFLoader();
3394
3532
  const modelData = await modelLoader.loadAsync(url);
3395
3533
  const parserJson = await resolveParserJson(modelData?.parser?.json, {
3396
3534
  url,
3397
3535
  ...parserJsonFallback
3398
3536
  });
3399
- const actualizedNamespaces = namespaces.length > 0 ? namespaces : ["default"];
3400
3537
  return parseScene(
3401
3538
  modelData.scene,
3402
- actualizedNamespaces,
3403
3539
  aggressiveImport,
3404
3540
  rootBounds,
3405
3541
  parserJson,
3406
3542
  modelData.animations
3407
3543
  );
3408
3544
  }
3409
- async function loadGLTFFromBlobWithBundle(blob, namespaces, aggressiveImport = false, rootBounds) {
3410
- const actualizedNamespaces = namespaces.length > 0 ? namespaces : ["default"];
3411
- if (typeof URL !== "undefined" && typeof URL.createObjectURL === "function") {
3412
- const objectUrl = URL.createObjectURL(blob);
3413
- try {
3414
- return await loadGLTFWithBundle(
3415
- objectUrl,
3416
- actualizedNamespaces,
3417
- aggressiveImport,
3418
- rootBounds,
3419
- { blob }
3420
- );
3421
- } finally {
3422
- URL.revokeObjectURL(objectUrl);
3423
- }
3424
- }
3425
- const arrayBuffer = typeof blob.arrayBuffer === "function" ? await blob.arrayBuffer() : await new Response(blob).arrayBuffer();
3426
- return new Promise((resolve, reject) => {
3427
- const loader = new GLTFLoader();
3428
- loader.setDRACOLoader(new DRACOLoader());
3429
- loader.parse(
3430
- arrayBuffer,
3431
- "",
3432
- async (gltf) => {
3433
- try {
3434
- const parserJson = await resolveParserJson(
3435
- gltf?.parser?.json,
3436
- { arrayBuffer }
3437
- );
3438
- const asset = parseScene(
3439
- gltf.scene,
3440
- actualizedNamespaces,
3441
- aggressiveImport,
3442
- rootBounds,
3443
- parserJson,
3444
- gltf.animations
3445
- );
3446
- resolve(asset);
3447
- } catch (error) {
3448
- if (error instanceof Error) {
3449
- reject(error);
3450
- } else {
3451
- reject(new Error(String(error)));
3452
- }
3453
- }
3454
- },
3455
- (error) => {
3456
- reject(new Error(`Error loading GLTF: ${error.message}`));
3457
- }
3458
- );
3545
+ async function loadGLTFFromBlobWithBundle(blob, aggressiveImport = false, rootBounds) {
3546
+ recordRenderCounter("gltfBlobLoadCount");
3547
+ const arrayBuffer = await blob.arrayBuffer();
3548
+ const loader = createTrackedGLTFLoader();
3549
+ const gltf = await new Promise((resolve, reject) => {
3550
+ loader.parse(arrayBuffer, "", resolve, (error) => {
3551
+ reject(new Error(`Error loading GLTF: ${error.message}`));
3552
+ });
3459
3553
  });
3460
- }
3461
-
3462
- // src/functions/load-gltf-blob.ts
3463
- import { DRACOLoader as DRACOLoader2, GLTFLoader as GLTFLoader2 } from "three-stdlib";
3464
- var loadGltfFromBlob = (blob, namespaces) => {
3465
- return new Promise((resolve, reject) => {
3466
- const loader = new GLTFLoader2();
3467
- loader.setDRACOLoader(new DRACOLoader2());
3468
- const reader = new FileReader();
3469
- reader.onload = () => {
3470
- const arrayBuffer = reader.result;
3471
- loader.parse(
3472
- arrayBuffer,
3473
- "",
3474
- // Base path for resolving external resources
3475
- (gltf) => {
3476
- const actualizedNamespaces = namespaces.length > 0 ? namespaces : ["default"];
3477
- resolve(traverseThree(gltf.scene, actualizedNamespaces));
3478
- },
3479
- (error) => {
3480
- reject(new Error(`Error loading GLTF: ${error.message}`));
3481
- }
3482
- );
3483
- };
3484
- reader.onerror = () => {
3485
- reject(new Error("Failed to read Blob as ArrayBuffer."));
3486
- };
3487
- reader.readAsArrayBuffer(blob);
3554
+ const parserJson = await resolveParserJson(gltf?.parser?.json, {
3555
+ arrayBuffer
3488
3556
  });
3489
- };
3557
+ return parseScene(
3558
+ gltf.scene,
3559
+ aggressiveImport,
3560
+ rootBounds,
3561
+ parserJson,
3562
+ gltf.animations
3563
+ );
3564
+ }
3490
3565
 
3491
3566
  // src/functions/export.ts
3492
3567
  import { GLTFExporter } from "three-stdlib";
@@ -3501,17 +3576,6 @@ function normalizeExportError(error) {
3501
3576
  }
3502
3577
  return new Error("Failed to export scene.");
3503
3578
  }
3504
- function triggerBlobDownload(blob, filename) {
3505
- const url = URL.createObjectURL(blob);
3506
- const link = document.createElement("a");
3507
- link.href = url;
3508
- link.download = filename;
3509
- link.style.display = "none";
3510
- document.body.appendChild(link);
3511
- link.click();
3512
- document.body.removeChild(link);
3513
- setTimeout(() => URL.revokeObjectURL(url), 0);
3514
- }
3515
3579
  var GLB_MAGIC2 = 1179937895;
3516
3580
  var GLB_VERSION2 = 2;
3517
3581
  var GLB_JSON_CHUNK_TYPE2 = 1313821514;
@@ -3593,7 +3657,7 @@ function normalizeExportedSceneJson(json, fallbackSceneName) {
3593
3657
  }
3594
3658
  return changed;
3595
3659
  }
3596
- function sanitizeExportedGlb(buffer, fallbackSceneName) {
3660
+ function patchGlbJson(buffer, mutate) {
3597
3661
  if (buffer.byteLength < GLB_HEADER_BYTES2 + GLB_CHUNK_HEADER_BYTES2) {
3598
3662
  return buffer;
3599
3663
  }
@@ -3623,8 +3687,7 @@ function sanitizeExportedGlb(buffer, fallbackSceneName) {
3623
3687
  } catch {
3624
3688
  return buffer;
3625
3689
  }
3626
- const changed = normalizeExportedSceneJson(jsonPayload, fallbackSceneName);
3627
- if (!changed) {
3690
+ if (!mutate(jsonPayload)) {
3628
3691
  return buffer;
3629
3692
  }
3630
3693
  const encodedJson = new TextEncoder().encode(JSON.stringify(jsonPayload));
@@ -3646,11 +3709,41 @@ function sanitizeExportedGlb(buffer, fallbackSceneName) {
3646
3709
  sanitizedBytes.set(remainingChunks, jsonChunkStart + paddedJsonLength);
3647
3710
  return sanitized;
3648
3711
  }
3649
- function exportScene(data, fileNameOrOptions = "scene.glb") {
3650
- const options = typeof fileNameOrOptions === "string" ? { fileName: fileNameOrOptions } : fileNameOrOptions ?? {};
3651
- const fileName = options.fileName ?? "scene.glb";
3652
- const animationClips = Array.isArray(options.animations) ? options.animations.filter(Boolean) : [];
3653
- const shouldAttachBundle = Boolean(options.bundle);
3712
+ function sanitizeExportedGlb(buffer, fallbackSceneName) {
3713
+ return patchGlbJson(
3714
+ buffer,
3715
+ (json) => normalizeExportedSceneJson(json, fallbackSceneName)
3716
+ );
3717
+ }
3718
+ function patchVizijBundleMetadata(glb, metadata) {
3719
+ return patchGlbJson(glb, (json) => {
3720
+ const carriers = [];
3721
+ const rootExtensions = json.extensions;
3722
+ if (rootExtensions && typeof rootExtensions === "object") {
3723
+ carriers.push(rootExtensions);
3724
+ }
3725
+ const nodes = Array.isArray(json.nodes) ? json.nodes : [];
3726
+ for (const node of nodes) {
3727
+ const extensions = node?.extensions;
3728
+ if (extensions && typeof extensions === "object") {
3729
+ carriers.push(extensions);
3730
+ }
3731
+ }
3732
+ let patched = false;
3733
+ for (const extensions of carriers) {
3734
+ const bundle = extensions.VIZIJ_bundle;
3735
+ if (!bundle || typeof bundle !== "object") {
3736
+ continue;
3737
+ }
3738
+ const record = bundle;
3739
+ const existing = record.metadata && typeof record.metadata === "object" ? record.metadata : {};
3740
+ record.metadata = { ...existing, ...metadata };
3741
+ patched = true;
3742
+ }
3743
+ return patched;
3744
+ });
3745
+ }
3746
+ async function exportScene(root, options = {}) {
3654
3747
  const exporter = new GLTFExporter();
3655
3748
  exporter.register(() => ({
3656
3749
  writeMesh(mesh, meshDef) {
@@ -3660,81 +3753,330 @@ function exportScene(data, fileNameOrOptions = "scene.glb") {
3660
3753
  }
3661
3754
  }
3662
3755
  }));
3663
- const sourceRoot = data;
3664
- const exportRoot = sourceRoot instanceof THREE6.Scene ? sourceRoot : sourceRoot.clone(true);
3756
+ const exportRoot = root instanceof THREE6.Scene ? root : root.clone(true);
3665
3757
  const exportTarget = exportRoot instanceof THREE6.Scene ? exportRoot : (() => {
3666
3758
  const scene = new THREE6.Scene();
3667
- scene.name = data.name?.trim() || "Scene";
3759
+ scene.name = root.name?.trim() || "Scene";
3668
3760
  scene.add(exportRoot);
3669
3761
  return scene;
3670
3762
  })();
3671
- const detachBundle = shouldAttachBundle && options.bundle ? applyVizijBundle(exportRoot, options.bundle) : () => {
3763
+ const detachBundle = options.bundle ? applyVizijBundle(exportRoot, options.bundle) : () => {
3672
3764
  };
3673
- const binary = options.binary ?? true;
3674
- const exporterOptions = {
3675
- trs: true,
3676
- onlyVisible: false,
3677
- binary,
3678
- includeCustomExtensions: true
3679
- };
3680
- if (animationClips.length > 0) {
3681
- exporterOptions.animations = animationClips;
3682
- }
3765
+ const animations = (options.animations ?? []).filter(Boolean);
3766
+ let gltf;
3683
3767
  try {
3684
- exporter.parse(
3685
- exportTarget,
3686
- (gltf) => {
3687
- detachBundle();
3688
- if (!(gltf instanceof ArrayBuffer)) {
3689
- const error = new Error("Failed to export scene.");
3690
- options.onError?.(error);
3691
- return;
3692
- }
3693
- const sanitizedGltf = sanitizeExportedGlb(
3694
- gltf,
3695
- data.name?.trim() || void 0
3696
- );
3697
- const trimmed = fileName.trim();
3698
- const safeFileName = trimmed.length > 0 ? trimmed : "scene.glb";
3699
- const downloadName = safeFileName.toLowerCase().endsWith(".glb") ? safeFileName : `${safeFileName}.glb`;
3700
- triggerBlobDownload(
3701
- new Blob([sanitizedGltf], {
3702
- type: "application/octet-stream"
3703
- }),
3704
- downloadName
3705
- );
3706
- options.onComplete?.();
3707
- },
3708
- (error) => {
3709
- detachBundle();
3710
- options.onError?.(normalizeExportError(error));
3711
- },
3712
- exporterOptions
3713
- );
3768
+ gltf = await exporter.parseAsync(exportTarget, {
3769
+ trs: true,
3770
+ onlyVisible: false,
3771
+ binary: true,
3772
+ includeCustomExtensions: true,
3773
+ ...animations.length > 0 ? { animations } : {}
3774
+ });
3714
3775
  } catch (error) {
3776
+ throw normalizeExportError(error);
3777
+ } finally {
3715
3778
  detachBundle();
3716
- const normalizedError = normalizeExportError(error);
3717
- options.onError?.(normalizedError);
3718
- throw normalizedError;
3779
+ }
3780
+ if (!(gltf instanceof ArrayBuffer)) {
3781
+ throw new Error("Failed to export scene.");
3782
+ }
3783
+ return sanitizeExportedGlb(gltf, root.name?.trim() || void 0);
3784
+ }
3785
+
3786
+ // src/functions/build-scene-graph.ts
3787
+ import * as THREE7 from "three";
3788
+ import {
3789
+ instanceOfRawEuler as instanceOfRawEuler5,
3790
+ instanceOfRawHSL as instanceOfRawHSL4,
3791
+ instanceOfRawNumber as instanceOfRawNumber5,
3792
+ instanceOfRawRGB as instanceOfRawRGB4,
3793
+ instanceOfRawVector2 as instanceOfRawVector24,
3794
+ instanceOfRawVector3 as instanceOfRawVector35
3795
+ } from "@vizij/utils";
3796
+ THREE7.Object3D.DEFAULT_UP.set(0, 0, 1);
3797
+ function buildSceneGraph(world, animatables, rootId) {
3798
+ const entry = world[rootId];
3799
+ if (!entry) {
3800
+ throw new Error(`The world has no element "${rootId}".`);
3801
+ }
3802
+ const used = {};
3803
+ for (const feature of Object.values(entry.features)) {
3804
+ if (feature.animated) {
3805
+ used[feature.value] = animatableOf(animatables, feature.value);
3806
+ }
3807
+ }
3808
+ const robotData = createStoredRenderable(entry, used);
3809
+ switch (entry.type) {
3810
+ case "group": {
3811
+ const object = new THREE7.Group();
3812
+ object.name = entry.name;
3813
+ object.userData = { gltfExtensions: { RobotData: robotData } };
3814
+ applyFeatures(entry.features, animatables, groupTransform(object));
3815
+ for (const child of entry.children) {
3816
+ object.add(buildSceneGraph(world, animatables, child));
3817
+ }
3818
+ return object;
3819
+ }
3820
+ case "shape": {
3821
+ const geometry = entry.geometry.clone();
3822
+ if (entry.name) {
3823
+ geometry.name = entry.name;
3824
+ }
3825
+ const material = createMaterial(entry.material);
3826
+ material.name = deriveMaterialName(entry, used) ?? entry.name ?? "";
3827
+ const object = new THREE7.Mesh(geometry, material);
3828
+ object.name = entry.name;
3829
+ object.userData = { gltfExtensions: { RobotData: robotData } };
3830
+ applyFeatures(entry.features, animatables, {
3831
+ ...shapeTransform(object),
3832
+ ...materialFactors(material),
3833
+ ...morphWeights(object, entry)
3834
+ });
3835
+ for (const child of entry.children ?? []) {
3836
+ object.add(buildSceneGraph(world, animatables, child));
3837
+ }
3838
+ return object;
3839
+ }
3840
+ default:
3841
+ throw new Error(
3842
+ `Element "${rootId}" is a ${entry.type}, which has no exported form.`
3843
+ );
3844
+ }
3845
+ }
3846
+ function animatableOf(animatables, id) {
3847
+ const animatable = animatables[id];
3848
+ if (!animatable) {
3849
+ throw new Error(`No animatable "${id}".`);
3850
+ }
3851
+ return animatable;
3852
+ }
3853
+ function applyFeatures(features, animatables, handlers) {
3854
+ for (const [key, handler] of Object.entries(handlers)) {
3855
+ const feature = features[key];
3856
+ if (!feature) {
3857
+ continue;
3858
+ }
3859
+ const value = feature.animated ? authoredDefault(animatableOf(animatables, feature.value)) : feature.value;
3860
+ if (value !== void 0) {
3861
+ handler(value);
3862
+ }
3863
+ }
3864
+ }
3865
+ function groupTransform(object) {
3866
+ return {
3867
+ translation: (value) => {
3868
+ if (instanceOfRawVector35(value)) {
3869
+ object.position.set(value.x, value.y, value.z);
3870
+ } else if (instanceOfRawVector24(value)) {
3871
+ object.position.set(value.x, value.y, object.position.z);
3872
+ }
3873
+ },
3874
+ rotation: (value) => {
3875
+ if (instanceOfRawEuler5(value)) {
3876
+ object.rotation.set(value.x, value.y, value.z, "ZYX");
3877
+ } else if (instanceOfRawNumber5(value)) {
3878
+ object.rotation.set(0, 0, 0);
3879
+ object.rotateZ(value);
3880
+ }
3881
+ },
3882
+ scale: (value) => {
3883
+ if (instanceOfRawVector35(value)) {
3884
+ if (value.x === null || value.y === null || value.z === null) {
3885
+ object.scale.set(0.1, 0.1, 0.1);
3886
+ } else {
3887
+ object.scale.set(value.x, value.y, value.z);
3888
+ }
3889
+ } else if (instanceOfRawNumber5(value)) {
3890
+ object.scale.set(value, value, value);
3891
+ } else {
3892
+ object.scale.set(1, 1, 1);
3893
+ }
3894
+ }
3895
+ };
3896
+ }
3897
+ function shapeTransform(object) {
3898
+ return {
3899
+ translation: (value) => {
3900
+ if (instanceOfRawVector35(value)) {
3901
+ object.position.set(value.x, value.y, value.z);
3902
+ }
3903
+ },
3904
+ rotation: (value) => {
3905
+ if (instanceOfRawEuler5(value)) {
3906
+ object.rotation.set(value.x, value.y, value.z, "ZYX");
3907
+ }
3908
+ },
3909
+ scale: (value) => {
3910
+ if (instanceOfRawVector35(value)) {
3911
+ object.scale.set(value.x, value.y, value.z);
3912
+ } else if (instanceOfRawNumber5(value)) {
3913
+ object.scale.set(value, value, value);
3914
+ } else {
3915
+ object.scale.set(1, 1, 1);
3916
+ }
3917
+ }
3918
+ };
3919
+ }
3920
+ function materialFactors(material) {
3921
+ const standard = material;
3922
+ const phong = material;
3923
+ return {
3924
+ opacity: (value) => {
3925
+ if (instanceOfRawNumber5(value)) {
3926
+ material.opacity = value;
3927
+ material.transparent = value < 1;
3928
+ }
3929
+ },
3930
+ color: (value) => {
3931
+ if (!standard.color) {
3932
+ return;
3933
+ }
3934
+ if (instanceOfRawRGB4(value)) {
3935
+ standard.color.setRGB(value.r, value.g, value.b);
3936
+ } else if (instanceOfRawVector35(value)) {
3937
+ standard.color.setRGB(value.x, value.y, value.z);
3938
+ } else if (instanceOfRawHSL4(value)) {
3939
+ standard.color.setHSL(value.h, value.s, value.l);
3940
+ }
3941
+ },
3942
+ roughness: (value) => {
3943
+ if (standard.roughness !== void 0 && instanceOfRawNumber5(value)) {
3944
+ standard.roughness = value;
3945
+ }
3946
+ },
3947
+ metalness: (value) => {
3948
+ if (standard.metalness !== void 0 && instanceOfRawNumber5(value)) {
3949
+ standard.metalness = value;
3950
+ }
3951
+ },
3952
+ shininess: (value) => {
3953
+ if (phong.shininess !== void 0 && instanceOfRawNumber5(value)) {
3954
+ phong.shininess = value;
3955
+ }
3956
+ },
3957
+ emissive: (value) => {
3958
+ if (standard.emissive && instanceOfRawRGB4(value)) {
3959
+ standard.emissive.setRGB(value.r, value.g, value.b);
3960
+ }
3961
+ },
3962
+ emissiveIntensity: (value) => {
3963
+ if (standard.emissiveIntensity !== void 0 && instanceOfRawNumber5(value)) {
3964
+ standard.emissiveIntensity = value;
3965
+ }
3966
+ },
3967
+ specular: (value) => {
3968
+ if (phong.specular && instanceOfRawRGB4(value)) {
3969
+ phong.specular.setRGB(value.r, value.g, value.b);
3970
+ }
3971
+ }
3972
+ };
3973
+ }
3974
+ function morphWeights(object, shape) {
3975
+ const targets = shape.morphTargets ?? [];
3976
+ if (targets.length === 0) {
3977
+ return {};
3978
+ }
3979
+ const influences = targets.map(() => 0);
3980
+ object.morphTargetDictionary = Object.fromEntries(
3981
+ targets.map((target, index) => [target, index])
3982
+ );
3983
+ object.morphTargetInfluences = influences;
3984
+ return Object.fromEntries(
3985
+ targets.map((target, index) => [
3986
+ target,
3987
+ (value) => {
3988
+ if (instanceOfRawNumber5(value)) {
3989
+ influences[index] = value;
3990
+ }
3991
+ }
3992
+ ])
3993
+ );
3994
+ }
3995
+ function createMaterial(kind) {
3996
+ const parameters = { side: THREE7.DoubleSide };
3997
+ switch (kind) {
3998
+ case "basic" /* Basic */:
3999
+ return new THREE7.MeshBasicMaterial(parameters);
4000
+ case "lambert" /* Lambert */:
4001
+ return new THREE7.MeshLambertMaterial(parameters);
4002
+ case "phong" /* Phong */:
4003
+ return new THREE7.MeshPhongMaterial(parameters);
4004
+ case "normal" /* Normal */:
4005
+ return new THREE7.MeshNormalMaterial(parameters);
4006
+ case "standard" /* Standard */:
4007
+ return new THREE7.MeshStandardMaterial(parameters);
4008
+ }
4009
+ }
4010
+ var SCALAR_KEYS = ["f32", "f64", "i64", "i32", "u64", "u32", "float"];
4011
+ function scalarOf(value) {
4012
+ if (typeof value === "number") {
4013
+ return value;
4014
+ }
4015
+ if (value && typeof value === "object") {
4016
+ for (const key of SCALAR_KEYS) {
4017
+ const scalar = value[key];
4018
+ if (typeof scalar === "number") {
4019
+ return scalar;
4020
+ }
4021
+ }
4022
+ }
4023
+ return void 0;
4024
+ }
4025
+ function authoredDefault(animatable) {
4026
+ const value = animatable.default;
4027
+ const scalar = scalarOf(value);
4028
+ if (scalar !== void 0) {
4029
+ return scalar;
4030
+ }
4031
+ const fields = value?.struct?.fields;
4032
+ if (!Array.isArray(fields)) {
4033
+ return value;
4034
+ }
4035
+ const n = fields.map(
4036
+ (field) => scalarOf(field?.value) ?? 0
4037
+ );
4038
+ switch (animatable.type) {
4039
+ case "vector3":
4040
+ case "euler":
4041
+ return { x: n[0], y: n[1], z: n[2] };
4042
+ case "vector2":
4043
+ return { x: n[0], y: n[1] };
4044
+ case "rgb":
4045
+ return { r: n[0], g: n[1], b: n[2] };
4046
+ case "hsl":
4047
+ return { h: n[0], s: n[1], l: n[2] };
4048
+ default:
4049
+ return void 0;
3719
4050
  }
3720
4051
  }
3721
4052
  export {
4053
+ DEFAULT_TONE_MAPPING_MODE,
3722
4054
  EmptyModelError,
3723
4055
  InnerVizij,
3724
4056
  ShapeMaterial,
4057
+ TONE_MAPPING_LABELS,
4058
+ TONE_MAPPING_MODES,
3725
4059
  Vizij,
3726
4060
  VizijContext,
3727
4061
  VizijSlice,
3728
4062
  applyVizijBundle,
4063
+ buildSceneGraph,
3729
4064
  createVizijStore,
4065
+ deriveMorphFeatureKeys,
4066
+ deriveRootBounds,
3730
4067
  exportScene,
3731
4068
  extractVizijBundle,
4069
+ isToneMappingMode,
3732
4070
  loadGLTF,
3733
4071
  loadGLTFFromBlob,
3734
4072
  loadGLTFFromBlobWithBundle,
3735
4073
  loadGLTFWithBundle,
3736
- loadGltfFromBlob,
4074
+ normalizeToneMappingMode,
3737
4075
  parseGlbJsonChunk,
4076
+ patchGlbJson,
4077
+ patchVizijBundleMetadata,
4078
+ resolveToneMapping,
4079
+ sanitizeMorphKey,
3738
4080
  useDefaultVizijStore,
3739
4081
  useFeatures,
3740
4082
  useVizijStore,